npm package: @geepers/media
Description: Headless TypeScript library for LLM media generation — images, video, and speech
Author: Luke Steuber
Latest version: 1.0.0

# @geepers/media

[![npm](https://img.shields.io/npm/v/@geepers/media)](https://www.npmjs.com/package/@geepers/media)
[![license](https://img.shields.io/npm/l/@geepers/media)](./LICENSE)

Headless TypeScript library for LLM media generation — images, video, and speech. Drop it into any project; bring your own UI.

Works with any API gateway that follows the standard REST shape. No framework dependencies. No React. Just fetch.

## Install

```bash
npm install @geepers/media
# or
pnpm add @geepers/media
```

## Quick start

```ts
import { ImageGenerator, TTSGenerator, VideoGenerator } from '@geepers/media';

const config = { baseUrl: 'https://api.example.com/v1' };

// Image
const images = new ImageGenerator(config);
const result = await images.generate({ prompt: 'a red fox in the snow', provider: 'openai' });
console.log(result[0].url);

// Speech
const tts = new TTSGenerator(config);
const audio = await tts.generate({ text: 'Hello world', provider: 'openai', voice: 'alloy' });
console.log(audio.audioUrl);

// Video
const video = new VideoGenerator(config);
const job = await video.generate({ prompt: 'ocean waves at sunset', provider: 'xai' });
console.log(job.videoUrl);
```

## ImageGenerator

Supports URL and base64 image responses, seed control, batch generation with per-image callbacks, and automatic retry on moderation failures (when you supply a rewrite function).

```ts
import { ImageGenerator } from '@geepers/media';

const gen = new ImageGenerator({
  baseUrl: 'https://api.example.com/v1',
  headers: { 'X-API-Key': process.env.API_KEY! },
});

// Single image
const [img] = await gen.generate({
  prompt: 'a geometric pattern in red and black',
  provider: 'xai',
  model: 'aurora',
  seed: 42,
  params: { aspectRatio: '16:9', resolution: '2k' },
});

// Batch — images stream in via onImage as each one finishes
const { images, successCount } = await gen.generateBatch({
  prompt: 'abstract watercolor landscape',
  provider: 'openai',
  quantity: 8,
  params: { size: '1024x1024', quality: 'high' },
  onProgress: (done, total) => console.log(`${done}/${total}`),
  onImage: (img) => saveToGallery(img),
});
```

### Provider params

| Provider | Type | Key params |
|----------|------|-----------|
| xAI | `XaiImageParams` | `aspectRatio`, `resolution` |
| OpenAI (DALL-E 3 / gpt-image-1) | `OpenAiImageParams` | `size`, `quality`, `style` |
| Gemini | `GeminiImageParams` | `aspectRatio`, `negativePrompt` |

## VideoGenerator

Two usage patterns: fire-and-poll for streaming UI updates, or `generate()` for a simple await.

```ts
import { VideoGenerator } from '@geepers/media';

const gen = new VideoGenerator({
  baseUrl: 'https://api.example.com/v1',
  pollIntervalMs: 5_000,
  pollTimeoutMs: 600_000,
});

// Wait for completion
const job = await gen.generate(
  { prompt: 'timelapse of clouds', provider: 'xai', params: { duration: 8, aspectRatio: '16:9' } },
  (update) => console.log(update.status), // called on each status change
);
console.log(job.videoUrl);

// Fire-and-poll (useful when you want to update UI manually)
const pending = await gen.start({ prompt: 'ocean waves', provider: 'openai' });
const { videoUrl } = await gen.poll(pending.requestId, pending.provider, (status, url) => {
  setStatus(status);
  if (url) setVideoUrl(url);
});

// Extend an existing xAI video
const extended = await gen.extend(job.videoUrl!, 'xai');
await gen.poll(extended.requestId, 'xai');
```

## TTSGenerator

Multi-provider, multi-voice TTS with batch support and xAI speech tags.

```ts
import { TTSGenerator, wrapWithTag } from '@geepers/media';

const tts = new TTSGenerator({ baseUrl: 'https://api.example.com/v1' });

// Fetch available providers and voices
const { providers } = await tts.getVoiceConfig();

// Single clip
const audio = await tts.generate({
  text: 'Welcome back.',
  provider: 'openai',
  voice: 'nova',
  model: 'tts-1-hd',
  speed: 1.1,
});

// xAI speech tags
const taggedText = `Hello. ${wrapWithTag('', 'pause')} ${wrapWithTag('This part is quiet.', 'whisper')}`;
const xaiAudio = await tts.generate({
  text: taggedText,
  provider: 'xai',
  voice: 'Aria',
  codec: 'mp3',
  sampleRate: 24000,
  language: 'en',
});

// Batch
const { audio: clips } = await tts.generateBatch({
  provider: 'openai',
  voice: 'alloy',
  texts: ['First sentence.', 'Second sentence.', 'Third sentence.'],
  onAudio: (clip) => console.log('ready:', clip.audioUrl),
});
```

## withRetry / withAutoRetry

Standalone retry utilities — use them with any async function, not just media generation.

```ts
import { withRetry, withAutoRetry } from '@geepers/media';

// Basic exponential backoff
const data = await withRetry(() => fetch(url).then(r => r.json()), {
  maxRetries: 3,
  baseDelayMs: 500,
  onRetry: (attempt, err) => console.warn(`Retry ${attempt}:`, err),
});

// Moderation-aware rewrite-and-retry
const { result, attempts } = await withAutoRetry(
  (prompt) => generateImage(prompt),
  'original prompt text',
  {
    maxRetries: 3,
    rewritePrompt: async (prompt, attempt) => callMyRewriter(prompt, attempt),
    onRewriteSuccess: (original, rewritten) => saveRuleToDb(original, rewritten),
  },
);
```

## API reference

Full TypeScript types are exported for every class, method, and option. Run `tsc` or check `dist/*.d.ts` for the full interface surface.

## License

MIT — [Luke Steuber](https://lukesteuber.com)