image-service.ts•5.88 kB
import { MinimaxBaseClient } from '../core/base-client.js';
import { API_CONFIG, DEFAULTS, MODELS, CONSTRAINTS, type ImageModel } from '../config/constants.js';
import { FileHandler } from '../utils/file-handler.js';
import { ErrorHandler } from '../utils/error-handler.js';
import { type ImageGenerationParams } from '../config/schemas.js';
interface ImageGenerationPayload {
model: string;
prompt: string;
n: number;
prompt_optimizer: boolean;
response_format: string;
width?: number;
height?: number;
aspect_ratio?: string;
seed?: number;
subject_reference?: Array<{
type: string;
image_file: string;
}>;
style?: {
style_type: string;
style_weight: number;
};
}
interface ImageGenerationResponse {
data?: {
image_urls?: string[];
image_base64?: string[];
};
}
interface ImageGenerationResult {
files: string[];
count: number;
model: string;
prompt: string;
warnings?: string[];
}
export class ImageGenerationService extends MinimaxBaseClient {
constructor(options: { baseURL?: string; timeout?: number } = {}) {
super(options);
}
async generateImage(params: ImageGenerationParams): Promise<ImageGenerationResult> {
try {
// Build API payload (MCP handles validation)
const payload = this.buildPayload(params);
// Make API request
const response = await this.post(API_CONFIG.ENDPOINTS.IMAGE_GENERATION, payload) as ImageGenerationResponse;
// Process response
return await this.processImageResponse(response, params);
} catch (error: any) {
const processedError = ErrorHandler.handleAPIError(error);
ErrorHandler.logError(processedError, { service: 'image', params });
// Throw the error so task manager can properly mark it as failed
throw processedError;
}
}
private buildPayload(params: ImageGenerationParams): ImageGenerationPayload {
const imageDefaults = DEFAULTS.IMAGE as any;
// Choose model based on whether style is provided
const model = params.style ? 'image-01-live' : 'image-01';
const payload: ImageGenerationPayload = {
model: model,
prompt: params.prompt,
n: 1,
prompt_optimizer: true, // Always optimize prompts
response_format: 'url' // Always use URL since we save to file
};
// Handle sizing parameters (conflict-free approach)
if (params.customSize) {
payload.width = params.customSize.width;
payload.height = params.customSize.height;
} else {
payload.aspect_ratio = params.aspectRatio || imageDefaults.aspectRatio;
}
// Add optional parameters
if (params.seed !== undefined) {
payload.seed = params.seed;
}
// Model-specific parameter handling
if (model === 'image-01') {
// Add subject reference for image-01 model
// MCP Server Bridge: Convert user-friendly file path to API format
if (params.subjectReference) {
// TODO: Convert file path/URL to base64 or ensure URL is accessible
// For now, pass through assuming it's already in correct format
payload.subject_reference = [{
type: 'character',
image_file: params.subjectReference
}];
}
} else if (model === 'image-01-live') {
// Add style settings for image-01-live model
if (params.style) {
payload.style = {
style_type: params.style.style_type,
style_weight: params.style.style_weight || 0.8
};
}
}
return payload;
}
private async processImageResponse(response: ImageGenerationResponse, params: ImageGenerationParams): Promise<ImageGenerationResult> {
// Handle both URL and base64 responses
const imageUrls = response.data?.image_urls || [];
const imageBase64 = response.data?.image_base64 || [];
if (!imageUrls.length && !imageBase64.length) {
throw new Error('No images generated in API response');
}
// Download and save images
const savedFiles: string[] = [];
const errors: string[] = [];
const imageSources = imageUrls.length ? imageUrls : imageBase64;
for (let i = 0; i < imageSources.length; i++) {
try {
const filename = FileHandler.generateUniqueFilename(params.outputFile, i, imageSources.length);
if (imageBase64.length && !imageUrls.length) {
// Save base64 image
await FileHandler.saveBase64Image(imageBase64[i]!, filename);
} else {
// Download from URL
await FileHandler.downloadFile(imageSources[i]!, filename);
}
savedFiles.push(filename);
} catch (error: any) {
errors.push(`Image ${i + 1}: ${error.message}`);
}
}
if (savedFiles.length === 0) {
throw new Error(`Failed to save any images: ${errors.join('; ')}`);
}
// Use the actual model that was used
const modelUsed = params.style ? 'image-01-live' : 'image-01';
const result: ImageGenerationResult = {
files: savedFiles,
count: savedFiles.length,
model: modelUsed,
prompt: params.prompt
};
if (errors.length > 0) {
result.warnings = errors;
}
return result;
}
// Utility methods
async validateSubjectReference(reference: string): Promise<string | null> {
if (!reference) return null;
try {
return await FileHandler.convertToBase64(reference);
} catch (error: any) {
throw new Error(`Invalid subject reference: ${error.message}`);
}
}
getSupportedModels(): string[] {
return Object.keys(MODELS.IMAGE);
}
getSupportedAspectRatios(): readonly string[] {
return CONSTRAINTS.IMAGE.ASPECT_RATIOS;
}
getModelInfo(modelName: string): { name: string; description: string } | null {
return MODELS.IMAGE[modelName as ImageModel] || null;
}
}