Back to skills
SkillHub ClubDesign ProductFull StackData / AIDesigner

image-generation

Generate images with Gemini (default) or fal.ai FLUX.2 klein 4B (--cheap for fast/low-cost). Generate videos with Grok Imagine (default) or fal.ai LTX-2 (--cheap). Use for: create image, generate visual, AI image generation, poster, video generation.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
20
Hot score
87
Updated
March 20, 2026
Overall rating
C1.8
Composite score
1.8
Best-practice grade
B73.6

Install command

npx @skill-hub/cli install aviz85-claude-skills-library-image-generation
imagevideoAIgenerationGeminifal.aiposter

Repository

aviz85/claude-skills-library

Skill path: skills/image-generation

Generate images with Gemini (default) or fal.ai FLUX.2 klein 4B (--cheap for fast/low-cost). Generate videos with Grok Imagine (default) or fal.ai LTX-2 (--cheap). Use for: create image, generate visual, AI image generation, poster, video generation.

Open repository

Best for

Primary workflow: Design Product.

Technical facets: Full Stack, Data / AI, Designer.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: aviz85.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install image-generation into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/aviz85/claude-skills-library before adding image-generation to shared team environments
  • Use image-generation for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: image-generation
description: "Generate images with Gemini (default) or fal.ai FLUX.2 klein 4B (--cheap for fast/low-cost). Generate videos with Grok Imagine (default) or fal.ai LTX-2 (--cheap). Use for: create image, generate visual, AI image generation, poster, video generation."
version: "1.0.0"
author: aviz85
tags: [image, video, AI, generation, Gemini, fal.ai, poster]
---

# Image Generation

Generate images using Google's Gemini model (default) or fal.ai FLUX.2 klein 4B (cheap mode).

## Quick Start

**MANDATORY:** Always use `-d` (destination) flag to specify output path. This avoids file management issues with `poster_0.jpg` in the scripts folder.

```bash
cd ~/.claude/skills/image-generation/scripts

# ALWAYS specify destination with -d flag
npx tsx generate_poster.ts -d /tmp/my-image.jpg "A futuristic city at sunset"

# Cheap mode: fal.ai FLUX.2 klein 4B (fast, lower cost)
npx tsx generate_poster.ts --cheap -d /tmp/city.jpg "A futuristic city at sunset"

# With aspect ratio
npx tsx generate_poster.ts -d /tmp/landscape.jpg --aspect 3:2 "A wide landscape poster"
npx tsx generate_poster.ts --cheap -d /tmp/story.jpg -a 9:16 "A vertical story format"

# With reference assets (image editing)
npx tsx generate_poster.ts -d /tmp/banner.jpg --assets "/path/to/avatar.jpg" "Create banner with character"
npx tsx generate_poster.ts --cheap -d /tmp/photo.jpg --assets "/path/to/image.jpg" "Turn this into a realistic photo"
```

**Why `-d` is mandatory:**
- Output goes to predictable location (e.g., `/tmp/` for temp images)
- Avoids hunting for `poster_0.jpg` in scripts folder
- Enables immediate use without file renaming

## Providers

| Provider | Flag | Use Case | Cost |
|----------|------|----------|------|
| **Gemini** | (default) | High quality, best results | Higher |
| **fal.ai klein 4B** | `--cheap` | Fast, budget-friendly | ~$0.003/image |

### Cheap Mode (`--cheap`)

Uses fal.ai FLUX.2 klein 4B - a distilled FLUX model optimized for speed and cost.

**Why use cheap mode?**
- **Cost**: ~$0.003 per image (vs Gemini's higher cost)
- **Speed**: Fast 4-step inference (~2-4 seconds)
- **Quality**: Good enough for drafts, social media, iterations
- **Batch friendly**: Generate many variations quickly

**Endpoints:**
- **Text-to-image**: `fal-ai/flux-2/klein/4b`
- **Image editing** (with --assets): `fal-ai/flux-2/klein/4b/edit`

**Best for:**
- Quick iterations and previews
- Social media content
- Concept exploration
- Batch generation (10+ images)

**Use Gemini instead for:**
- Final production assets
- Complex compositions
- Text-heavy images (especially Hebrew)

## Quality

Control image resolution with `--quality` or `-q`:

| Quality | Resolution | Use Case |
|---------|------------|----------|
| `1K` | 1024px | **Default** - fast, good for web |
| `2K` | 2048px | High quality - print, detailed posters |

```bash
npx tsx generate_poster.ts -q 2K "High quality poster"
```

## Aspect Ratio

**IMPORTANT:** Always use the default 3:2 aspect ratio unless the user explicitly requests a different format (like "vertical", "story", "square", etc.). Do NOT change the aspect ratio on your own.

Control image dimensions with `--aspect` or `-a`:

| Ratio | Use Case |
|-------|----------|
| `3:2` | Horizontal **(DEFAULT - use this unless user specifies otherwise)** |
| `1:1` | Square - Instagram, profile pics |
| `2:3` | Vertical - Pinterest, posters |
| `16:9` | Wide - YouTube thumbnails, headers |
| `9:16` | Tall - Stories, reels, TikTok |

```bash
npx tsx generate_poster.ts --aspect 3:2 "Your prompt"
npx tsx generate_poster.ts -a 16:9 "Your prompt"
```

## Adding Assets (Reference Images)

Use `--assets` with full paths to include reference images:

```bash
# Single asset
npx tsx generate_poster.ts --assets "/full/path/to/image.jpg" "Your prompt"

# Multiple assets (comma-separated)
npx tsx generate_poster.ts --assets "/path/a.jpg,/path/b.png" "Use both images"

# With cheap mode - uses fal.ai edit endpoint
npx tsx generate_poster.ts --cheap --assets "/path/to/image.jpg" "Turn this into a painting"
```

**Supported formats:** `.jpg`, `.jpeg`, `.png`, `.webp`, `.gif`

**IMPORTANT:** Assets are NOT automatically included. You must explicitly pass them via `--assets`.

## Destination (MANDATORY)

**ALWAYS** use `--destination` or `-d` to specify output path:

```bash
# REQUIRED: Always specify destination
npx tsx generate_poster.ts -d /tmp/poster.jpg "My poster"

# If file exists, auto-adds suffix: poster_1.jpg, poster_2.jpg, etc.
```

**Features:**
- Auto-creates parent directories if needed
- Collision avoidance: if file exists, adds `_1`, `_2`, etc.
- Use `/tmp/` for temporary images that will be uploaded elsewhere

## API Configuration

Create `scripts/.env`:
```
GEMINI_API_KEY=your_gemini_api_key
FAL_KEY=your_fal_api_key
XAI_API_KEY=your_xai_api_key
```

## Hebrew/RTL Content

**IMPORTANT:** When the image contains Hebrew text, you MUST add the following sentence to the prompt:

```
"CRITICAL: Layout is RTL (right-to-left). All text in Hebrew. Visual flow, reading order, and panel sequence go from RIGHT to LEFT."
```

Copy this exact sentence and paste it at the BEGINNING of your prompt. Without it, the image will render left-to-right which is wrong for Hebrew content.

## WOW Mode

When user asks for "wow mode" or wants maximum visual impact, add these epic elements to the prompt:

```
"EPIC CINEMATIC WOW: Amazed expression, mind-blown reaction, intense VFX - shattered glass particles, explosive energy bursts, volumetric light rays, dramatic lens flares, particle explosions, motion blur streaks, holographic glitches, electric sparks, cinematic color grading, dramatic rim lighting, depth of field bokeh, anamorphic lens effects. Maximum visual spectacle."
```

## Output

- Files saved to path specified by `-d` flag (MANDATORY)
- Aspect ratio: Configurable via `--aspect` (default: 3:2)
- Quality: 1K (1024px on longest edge)

---

# Video Generation

Generate videos using xAI Grok Imagine Video (default) or fal.ai LTX-2 (cheap mode).

## Quick Start

```bash
cd ~/.claude/skills/image-generation/scripts

# Text-to-video (Grok Imagine - high quality, 720p)
npx tsx generate_video.ts -d /tmp/video.mp4 "A futuristic city at sunset, cinematic drone shot"

# Cheap mode: fal.ai LTX-2 (fast, low cost)
npx tsx generate_video.ts --cheap -d /tmp/video.mp4 "A futuristic city at sunset"

# Image-to-video (animate a still image)
npx tsx generate_video.ts -d /tmp/animated.mp4 --image /path/to/image.jpg "Slow zoom with particles floating"
npx tsx generate_video.ts --cheap -d /tmp/animated.mp4 --image /path/to/image.jpg "Gentle camera movement"

# Custom duration and aspect ratio
npx tsx generate_video.ts -d /tmp/long.mp4 -t 10 -a 9:16 "Vertical video for stories"
```

## Video Providers

| Provider | Flag | Quality | Duration | Resolution | Cost |
|----------|------|---------|----------|------------|------|
| **Grok Imagine** | (default) | High, cinematic | 1-15s | 480p/720p | Per-second billing |
| **fal.ai LTX-2** | `--cheap` | Good, fast | 1-10s | ~480p | Budget-friendly |

## Video Options

| Option | Flag | Default | Description |
|--------|------|---------|-------------|
| Prompt | (positional) | required | Text description of the video |
| Destination | `-d` | required | Output file path (.mp4) |
| Duration | `-t` | 5 | Duration in seconds |
| Aspect ratio | `-a` | 16:9 | 16:9, 9:16, 1:1, 4:3, 3:4, 3:2, 2:3 |
| Resolution | `-r` | 720p | 480p or 720p (Grok only) |
| Image source | `--image` | none | Source image for image-to-video |
| Cheap mode | `--cheap` | false | Use LTX-2 instead of Grok |


---

## Skill Companion Files

> Additional files collected from the skill directory layout.

### scripts/generate_poster.ts

```typescript
// Image Generation Script
// Supports: Google Gemini (default) and fal.ai FLUX.2 klein 4B (--cheap)
//
// Install dependencies:
// npm install @google/genai @fal-ai/client mime dotenv
// npm install -D @types/node typescript ts-node

import {
  GoogleGenAI,
  createUserContent,
  createPartFromUri,
} from '@google/genai';
import { fal } from '@fal-ai/client';
import mime from 'mime';
import { writeFile, copyFileSync, existsSync, readFileSync } from 'fs';
import * as dotenv from 'dotenv';
import * as path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// Load environment variables
dotenv.config();

function saveBinaryFile(fileName: string, content: Buffer) {
  writeFile(fileName, content, (err) => {
    if (err) {
      console.error(`Error writing file ${fileName}:`, err);
      return;
    }
    console.log(`File ${fileName} saved to file system.`);
  });
}

// Copy file to destination with collision avoidance
function copyToDestination(sourcePath: string, destPath: string): string {
  let finalPath = destPath;

  if (existsSync(destPath)) {
    // File exists, add suffix
    const dir = path.dirname(destPath);
    const ext = path.extname(destPath);
    const base = path.basename(destPath, ext);
    let counter = 1;

    while (existsSync(finalPath)) {
      finalPath = path.join(dir, `${base}_${counter}${ext}`);
      counter++;
    }
  }

  copyFileSync(sourcePath, finalPath);
  return finalPath;
}

// Map aspect ratio to fal.ai image_size enum
function aspectToFalSize(aspectRatio: string): string {
  const mapping: Record<string, string> = {
    '1:1': 'square',
    '3:2': 'landscape_4_3',
    '2:3': 'portrait_4_3',
    '16:9': 'landscape_16_9',
    '9:16': 'portrait_16_9',
  };
  return mapping[aspectRatio] || 'landscape_4_3';
}

// Parse command line arguments
interface ParsedArgs {
  prompt: string;
  aspectRatio: string;
  assets: string[];
  destination: string | null;
  quality: string;
  cheap: boolean;
}

function parseArgs(args: string[]): ParsedArgs {
  let aspectRatio = '3:2'; // default - best for most social media
  let assets: string[] = [];
  let destination: string | null = null;
  let quality = '1K'; // default quality
  let cheap = false;
  let promptParts: string[] = [];

  for (let i = 0; i < args.length; i++) {
    if (args[i] === '--aspect' || args[i] === '-a') {
      if (args[i + 1]) {
        aspectRatio = args[i + 1];
        i++; // skip next arg
      }
    } else if (args[i] === '--assets') {
      if (args[i + 1]) {
        // Support comma-separated assets or full paths
        assets = args[i + 1].split(',').map(a => a.trim());
        i++; // skip next arg
      }
    } else if (args[i] === '--destination' || args[i] === '-d') {
      if (args[i + 1]) {
        destination = args[i + 1];
        i++; // skip next arg
      }
    } else if (args[i] === '--quality' || args[i] === '-q') {
      if (args[i + 1]) {
        quality = args[i + 1].toUpperCase();
        i++; // skip next arg
      }
    } else if (args[i] === '--cheap' || args[i] === '-c') {
      cheap = true;
    } else if (args[i] === '--save-to-gallery') {
      if (args[i + 1]) {
        i++; // skip next arg (gallery name)
      }
    } else {
      promptParts.push(args[i]);
    }
  }

  return { prompt: promptParts.join(' '), aspectRatio, assets, destination, quality, cheap };
}

// Generate with fal.ai FLUX.2 klein 4B
async function generateWithFal(
  prompt: string,
  aspectRatio: string,
  assets: string[],
  destination: string | null
): Promise<void> {
  if (!process.env.FAL_KEY) {
    console.error('Error: FAL_KEY not found in environment variables');
    console.error('Please add FAL_KEY=your_api_key to .env file');
    process.exit(1);
  }

  // Configure fal client
  fal.config({
    credentials: process.env.FAL_KEY,
  });

  const imageSize = aspectToFalSize(aspectRatio);
  console.log(`Using fal.ai FLUX.2 klein 4B (cheap mode)`);
  console.log(`Aspect ratio: ${aspectRatio} -> ${imageSize}`);

  let result: any;

  if (assets.length > 0) {
    // Image editing mode
    console.log(`Mode: Image editing with ${assets.length} reference image(s)`);

    // Upload images to fal storage
    const imageUrls: string[] = [];
    for (const assetPath of assets) {
      console.log(`Uploading asset: ${assetPath}...`);
      const fileBuffer = readFileSync(assetPath);
      const file = new File([fileBuffer], path.basename(assetPath), {
        type: mime.getType(assetPath) || 'image/jpeg',
      });
      const url = await fal.storage.upload(file);
      imageUrls.push(url);
      console.log(`Uploaded: ${url}`);
    }

    result = await fal.subscribe('fal-ai/flux-2/klein/4b/edit', {
      input: {
        prompt,
        image_urls: imageUrls,
        image_size: imageSize,
        num_inference_steps: 4,
        num_images: 1,
        output_format: 'jpeg',
      },
      logs: true,
      onQueueUpdate: (update) => {
        if (update.status === 'IN_PROGRESS' && update.logs) {
          update.logs.map((log) => log.message).forEach(console.log);
        }
      },
    });
  } else {
    // Text-to-image mode
    console.log(`Mode: Text-to-image`);

    result = await fal.subscribe('fal-ai/flux-2/klein/4b', {
      input: {
        prompt,
        image_size: imageSize,
        num_inference_steps: 4,
        num_images: 1,
        output_format: 'jpeg',
      },
      logs: true,
      onQueueUpdate: (update) => {
        if (update.status === 'IN_PROGRESS' && update.logs) {
          update.logs.map((log) => log.message).forEach(console.log);
        }
      },
    });
  }

  // Process results
  if (result.data?.images && result.data.images.length > 0) {
    for (let i = 0; i < result.data.images.length; i++) {
      const image = result.data.images[i];
      const imageUrl = image.url;

      // Download the image
      console.log(`Downloading image from: ${imageUrl}`);
      const response = await fetch(imageUrl);
      const buffer = Buffer.from(await response.arrayBuffer());

      const localFile = `poster_${i}.jpg`;
      saveBinaryFile(localFile, buffer);

      // Copy to destination if specified
      if (destination) {
        await new Promise(resolve => setTimeout(resolve, 500));
        try {
          const finalPath = copyToDestination(localFile, destination);
          console.log(`Copied to: ${finalPath}`);
        } catch (err) {
          console.error(`Error copying to destination:`, err);
        }
      }
    }
  } else {
    console.error('No images generated');
    console.log('Result:', JSON.stringify(result, null, 2));
  }
}

// Generate with Google Gemini (original implementation)
async function generateWithGemini(
  prompt: string,
  aspectRatio: string,
  assets: string[],
  destination: string | null,
  quality: string
): Promise<void> {
  if (!process.env.GEMINI_API_KEY) {
    console.error('Error: GEMINI_API_KEY not found in environment variables');
    console.error('Please create a .env file with GEMINI_API_KEY=your_api_key');
    process.exit(1);
  }

  const ai = new GoogleGenAI({
    apiKey: process.env.GEMINI_API_KEY,
  });

  // Upload asset images if provided
  const uploadedAssets: Array<{ name: string; uri: string; mimeType: string }> = [];

  for (const assetPath of assets) {
    try {
      console.log(`Uploading asset: ${assetPath}...`);
      const uploaded = await ai.files.upload({
        file: assetPath,
        config: { mimeType: mime.getType(assetPath) || 'image/jpeg' },
      });
      uploadedAssets.push({
        name: uploaded.name,
        uri: uploaded.uri,
        mimeType: uploaded.mimeType || 'image/jpeg',
      });
      console.log(`Asset uploaded successfully: ${uploaded.name}`);
    } catch (error) {
      console.error(`Warning: Could not upload asset ${assetPath}:`, error);
    }
  }

  const config = {
    responseModalities: [
        'IMAGE',
        'TEXT',
    ],
    imageConfig: {
      aspectRatio: aspectRatio,
      imageSize: quality, // 1K (default) or 2K
    },
  };

  const model = 'gemini-3-pro-image-preview';

  // Build content parts - include uploaded assets first, then prompt
  const contentParts: Array<any> = [];
  for (const asset of uploadedAssets) {
    contentParts.push(createPartFromUri(asset.uri, asset.mimeType));
  }
  if (uploadedAssets.length > 0) {
    contentParts.push('Use the provided reference image(s) as specified in the prompt.');
  }
  contentParts.push(prompt);

  const contents = createUserContent(contentParts);

  console.log(`Generating with Gemini...`);

  const response = await ai.models.generateContentStream({
    model,
    config,
    contents,
  });

  let fileIndex = 0;
  for await (const chunk of response) {
    if (!chunk.candidates || !chunk.candidates[0].content || !chunk.candidates[0].content.parts) {
      continue;
    }
    if (chunk.candidates?.[0]?.content?.parts?.[0]?.inlineData) {
      const fileName = `poster_${fileIndex++}`;
      const inlineData = chunk.candidates[0].content.parts[0].inlineData;
      const fileExtension = mime.getExtension(inlineData.mimeType || '');
      const buffer = Buffer.from(inlineData.data || '', 'base64');
      const localFile = `${fileName}.${fileExtension}`;
      saveBinaryFile(localFile, buffer);

      // Copy to destination if specified
      if (destination) {
        // Wait a bit for file to be written
        await new Promise(resolve => setTimeout(resolve, 500));
        try {
          const finalPath = copyToDestination(localFile, destination);
          console.log(`Copied to: ${finalPath}`);
        } catch (err) {
          console.error(`Error copying to destination:`, err);
        }
      }
    }
    else {
      console.log(chunk.text);
    }
  }

  // Clean up: delete uploaded asset files
  for (const asset of uploadedAssets) {
    try {
      await ai.files.delete({ name: asset.name });
      console.log(`Asset ${asset.name} cleaned up from server.`);
    } catch (error) {
      console.error(`Warning: Could not delete asset ${asset.name}:`, error);
    }
  }
}

async function main() {
  // Get prompt and options from command-line arguments
  const { prompt, aspectRatio, assets, destination, quality, cheap } = parseArgs(process.argv.slice(2));

  if (!prompt) {
    console.error('Error: Please provide a prompt as a command-line argument');
    console.error('Usage: npx ts-node generate_poster.ts [OPTIONS] "your prompt here"');
    console.error('');
    console.error('Options:');
    console.error('  --cheap, -c        Use fal.ai FLUX.2 klein 4B (fast, low cost)');
    console.error('  --aspect, -a       Aspect ratio (1:1, 3:2, 2:3, 16:9, 9:16) - default: 3:2');
    console.error('  --quality, -q      Image quality (1K, 2K) - default: 1K (Gemini only)');
    console.error('  --assets           Comma-separated paths to reference images');
    console.error('  --destination, -d  Copy output to this path (auto-suffixes if exists)');
    console.error('');
    console.error('Examples:');
    console.error('  npx ts-node generate_poster.ts "A sunset over mountains"');
    console.error('  npx ts-node generate_poster.ts --cheap "A sunset over mountains"');
    console.error('  npx ts-node generate_poster.ts --aspect 16:9 "A wide landscape"');
    console.error('  npx ts-node generate_poster.ts -d /path/to/output.jpg "My poster"');
    console.error('  npx ts-node generate_poster.ts --cheap --assets "/path/to/image.jpg" "Edit this"');
    process.exit(1);
  }

  // Validate aspect ratio
  const validRatios = ['1:1', '3:2', '2:3', '16:9', '9:16'];
  if (!validRatios.includes(aspectRatio)) {
    console.error(`Error: Invalid aspect ratio "${aspectRatio}"`);
    console.error(`Valid options: ${validRatios.join(', ')}`);
    process.exit(1);
  }

  console.log(`Provider: ${cheap ? 'fal.ai FLUX.2 klein 4B' : 'Google Gemini'}`);
  console.log(`Aspect ratio: ${aspectRatio}`);
  if (!cheap) console.log(`Quality: ${quality}`);
  if (assets.length > 0) {
    console.log(`Assets: ${assets.join(', ')}`);
  }
  if (destination) {
    console.log(`Destination: ${destination}`);
  }
  console.log(`Prompt: "${prompt}"`);
  console.log('');

  if (cheap) {
    await generateWithFal(prompt, aspectRatio, assets, destination);
  } else {
    await generateWithGemini(prompt, aspectRatio, assets, destination, quality);
  }
}

main();

```

### scripts/generate_video.ts

```typescript
// Video Generation Script
// Supports: xAI Grok Imagine Video (default) and fal.ai LTX-2 (--cheap)
//
// Usage:
//   npx tsx generate_video.ts -d /tmp/video.mp4 "A futuristic city at sunset"
//   npx tsx generate_video.ts --cheap -d /tmp/video.mp4 "A futuristic city"
//   npx tsx generate_video.ts -d /tmp/video.mp4 --image /path/to/image.jpg "Animate this scene"

import { fal } from '@fal-ai/client';
import { writeFileSync, readFileSync, existsSync, copyFileSync, mkdirSync } from 'fs';
import * as dotenv from 'dotenv';
import * as path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

dotenv.config();

// ─── Argument Parsing ───

interface ParsedArgs {
  prompt: string;
  aspectRatio: string;
  duration: number;
  resolution: string;
  image: string | null;
  destination: string | null;
  cheap: boolean;
}

function parseArgs(args: string[]): ParsedArgs {
  let aspectRatio = '16:9';
  let duration = 5;
  let resolution = '720p';
  let image: string | null = null;
  let destination: string | null = null;
  let cheap = false;
  let promptParts: string[] = [];

  for (let i = 0; i < args.length; i++) {
    if (args[i] === '--aspect' || args[i] === '-a') {
      aspectRatio = args[++i];
    } else if (args[i] === '--duration' || args[i] === '-t') {
      duration = parseInt(args[++i], 10);
    } else if (args[i] === '--resolution' || args[i] === '-r') {
      resolution = args[++i];
    } else if (args[i] === '--image' || args[i] === '--assets') {
      image = args[++i];
    } else if (args[i] === '--destination' || args[i] === '-d') {
      destination = args[++i];
    } else if (args[i] === '--cheap' || args[i] === '-c') {
      cheap = true;
    } else {
      promptParts.push(args[i]);
    }
  }

  return { prompt: promptParts.join(' '), aspectRatio, duration, resolution, image, destination, cheap };
}

// ─── File Helpers ───

function copyToDestination(sourcePath: string, destPath: string): string {
  // Create parent directory if needed
  const dir = path.dirname(destPath);
  if (!existsSync(dir)) {
    mkdirSync(dir, { recursive: true });
  }

  let finalPath = destPath;
  if (existsSync(destPath)) {
    const ext = path.extname(destPath);
    const base = path.basename(destPath, ext);
    let counter = 1;
    while (existsSync(finalPath)) {
      finalPath = path.join(dir, `${base}_${counter}${ext}`);
      counter++;
    }
  }
  copyFileSync(sourcePath, finalPath);
  return finalPath;
}

// ─── Grok Imagine Video (xAI) ───

async function generateWithGrok(args: ParsedArgs): Promise<void> {
  const apiKey = process.env.XAI_API_KEY;
  if (!apiKey) {
    console.error('Error: XAI_API_KEY not found in .env');
    console.error('Get your key at https://console.x.ai/team/default/api-keys');
    process.exit(1);
  }

  const baseUrl = 'https://api.x.ai/v1';
  const headers = {
    'Authorization': `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  };

  // Build request body
  const body: any = {
    model: 'grok-imagine-video',
    prompt: args.prompt,
    duration: args.duration,
    aspect_ratio: args.aspectRatio,
    resolution: args.resolution,
  };

  // Image-to-video mode
  if (args.image) {
    console.log(`Mode: Image-to-Video`);
    console.log(`Source image: ${args.image}`);
    const imageBuffer = readFileSync(args.image);
    const base64 = imageBuffer.toString('base64');
    const ext = path.extname(args.image).toLowerCase().replace('.', '');
    const mimeType = ext === 'png' ? 'image/png' : 'image/jpeg';
    body.image = { url: `data:${mimeType};base64,${base64}` };
  } else {
    console.log(`Mode: Text-to-Video`);
  }

  console.log(`Provider: xAI Grok Imagine Video`);
  console.log(`Duration: ${args.duration}s | Ratio: ${args.aspectRatio} | Resolution: ${args.resolution}`);
  console.log(`Prompt: "${args.prompt}"`);
  console.log('');

  // Step 1: Start generation
  console.log('Starting video generation...');
  const startRes = await fetch(`${baseUrl}/videos/generations`, {
    method: 'POST',
    headers,
    body: JSON.stringify(body),
  });

  if (!startRes.ok) {
    const err = await startRes.text();
    console.error(`API error (${startRes.status}): ${err}`);
    process.exit(1);
  }

  const startData = await startRes.json() as any;
  const requestId = startData.request_id;
  console.log(`Request ID: ${requestId}`);

  // Step 2: Poll for completion
  console.log('Generating video (this may take a few minutes)...');
  const maxWait = 10 * 60 * 1000; // 10 minutes
  const pollInterval = 3000; // 3 seconds
  const startTime = Date.now();

  while (Date.now() - startTime < maxWait) {
    await new Promise(r => setTimeout(r, pollInterval));

    const pollRes = await fetch(`${baseUrl}/videos/${requestId}`, { headers: { 'Authorization': `Bearer ${apiKey}` } });
    if (!pollRes.ok) {
      console.error(`Poll error (${pollRes.status}): ${await pollRes.text()}`);
      continue;
    }

    const pollData = await pollRes.json() as any;
    const elapsed = Math.round((Date.now() - startTime) / 1000);

    // API returns video object directly when done (no status field)
    if (pollData.video?.url) {
      console.log(`\nVideo ready! (took ${elapsed}s)`);
      const videoUrl = pollData.video.url;

      // Download video
      console.log('Downloading video...');
      const videoRes = await fetch(videoUrl);
      const videoBuffer = Buffer.from(await videoRes.arrayBuffer());
      const localFile = 'video_output.mp4';
      writeFileSync(localFile, videoBuffer);
      console.log(`Saved: ${localFile} (${(videoBuffer.length / 1024 / 1024).toFixed(1)}MB)`);

      if (args.destination) {
        const finalPath = copyToDestination(localFile, args.destination);
        console.log(`Copied to: ${finalPath}`);
      }
      return;
    } else if (pollData.status === 'expired' || pollData.error) {
      console.error('Video generation failed:', JSON.stringify(pollData));
      process.exit(1);
    } else {
      const status = pollData.status || 'processing';
      process.stdout.write(`\r  Waiting... ${elapsed}s elapsed (status: ${status})`);
    }
  }

  console.error('\nTimeout: video generation took too long (>10 minutes)');
  process.exit(1);
}

// ─── LTX-2 via fal.ai (cheap mode) ───

async function generateWithLtx(args: ParsedArgs): Promise<void> {
  if (!process.env.FAL_KEY) {
    console.error('Error: FAL_KEY not found in .env');
    process.exit(1);
  }

  fal.config({ credentials: process.env.FAL_KEY });

  console.log(`Provider: fal.ai LTX-2 (cheap mode)`);
  console.log(`Duration: ${args.duration}s | Ratio: ${args.aspectRatio}`);
  console.log(`Prompt: "${args.prompt}"`);
  console.log('');

  // Map aspect ratio to resolution
  const resMap: Record<string, { width: number; height: number }> = {
    '16:9': { width: 768, height: 432 },
    '9:16': { width: 432, height: 768 },
    '1:1': { width: 512, height: 512 },
    '4:3': { width: 640, height: 480 },
    '3:4': { width: 480, height: 640 },
    '3:2': { width: 640, height: 432 },
    '2:3': { width: 432, height: 640 },
  };
  const res = resMap[args.aspectRatio] || resMap['16:9'];

  let result: any;

  if (args.image) {
    // Image-to-video
    console.log(`Mode: Image-to-Video`);
    const fileBuffer = readFileSync(args.image);
    const file = new File([fileBuffer], path.basename(args.image), {
      type: 'image/jpeg',
    });
    const imageUrl = await fal.storage.upload(file);
    console.log(`Image uploaded: ${imageUrl}`);

    result = await fal.subscribe('fal-ai/ltx-2/image-to-video', {
      input: {
        prompt: args.prompt,
        image_url: imageUrl,
        num_frames: Math.min(args.duration * 24, 257),
        width: res.width,
        height: res.height,
      },
      logs: true,
      onQueueUpdate: (update) => {
        if (update.status === 'IN_PROGRESS' && update.logs) {
          update.logs.map((log) => log.message).forEach(console.log);
        }
      },
    });
  } else {
    // Text-to-video
    console.log(`Mode: Text-to-Video`);

    result = await fal.subscribe('fal-ai/ltx-2/text-to-video', {
      input: {
        prompt: args.prompt,
        num_frames: Math.min(args.duration * 24, 257),
        width: res.width,
        height: res.height,
      },
      logs: true,
      onQueueUpdate: (update) => {
        if (update.status === 'IN_PROGRESS' && update.logs) {
          update.logs.map((log) => log.message).forEach(console.log);
        }
      },
    });
  }

  // Download result
  const videoUrl = result.data?.video?.url;
  if (!videoUrl) {
    console.error('No video URL in response');
    console.log(JSON.stringify(result, null, 2));
    process.exit(1);
  }

  console.log('Downloading video...');
  const videoRes = await fetch(videoUrl);
  const videoBuffer = Buffer.from(await videoRes.arrayBuffer());
  const localFile = 'video_output.mp4';
  writeFileSync(localFile, videoBuffer);
  console.log(`Saved: ${localFile} (${(videoBuffer.length / 1024 / 1024).toFixed(1)}MB)`);

  if (args.destination) {
    const finalPath = copyToDestination(localFile, args.destination);
    console.log(`Copied to: ${finalPath}`);
  }
}

// ─── Main ───

async function main() {
  const args = parseArgs(process.argv.slice(2));

  if (!args.prompt && !args.image) {
    console.error('Usage: npx tsx generate_video.ts [OPTIONS] "your prompt"');
    console.error('');
    console.error('Options:');
    console.error('  --cheap, -c          Use fal.ai LTX-2 (fast, low cost)');
    console.error('  --image <path>       Source image for image-to-video');
    console.error('  --duration, -t <s>   Duration in seconds (default: 5)');
    console.error('  --aspect, -a <ratio> Aspect ratio (default: 16:9)');
    console.error('  --resolution, -r     Resolution: 480p or 720p (Grok only, default: 720p)');
    console.error('  --destination, -d    Output file path');
    console.error('');
    console.error('Providers:');
    console.error('  Default:  xAI Grok Imagine Video (high quality, 720p, up to 15s)');
    console.error('  --cheap:  fal.ai LTX-2 (fast, budget-friendly)');
    process.exit(1);
  }

  if (args.cheap) {
    await generateWithLtx(args);
  } else {
    await generateWithGrok(args);
  }
}

main();

```

### scripts/package.json

```json
{
  "name": "image-generation-scripts",
  "version": "1.0.0",
  "description": "Image and video generation with Gemini, fal.ai, and Grok",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "aviz85",
  "license": "ISC",
  "type": "module",
  "dependencies": {
    "@fal-ai/client": "^1.8.3",
    "@google/genai": "^1.34.0",
    "@types/node": "^25.0.3",
    "dotenv": "^17.2.3",
    "mime": "^4.1.0",
    "ts-node": "^10.9.2",
    "typescript": "^5.9.3"
  }
}

```

image-generation | SkillHub