harden media

This commit is contained in:
2025-12-31 15:39:28 +01:00
parent 92226800df
commit 8260bf7baf
413 changed files with 8361 additions and 1544 deletions

View File

@@ -1,7 +1,9 @@
import type { ParticipantRef } from '@core/racing/domain/types/ParticipantRef';
import type { ChampionshipType } from '@core/racing/domain/types/ChampionshipType';
import { MediaReference } from '../../../core/domain/media/MediaReference';
export const makeDriverRef = (id: string): ParticipantRef => ({
type: 'driver' as ChampionshipType,
id,
avatarRef: MediaReference.systemDefault('avatar'),
});

View File

@@ -1,124 +0,0 @@
import type {
AvatarGenerationPort,
AvatarGenerationOptions,
AvatarGenerationResult,
} from '@core/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.getPlaceholderAvatars(options.suitColor) ?? [];
// 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 getPlaceholderAvatars(color: string): string[] | undefined {
const avatars = this.placeholderAvatars[color];
if (!avatars || avatars.length === 0) {
return this.placeholderAvatars.blue;
}
return avatars;
}
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

@@ -1,62 +0,0 @@
import type { FaceValidationPort, FaceValidationResult } from '@core/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

@@ -1,41 +0,0 @@
import type { ImageServicePort } from '@core/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 numericSuffixString = numericSuffixMatch[1] ?? '';
const numericSuffix = Number.parseInt(numericSuffixString, 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

@@ -21,12 +21,14 @@ export type Friendship = {
friendId: string;
};
import { MediaReference } from '@core/domain/media/MediaReference';
export interface DemoTeamDTO {
id: string;
name: string;
tag: string;
description: string;
logoUrl: string;
logoRef: MediaReference;
primaryLeagueId: string;
memberCount: number;
}
@@ -172,7 +174,7 @@ export function createLeagues(ownerIds: string[]): League[] {
return leagues;
}
export function createTeams(leagues: League[], getTeamLogo: (id: string) => string): DemoTeamDTO[] {
export function createTeams(leagues: League[]): DemoTeamDTO[] {
const teams: DemoTeamDTO[] = [];
const teamCount = 24 + faker.number.int({ min: 0, max: 12 });
@@ -188,7 +190,7 @@ export function createTeams(leagues: League[], getTeamLogo: (id: string) => stri
name,
tag,
description: faker.lorem.sentence(),
logoUrl: getTeamLogo(id),
logoRef: MediaReference.systemDefault('logo'),
primaryLeagueId: primaryLeague.id,
memberCount,
});

View File

@@ -8,7 +8,6 @@ import type { FeedItem } from '@core/social/domain/types/FeedItem';
import type { SocialFriendSummary } from '@core/social/application/types/SocialUser';
import { faker } from '../../helpers/faker/faker';
import { getTeamLogo } from '../../helpers/images/images';
import {
createDrivers,
@@ -70,7 +69,7 @@ export function createStaticRacingSeed(seed: number): RacingSeedData {
const drivers = createDrivers(96);
const leagues = createLeagues(drivers.slice(0, 12).map((d) => d.id));
const teams = createTeams(leagues, getTeamLogo);
const teams = createTeams(leagues);
const memberships = createMemberships(drivers, leagues, teams);
const races = createRaces(leagues);
const results = createResults(drivers, races);

View File

@@ -1,68 +0,0 @@
import { faker } from '../faker/faker';
const DRIVER_AVATARS = [
'/images/avatars/avatar-1.svg',
'/images/avatars/avatar-2.svg',
'/images/avatars/avatar-3.svg',
'/images/avatars/avatar-4.svg',
'/images/avatars/avatar-5.svg',
'/images/avatars/avatar-6.svg',
] as const;
const TEAM_LOGOS = [
'/images/logos/team-1.svg',
'/images/logos/team-2.svg',
'/images/logos/team-3.svg',
'/images/logos/team-4.svg',
] as const;
const LEAGUE_BANNERS = [
'/images/header.jpeg',
'/images/ff1600.jpeg',
'/images/lmp3.jpeg',
'/images/porsche.jpeg',
] as const;
function hashString(input: string): number {
let hash = 0;
for (let i = 0; i < input.length; i += 1) {
hash = (hash * 31 + input.charCodeAt(i)) | 0;
}
return Math.abs(hash);
}
export function getDriverAvatar(driverId: string): string {
const index = hashString(driverId) % DRIVER_AVATARS.length;
const avatar = DRIVER_AVATARS[index] ?? DRIVER_AVATARS[0];
return avatar;
}
export function getTeamLogo(teamId: string): string {
const index = hashString(teamId) % TEAM_LOGOS.length;
const logo = TEAM_LOGOS[index] ?? TEAM_LOGOS[0];
return logo;
}
export function getLeagueBanner(leagueId: string): string {
const index = hashString(leagueId) % LEAGUE_BANNERS.length;
const banner = LEAGUE_BANNERS[index] ?? LEAGUE_BANNERS[0];
return banner;
}
export interface LeagueCoverImage {
url: string;
alt: string;
}
export function getLeagueCoverImage(leagueId: string): LeagueCoverImage {
const seed = hashString(leagueId);
faker.seed(seed);
const alt = faker.lorem.words(3);
const url = `https://picsum.photos/seed/${seed}/1200/280?blur=2`;
return { url, alt };
}
export { DRIVER_AVATARS, TEAM_LOGOS, LEAGUE_BANNERS };