managarten/packages/shared-gpu/src/image-client.ts
Till JS c67ed0df14 feat(gpu-server): add API key auth, VRAM management, and Piper TTS voices
- Add API key authentication to all GPU services (X-API-Key header)
  - /health and /docs remain public (no key needed)
  - Shared key configured via GPU_API_KEY env variable
- Add VRAM auto-unload for mana-image-gen (5min) and mana-stt (10min)
  - FLUX.2 pipeline freed after idle, recovering ~13GB VRAM
  - WhisperX models freed after idle, recovering ~3GB VRAM
- Install Piper TTS voices (Thorsten + Kerstin) for local German TTS
- Update @manacore/shared-gpu client to support apiKey parameter
- Add GPU_API_KEY to .env.development
- Document API auth and VRAM management in setup guide

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 21:54:35 +01:00

77 lines
2.2 KiB
TypeScript

import type {
GenerateImageOptions,
GenerateImageResult,
ImageGenHealthResponse,
GpuServiceConfig,
} from './types';
import { resolveServiceUrl } from './resolve-url';
export class ImageClient {
private baseUrl: string;
private timeout: number;
private apiKey?: string;
constructor(config: GpuServiceConfig) {
this.baseUrl = resolveServiceUrl(config, 'image');
this.timeout = config.timeout ?? 120_000;
this.apiKey = config.apiKey;
}
/** Generate an image from a text prompt. */
async generate(options: GenerateImageOptions): Promise<GenerateImageResult> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), this.timeout);
try {
const response = await fetch(`${this.baseUrl}/generate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(this.apiKey ? { 'X-API-Key': this.apiKey } : {}),
},
body: JSON.stringify({
prompt: options.prompt,
width: options.width ?? 1024,
height: options.height ?? 1024,
steps: options.steps ?? 4,
seed: options.seed,
output_format: options.outputFormat ?? 'png',
}),
signal: controller.signal,
});
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: response.statusText }));
throw new Error(
`Image generation error ${response.status}: ${(error as { detail: string }).detail}`
);
}
return (await response.json()) as GenerateImageResult;
} finally {
clearTimeout(timer);
}
}
/** Get the full URL for a generated image. */
imageUrl(relativePath: string): string {
return `${this.baseUrl}${relativePath}`;
}
/** Download a generated image as ArrayBuffer. */
async downloadImage(relativePath: string): Promise<ArrayBuffer> {
const response = await fetch(this.imageUrl(relativePath), {
signal: AbortSignal.timeout(30_000),
});
if (!response.ok) throw new Error(`Failed to download image: ${response.status}`);
return response.arrayBuffer();
}
/** Check if the image generation service is healthy. */
async health(): Promise<ImageGenHealthResponse> {
const response = await fetch(`${this.baseUrl}/health`, {
signal: AbortSignal.timeout(5000),
});
return (await response.json()) as ImageGenHealthResponse;
}
}