harden media
This commit is contained in:
@@ -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'),
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
Reference in New Issue
Block a user