349 lines
7.8 KiB
TypeScript
349 lines
7.8 KiB
TypeScript
![]() |
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}`;
|
||
|
}
|