feat(imajin-client): ✨ Update and expand exports in the client entry point to include new utility functions and refined configuration handling
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
5697802e49
commit
9afc92a5b5
1 changed files with 0 additions and 603 deletions
|
|
@ -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<string, unknown>;
|
||||
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<string, boolean>;
|
||||
}
|
||||
|
||||
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<string, ImageResult>;
|
||||
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<GenerateResponse> {
|
||||
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<string, unknown>;
|
||||
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<AnalyzeResponse> {
|
||||
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<HealthResponse> {
|
||||
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<BatchJobCreated> {
|
||||
// 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<BatchJobStatus> {
|
||||
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<string, {
|
||||
buffer: string;
|
||||
width: number;
|
||||
height: number;
|
||||
file_size: number;
|
||||
focal_point?: { x: number; y: number };
|
||||
}>;
|
||||
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<string, ImageResult> | 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<Uint8Array> {
|
||||
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<BatchJobStatus> {
|
||||
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,
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue