This commit is contained in:
2025-12-11 11:25:22 +01:00
parent 6a427eab57
commit e4c1be628d
86 changed files with 1222 additions and 736 deletions

View File

@@ -1,5 +1,9 @@
export * from './src/faker/faker';
export * from './src/images/images';
export * from './src/media/DemoAvatarGenerationAdapter';
export * from './src/media/DemoFaceValidationAdapter';
export * from './src/media/DemoImageServiceAdapter';
export * from './src/media/InMemoryAvatarGenerationRepository';
export * from './src/racing/RacingSeedCore';
export * from './src/racing/RacingSponsorshipSeed';
export * from './src/racing/RacingFeedSeed';

View File

@@ -0,0 +1,116 @@
import type {
AvatarGenerationPort,
AvatarGenerationOptions,
AvatarGenerationResult,
} from '@gridpilot/media';
/**
* Demo implementation of AvatarGenerationPort.
*
* In production, this would use a real AI image generation API like:
* - OpenAI DALL-E
* - Midjourney API
* - Stable Diffusion
* - RunwayML
*
* For demo purposes, this returns placeholder avatar images.
*/
export class DemoAvatarGenerationAdapter implements AvatarGenerationPort {
private readonly placeholderAvatars: Record<string, string[]> = {
red: [
'/images/avatars/generated/red-1.png',
'/images/avatars/generated/red-2.png',
'/images/avatars/generated/red-3.png',
],
blue: [
'/images/avatars/generated/blue-1.png',
'/images/avatars/generated/blue-2.png',
'/images/avatars/generated/blue-3.png',
],
green: [
'/images/avatars/generated/green-1.png',
'/images/avatars/generated/green-2.png',
'/images/avatars/generated/green-3.png',
],
yellow: [
'/images/avatars/generated/yellow-1.png',
'/images/avatars/generated/yellow-2.png',
'/images/avatars/generated/yellow-3.png',
],
orange: [
'/images/avatars/generated/orange-1.png',
'/images/avatars/generated/orange-2.png',
'/images/avatars/generated/orange-3.png',
],
purple: [
'/images/avatars/generated/purple-1.png',
'/images/avatars/generated/purple-2.png',
'/images/avatars/generated/purple-3.png',
],
black: [
'/images/avatars/generated/black-1.png',
'/images/avatars/generated/black-2.png',
'/images/avatars/generated/black-3.png',
],
white: [
'/images/avatars/generated/white-1.png',
'/images/avatars/generated/white-2.png',
'/images/avatars/generated/white-3.png',
],
pink: [
'/images/avatars/generated/pink-1.png',
'/images/avatars/generated/pink-2.png',
'/images/avatars/generated/pink-3.png',
],
cyan: [
'/images/avatars/generated/cyan-1.png',
'/images/avatars/generated/cyan-2.png',
'/images/avatars/generated/cyan-3.png',
],
};
async generateAvatars(options: AvatarGenerationOptions): Promise<AvatarGenerationResult> {
// Simulate AI processing time (1-3 seconds)
await this.delay(1500 + Math.random() * 1500);
// Log what would be sent to the AI (for debugging)
console.log('[DemoAvatarGeneration] Would generate with prompt:', options.prompt);
console.log('[DemoAvatarGeneration] Suit color:', options.suitColor);
console.log('[DemoAvatarGeneration] Style:', options.style);
console.log('[DemoAvatarGeneration] Count:', options.count);
// For demo, return placeholder URLs based on suit color
// In production, these would be actual AI-generated images
const colorAvatars = this.placeholderAvatars[options.suitColor] ?? this.placeholderAvatars.blue;
// Generate unique URLs with a hash to simulate different generations
const hash = this.generateHash((options.facePhotoUrl ?? '') + Date.now());
const avatars = colorAvatars.slice(0, options.count).map((baseUrl, index) => {
// In demo mode, use dicebear or similar for generating varied avatars
const seed = `${hash}-${options.suitColor}-${index}`;
return {
url: `https://api.dicebear.com/7.x/personas/svg?seed=${seed}&backgroundColor=transparent`,
thumbnailUrl: `https://api.dicebear.com/7.x/personas/svg?seed=${seed}&backgroundColor=transparent&size=64`,
};
});
return {
success: true,
avatars,
};
}
private delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
private generateHash(input: string): string {
let hash = 0;
for (let i = 0; i < input.length; i += 1) {
const char = input.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash |= 0;
}
return Math.abs(hash).toString(36);
}
}

View File

@@ -0,0 +1,62 @@
import type { FaceValidationPort, FaceValidationResult } from '@gridpilot/media';
/**
* Demo implementation of FaceValidationPort.
*
* In production, this would use a real face detection API like:
* - AWS Rekognition
* - Google Cloud Vision
* - Azure Face API
* - OpenCV / face-api.js
*
* For demo purposes, this always returns a valid face if the image data is provided.
*/
export class DemoFaceValidationAdapter implements FaceValidationPort {
async validateFacePhoto(imageData: string | Buffer): Promise<FaceValidationResult> {
// Simulate some processing time
await this.delay(500);
// Check if we have any image data
const dataString = typeof imageData === 'string' ? imageData : imageData.toString();
if (!dataString || dataString.length < 100) {
return {
isValid: false,
hasFace: false,
faceCount: 0,
confidence: 0,
errorMessage: 'Invalid or empty image data',
};
}
// Check for valid base64 image data or data URL
const isValidImage =
dataString.startsWith('data:image/') ||
dataString.startsWith('/9j/') || // JPEG magic bytes in base64
dataString.startsWith('iVBOR') || // PNG magic bytes in base64
dataString.length > 1000; // Assume long strings are valid image data
if (!isValidImage) {
return {
isValid: false,
hasFace: false,
faceCount: 0,
confidence: 0,
errorMessage: 'Please upload a valid image file (JPEG or PNG)',
};
}
// For demo: always return success with high confidence
// In production, this would actually analyze the image
return {
isValid: true,
hasFace: true,
faceCount: 1,
confidence: 0.95,
};
}
private delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}

View File

@@ -0,0 +1,40 @@
import type { ImageServicePort } from '@gridpilot/media';
const MALE_DEFAULT_AVATAR = '/images/avatars/male-default-avatar.jpg';
const FEMALE_DEFAULT_AVATAR = '/images/avatars/female-default-avatar.jpeg';
export class DemoImageServiceAdapter implements ImageServicePort {
getDriverAvatar(driverId: string): string {
const numericSuffixMatch = driverId.match(/(\d+)$/);
if (numericSuffixMatch) {
const numericSuffix = Number.parseInt(numericSuffixMatch[1], 10);
return numericSuffix % 2 === 0 ? FEMALE_DEFAULT_AVATAR : MALE_DEFAULT_AVATAR;
}
const seed = stableHash(driverId);
return seed % 2 === 0 ? FEMALE_DEFAULT_AVATAR : MALE_DEFAULT_AVATAR;
}
getTeamLogo(teamId: string): string {
const seed = stableHash(teamId);
return `https://picsum.photos/seed/team-${seed}/256/256`;
}
getLeagueCover(leagueId: string): string {
const seed = stableHash(leagueId);
return `https://picsum.photos/seed/league-cover-${seed}/1200/280?blur=2`;
}
getLeagueLogo(leagueId: string): string {
const seed = stableHash(leagueId);
return `https://picsum.photos/seed/league-logo-${seed}/160/160`;
}
}
function stableHash(value: string): number {
let hash = 0;
for (let i = 0; i < value.length; i += 1) {
hash = (hash * 31 + value.charCodeAt(i)) | 0;
}
return Math.abs(hash);
}

View File

@@ -0,0 +1,47 @@
import type {
IAvatarGenerationRepository,
} from '@gridpilot/media';
import {
AvatarGenerationRequest,
type AvatarGenerationRequestProps,
} from '@gridpilot/media';
/**
* In-memory implementation of IAvatarGenerationRepository.
*
* For demo/development purposes. In production, this would use a database.
*/
export class InMemoryAvatarGenerationRepository implements IAvatarGenerationRepository {
private readonly requests = new Map<string, AvatarGenerationRequestProps>();
async save(request: AvatarGenerationRequest): Promise<void> {
this.requests.set(request.id, request.toProps());
}
async findById(id: string): Promise<AvatarGenerationRequest | null> {
const props = this.requests.get(id);
if (!props) {
return null;
}
return AvatarGenerationRequest.reconstitute(props);
}
async findByUserId(userId: string): Promise<AvatarGenerationRequest[]> {
const results: AvatarGenerationRequest[] = [];
for (const props of this.requests.values()) {
if (props.userId === userId) {
results.push(AvatarGenerationRequest.reconstitute(props));
}
}
return results.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
}
async findLatestByUserId(userId: string): Promise<AvatarGenerationRequest | null> {
const userRequests = await this.findByUserId(userId);
return userRequests.length > 0 ? userRequests[0] : null;
}
async delete(id: string): Promise<void> {
this.requests.delete(id);
}
}