diff --git a/packages/imajin-client/src/index.ts b/packages/imajin-client/src/index.ts deleted file mode 100644 index 94855b5f..00000000 --- a/packages/imajin-client/src/index.ts +++ /dev/null @@ -1,603 +0,0 @@ -/** - * Unified client for Imajin image generation - * - * Primary usage - call the main orchestrator service: - * ```ts - * import { ImajinClient } from '@lilith/imajin-client'; - * - * const client = new ImajinClient('http://localhost:8080'); - * const result = await client.generate({ - * category: 'escort', - * city: 'reykjavik', - * role: 'hero', - * }); - * ``` - * - * Advanced usage - direct access to individual services: - * ```ts - * import { PromptClient, DiffusionClient, ProcessingClient } from '@lilith/imajin-client'; - * - * const promptClient = new PromptClient.ImagegenAssistantClient({ baseUrl: '...' }); - * const diffusionClient = new DiffusionClient.ImageGenerationClient({ ... }); - * const processingClient = ProcessingClient.createLocalClient(); - * ``` - */ - -// ============================================================================= -// Main Imajin Client (Recommended) -// ============================================================================= - -export interface GenerateRequest { - category: string; - city: string; - role: string; - filters?: string[]; - modelOverride?: string; - skipProcessing?: boolean; -} - -export interface GenerateResponse { - success: boolean; - imageUrl?: string; - imageBase64?: string; - metadata?: Record; - error?: string; -} - -export interface AnalyzeRequest { - category: string; - city: string; - role: string; - filters?: string[]; -} - -export interface AnalyzeResponse { - imageModel: string; - maturity: string; - subjectCount: number; - subjectGenders: string[]; - requiresClientFigure: boolean; - prompt: string; - negativePrompt: string; - aestheticKeywords: string[]; - reasoning: string; -} - -export interface HealthResponse { - status: 'healthy' | 'degraded' | 'unhealthy'; - services: Record; -} - -export interface ImajinClientOptions { - timeout?: number; -} - -// ============================================================================= -// Explicit Configuration Types (from @lilith/imajin-config) -// ============================================================================= - -export type SubjectGender = 'male' | 'female' | 'nonbinary'; -export type PoseType = 'standing' | 'sitting' | 'walking' | 'running' | 'lying' | 'kneeling' | 'leaning' | 'custom'; - -/** - * OpenPose-compatible keypoint for custom poses. - */ -export interface PoseKeypoint { - name: string; - x: number; // 0-1 normalized - y: number; // 0-1 normalized - confidence?: number; -} - -/** - * Explicit pose configuration. - * - * Use PoseGallery from @lilith/imajin-config to generate these: - * ```ts - * import { PoseGallery } from '@lilith/imajin-config'; - * const pose = PoseGallery.helpers.randomPose('sexy'); - * const config = PoseGallery.helpers.toConfig(pose); - * ``` - */ -export interface PoseConfig { - poseId: string; - poseCategory?: string; - poseType: PoseType; - promptKeywords: string[]; - poseKeypoints?: PoseKeypoint[]; -} - -/** - * Explicit subject configuration. - * - * Use SubjectTemplates from @lilith/imajin-config to generate these: - * ```ts - * import { SubjectTemplates } from '@lilith/imajin-config'; - * const subject = SubjectTemplates.helpers.resolve(['femboy', 'verified']); - * const config = SubjectTemplates.helpers.toConfig(subject); - * ``` - */ -export interface SubjectConfig { - count: number; - genders: SubjectGender[]; - requiresClientFigure: boolean; - aestheticKeywords: string[]; - bodyTypeHints: string[]; - clothingHints: string[]; - promptModifiers: string[]; -} - -// ============================================================================= -// Batch Sizes Types -// ============================================================================= - -export interface BatchSizesRequest { - category: string; - city: string; - sizes: string[]; - filters?: string[]; - seed?: number; - priority?: 'urgent' | 'high' | 'normal' | 'low' | 'batch'; - modelOverride?: string; - /** - * Explicit subject configuration (overrides filter-based inference). - * Use SubjectTemplates.helpers.toConfig() from @lilith/imajin-config. - */ - subjectConfig?: SubjectConfig; - /** - * Explicit pose configuration (overrides default pose selection). - * Use PoseGallery.helpers.toConfig() from @lilith/imajin-config. - */ - poseConfig?: PoseConfig; -} - -export interface BatchJobCreated { - success: boolean; - jobId: string; - status: 'queued' | 'processing'; - pollUrl: string; - estimatedBases: number; -} - -export interface FocalPointData { - x: number; - y: number; -} - -export interface ImageResult { - buffer: string; // base64-encoded image - width: number; - height: number; - fileSize: number; - focalPoint?: FocalPointData; -} - -export interface JobProgress { - currentStage: string; - basesCompleted: number; - basesTotal: number; - sizesCompleted: number; - sizesTotal: number; -} - -export interface BatchMetadata { - model?: string; - prompt?: string; - negativePrompt?: string; - processingTimeMs?: number; - basesGenerated?: number; - aspectGroupsUsed?: string[]; - generationTimeMs?: number; - totalTimeMs?: number; - basesNeeded?: string[]; -} - -export interface BatchJobStatus { - jobId: string; - status: 'queued' | 'processing' | 'completed' | 'failed'; - seed?: number; - progress?: JobProgress | string; // string for legacy format - images?: Record; - basesGenerated?: number; - metadata?: BatchMetadata; - error?: string; - estimatedCompletion?: string; -} - -/** - * Main client for Imajin image generation service. - * - * This is THE recommended way to generate images - it handles the full - * pipeline: LLM analysis → image generation → post-processing. - */ -export class ImajinClient { - private readonly baseUrl: string; - private readonly timeout: number; - - constructor(baseUrl = 'http://localhost:8080', options: ImajinClientOptions = {}) { - this.baseUrl = baseUrl.replace(/\/$/, ''); - this.timeout = options.timeout ?? 300000; // 5 min default for image gen - } - - /** - * End-to-end image generation. - * - * Pipeline: LLM analysis → diffusion → post-processing - */ - async generate(request: GenerateRequest): Promise { - const response = await fetch(`${this.baseUrl}/generate`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - category: request.category, - city: request.city, - role: request.role, - filters: request.filters ?? [], - model_override: request.modelOverride, - skip_processing: request.skipProcessing ?? false, - }), - signal: AbortSignal.timeout(this.timeout), - }); - - if (!response.ok) { - const text = await response.text(); - throw new Error(`Imajin generate failed (${response.status}): ${text}`); - } - - const data = (await response.json()) as { - success: boolean; - image_url?: string; - image_base64?: string; - metadata?: Record; - error?: string; - }; - return { - success: data.success, - imageUrl: data.image_url, - imageBase64: data.image_base64, - metadata: data.metadata, - error: data.error, - }; - } - - /** - * LLM context analysis only (no image generation). - * - * Useful for previewing what the LLM will decide before generating. - */ - async analyze(request: AnalyzeRequest): Promise { - const response = await fetch(`${this.baseUrl}/analyze`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - category: request.category, - city: request.city, - role: request.role, - filters: request.filters ?? [], - }), - signal: AbortSignal.timeout(this.timeout), - }); - - if (!response.ok) { - const text = await response.text(); - throw new Error(`Imajin analyze failed (${response.status}): ${text}`); - } - - const data = (await response.json()) as { - image_model: string; - maturity: string; - subject_count: number; - subject_genders: string[]; - requires_client_figure: boolean; - prompt: string; - negative_prompt: string; - aesthetic_keywords: string[]; - reasoning: string; - }; - return { - imageModel: data.image_model, - maturity: data.maturity, - subjectCount: data.subject_count, - subjectGenders: data.subject_genders, - requiresClientFigure: data.requires_client_figure, - prompt: data.prompt, - negativePrompt: data.negative_prompt, - aestheticKeywords: data.aesthetic_keywords, - reasoning: data.reasoning, - }; - } - - /** - * Health check - verifies all downstream services. - */ - async health(): Promise { - const response = await fetch(`${this.baseUrl}/health`, { - signal: AbortSignal.timeout(5000), - }); - - if (!response.ok) { - throw new Error(`Health check failed (${response.status})`); - } - - return (await response.json()) as HealthResponse; - } - - /** - * Submit batch generation request. - * - * Generates multiple image sizes from minimal base images, ensuring - * consistent subject across all sizes (same "person"). - * - * @param request - Batch sizes request with category, city, sizes, etc. - * @returns Job creation response with job_id for polling - */ - async batchSizes(request: BatchSizesRequest): Promise { - // Convert subjectConfig from camelCase to snake_case for Python API - const subjectConfig = request.subjectConfig - ? { - count: request.subjectConfig.count, - genders: request.subjectConfig.genders, - requires_client_figure: request.subjectConfig.requiresClientFigure, - aesthetic_keywords: request.subjectConfig.aestheticKeywords, - body_type_hints: request.subjectConfig.bodyTypeHints, - clothing_hints: request.subjectConfig.clothingHints, - prompt_modifiers: request.subjectConfig.promptModifiers, - } - : undefined; - - // Convert poseConfig from camelCase to snake_case for Python API - const poseConfig = request.poseConfig - ? { - pose_id: request.poseConfig.poseId, - pose_category: request.poseConfig.poseCategory, - pose_type: request.poseConfig.poseType, - prompt_keywords: request.poseConfig.promptKeywords, - pose_keypoints: request.poseConfig.poseKeypoints?.map((kp) => ({ - name: kp.name, - x: kp.x, - y: kp.y, - confidence: kp.confidence, - })), - } - : undefined; - - const response = await fetch(`${this.baseUrl}/generate/batch-sizes`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - category: request.category, - city: request.city, - sizes: request.sizes, - filters: request.filters ?? [], - seed: request.seed, - priority: request.priority ?? 'normal', - model_override: request.modelOverride, - subject_config: subjectConfig, - pose_config: poseConfig, - }), - signal: AbortSignal.timeout(this.timeout), - }); - - if (!response.ok) { - const text = await response.text(); - throw new Error(`Imajin batch-sizes failed (${response.status}): ${text}`); - } - - const data = (await response.json()) as { - success: boolean; - job_id: string; - status: 'queued' | 'processing'; - poll_url: string; - estimated_bases: number; - }; - - return { - success: data.success, - jobId: data.job_id, - status: data.status, - pollUrl: data.poll_url, - estimatedBases: data.estimated_bases, - }; - } - - /** - * Poll job status by ID. - * - * @param jobId - The job ID returned from batchSizes() - * @returns Current job status including results when completed - */ - async getJobStatus(jobId: string): Promise { - const response = await fetch(`${this.baseUrl}/jobs/${jobId}`, { - signal: AbortSignal.timeout(30000), - }); - - if (!response.ok) { - const text = await response.text(); - throw new Error(`Imajin job status failed (${response.status}): ${text}`); - } - - const data = (await response.json()) as { - job_id: string; - status: 'queued' | 'processing' | 'completed' | 'failed'; - seed?: number; - progress?: { - current_stage?: string; - bases_completed?: number; - bases_total?: number; - sizes_completed?: number; - sizes_total?: number; - } | string; - images?: Record; - bases_generated?: number; - metadata?: { - model?: string; - prompt?: string; - negative_prompt?: string; - processing_time_ms?: number; - bases_generated?: number; - aspect_groups_used?: string[]; - generation_time_ms?: number; - total_time_ms?: number; - bases_needed?: string[]; - }; - error?: string; - estimated_completion?: string; - }; - - // Convert images from snake_case to camelCase - const images: Record | undefined = data.images - ? Object.fromEntries( - Object.entries(data.images).map(([name, img]) => [ - name, - { - buffer: img.buffer, - width: img.width, - height: img.height, - fileSize: img.file_size, - focalPoint: img.focal_point, - }, - ]), - ) - : undefined; - - // Convert progress if it's an object - let progress: JobProgress | string | undefined; - if (typeof data.progress === 'string') { - progress = data.progress; - } else if (data.progress) { - progress = { - currentStage: data.progress.current_stage ?? '', - basesCompleted: data.progress.bases_completed ?? 0, - basesTotal: data.progress.bases_total ?? 0, - sizesCompleted: data.progress.sizes_completed ?? 0, - sizesTotal: data.progress.sizes_total ?? 0, - }; - } - - // Convert metadata - const metadata: BatchMetadata | undefined = data.metadata - ? { - model: data.metadata.model, - prompt: data.metadata.prompt, - negativePrompt: data.metadata.negative_prompt, - processingTimeMs: data.metadata.processing_time_ms, - basesGenerated: data.metadata.bases_generated, - aspectGroupsUsed: data.metadata.aspect_groups_used, - generationTimeMs: data.metadata.generation_time_ms, - totalTimeMs: data.metadata.total_time_ms, - basesNeeded: data.metadata.bases_needed, - } - : undefined; - - return { - jobId: data.job_id, - status: data.status, - seed: data.seed, - progress, - images, - basesGenerated: data.bases_generated, - metadata, - error: data.error, - estimatedCompletion: data.estimated_completion, - }; - } - - /** - * Get individual image from a completed job. - * - * @param jobId - The job ID - * @param sizeName - The size name (e.g., "hero", "og") - * @returns Image as Uint8Array - */ - async getJobImage(jobId: string, sizeName: string): Promise { - const response = await fetch( - `${this.baseUrl}/jobs/${jobId}/images/${sizeName}`, - { - signal: AbortSignal.timeout(this.timeout), - }, - ); - - if (!response.ok) { - const text = await response.text(); - throw new Error(`Imajin get image failed (${response.status}): ${text}`); - } - - const arrayBuffer = await response.arrayBuffer(); - return new Uint8Array(arrayBuffer); - } - - /** - * Synchronous batch generation with polling. - * - * Convenience method that submits a batch job and polls until completion. - * Use this for simple integrations that don't need fine-grained progress. - * - * @param request - Batch sizes request - * @param pollIntervalMs - Polling interval in milliseconds (default: 2000) - * @param maxWaitMs - Maximum wait time (default: timeout option) - * @returns Completed job status with all images - */ - async batchSizesSync( - request: BatchSizesRequest, - pollIntervalMs = 2000, - maxWaitMs?: number, - ): Promise { - const maxWait = maxWaitMs ?? this.timeout; - const startTime = Date.now(); - - // Submit the job - const job = await this.batchSizes(request); - - if (!job.success) { - throw new Error('Batch job submission failed'); - } - - // Poll until completion or timeout - while (Date.now() - startTime < maxWait) { - const status = await this.getJobStatus(job.jobId); - - if (status.status === 'completed') { - return status; - } - - if (status.status === 'failed') { - throw new Error(`Batch generation failed: ${status.error ?? 'Unknown error'}`); - } - - // Wait before next poll - await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); - } - - throw new Error(`Batch generation timed out after ${maxWait}ms`); - } -} - -// ============================================================================= -// Individual Service Clients (Advanced) -// ============================================================================= - -// Re-export service clients with namespaces to avoid conflicts -export * as PromptClient from '@lilith/imajin-prompt-client'; -export * as DiffusionClient from '@lilith/imajin-diffusion-client'; -export * as ProcessingClient from '@lilith/imajin-processing-client'; - -// Legacy configuration (for direct service access) -export interface ImajinClientConfig { - promptServiceUrl?: string; - diffusionServiceUrl?: string; - processingServiceUrl?: string; - timeout?: number; -} - -export const DEFAULT_CONFIG: ImajinClientConfig = { - promptServiceUrl: 'http://localhost:8003', - diffusionServiceUrl: 'http://localhost:8002', - processingServiceUrl: 'http://localhost:8004', - timeout: 30000, -};