import { IsString, IsEnum, IsInt, IsOptional, IsUUID, IsObject, MinLength, MaxLength, Min, IsDate } from 'class-validator'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ImageStatus } from '@prisma/client'; import { Type } from 'class-transformer'; export interface VisionTagsInterface { objects?: string[]; colors?: string[]; scene?: string; description?: string; confidence?: number; aiModel?: string; processingTime?: number; } export interface ImageDimensionsInterface { width: number; height: number; aspectRatio?: string; } export class CreateImageDto { @ApiProperty({ description: 'ID of the batch this image belongs to', example: '550e8400-e29b-41d4-a716-446655440000' }) @IsUUID() batchId: string; @ApiProperty({ description: 'Original filename of the image', example: 'IMG_20240101_123456.jpg' }) @IsString() @MinLength(1) @MaxLength(255) originalName: string; @ApiPropertyOptional({ description: 'File size in bytes', example: 2048576 }) @IsOptional() @IsInt() @Min(0) fileSize?: number; @ApiPropertyOptional({ description: 'MIME type of the image', example: 'image/jpeg' }) @IsOptional() @IsString() mimeType?: string; @ApiPropertyOptional({ description: 'Image dimensions', example: { width: 1920, height: 1080, aspectRatio: '16:9' } }) @IsOptional() @IsObject() dimensions?: ImageDimensionsInterface; @ApiPropertyOptional({ description: 'S3 object key for storage', example: 'uploads/user123/batch456/original/image.jpg' }) @IsOptional() @IsString() s3Key?: string; } export class UpdateImageDto { @ApiPropertyOptional({ description: 'AI-generated proposed filename', example: 'modern-kitchen-with-stainless-steel-appliances.jpg' }) @IsOptional() @IsString() @MaxLength(255) proposedName?: string; @ApiPropertyOptional({ description: 'User-approved final filename', example: 'kitchen-renovation-final.jpg' }) @IsOptional() @IsString() @MaxLength(255) finalName?: string; @ApiPropertyOptional({ description: 'AI vision analysis results', example: { objects: ['kitchen', 'refrigerator', 'countertop'], colors: ['white', 'stainless steel', 'black'], scene: 'modern kitchen interior', description: 'A modern kitchen with stainless steel appliances', confidence: 0.95, aiModel: 'gpt-4-vision', processingTime: 2.5 } }) @IsOptional() @IsObject() visionTags?: VisionTagsInterface; @ApiPropertyOptional({ description: 'Image processing status', enum: ImageStatus }) @IsOptional() @IsEnum(ImageStatus) status?: ImageStatus; @ApiPropertyOptional({ description: 'Error message if processing failed', example: 'Image format not supported' }) @IsOptional() @IsString() @MaxLength(500) processingError?: string; } export class ImageResponseDto { @ApiProperty({ description: 'Unique image identifier', example: '550e8400-e29b-41d4-a716-446655440000' }) @IsUUID() id: string; @ApiProperty({ description: 'ID of the batch this image belongs to', example: '550e8400-e29b-41d4-a716-446655440000' }) @IsUUID() batchId: string; @ApiProperty({ description: 'Original filename of the image', example: 'IMG_20240101_123456.jpg' }) @IsString() originalName: string; @ApiPropertyOptional({ description: 'AI-generated proposed filename', example: 'modern-kitchen-with-stainless-steel-appliances.jpg' }) @IsOptional() @IsString() proposedName?: string; @ApiPropertyOptional({ description: 'User-approved final filename', example: 'kitchen-renovation-final.jpg' }) @IsOptional() @IsString() finalName?: string; @ApiPropertyOptional({ description: 'AI vision analysis results' }) @IsOptional() @IsObject() visionTags?: VisionTagsInterface; @ApiProperty({ description: 'Current image processing status', enum: ImageStatus }) @IsEnum(ImageStatus) status: ImageStatus; @ApiPropertyOptional({ description: 'File size in bytes', example: 2048576 }) @IsOptional() @IsInt() fileSize?: number; @ApiPropertyOptional({ description: 'Image dimensions' }) @IsOptional() @IsObject() dimensions?: ImageDimensionsInterface; @ApiPropertyOptional({ description: 'MIME type of the image', example: 'image/jpeg' }) @IsOptional() @IsString() mimeType?: string; @ApiPropertyOptional({ description: 'S3 object key for storage', example: 'uploads/user123/batch456/original/image.jpg' }) @IsOptional() @IsString() s3Key?: string; @ApiPropertyOptional({ description: 'Error message if processing failed' }) @IsOptional() @IsString() processingError?: string; @ApiProperty({ description: 'Image creation timestamp' }) @IsDate() createdAt: Date; @ApiProperty({ description: 'Image last update timestamp' }) @IsDate() updatedAt: Date; @ApiPropertyOptional({ description: 'Image processing completion timestamp' }) @IsOptional() @IsDate() processedAt?: Date; } export class ImageProcessingResultDto { @ApiProperty({ description: 'Image details' }) image: ImageResponseDto; @ApiProperty({ description: 'Processing success status' }) success: boolean; @ApiPropertyOptional({ description: 'Processing time in seconds' }) @IsOptional() @Type(() => Number) processingTime?: number; @ApiPropertyOptional({ description: 'Error details if processing failed' }) @IsOptional() @IsString() error?: string; } export class BulkImageUpdateDto { @ApiProperty({ description: 'Array of image IDs to update', example: ['550e8400-e29b-41d4-a716-446655440000', '660f9511-f39c-52e5-b827-557766551111'] }) @IsUUID(undefined, { each: true }) imageIds: string[]; @ApiPropertyOptional({ description: 'Status to set for all images', enum: ImageStatus }) @IsOptional() @IsEnum(ImageStatus) status?: ImageStatus; @ApiPropertyOptional({ description: 'Apply proposed names as final names for all images' }) @IsOptional() applyProposedNames?: boolean; } // Helper function to generate SEO-friendly filename export function generateSeoFriendlyFilename( visionTags: VisionTagsInterface, originalName: string ): string { if (!visionTags.objects && !visionTags.description) { return originalName; } let filename = ''; // Use description if available, otherwise use objects if (visionTags.description) { filename = visionTags.description .toLowerCase() .replace(/[^a-z0-9\s-]/g, '') // Remove special characters .replace(/\s+/g, '-') // Replace spaces with hyphens .replace(/-+/g, '-') // Replace multiple hyphens with single .substring(0, 100); // Limit length } else if (visionTags.objects && visionTags.objects.length > 0) { filename = visionTags.objects .slice(0, 3) // Take first 3 objects .join('-') .toLowerCase() .replace(/[^a-z0-9-]/g, '') .substring(0, 100); } // Get file extension from original name const extension = originalName.split('.').pop()?.toLowerCase() || 'jpg'; return filename ? `${filename}.${extension}` : originalName; } // Helper function to validate image file type export function isValidImageType(mimeType: string): boolean { const validTypes = [ 'image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif', 'image/bmp', 'image/tiff' ]; return validTypes.includes(mimeType.toLowerCase()); } // Helper function to calculate aspect ratio export function calculateAspectRatio(width: number, height: number): string { const gcd = (a: number, b: number): number => b === 0 ? a : gcd(b, a % b); const divisor = gcd(width, height); return `${width / divisor}:${height / divisor}`; }