From 9d189b1331ee4c03b0b741096a862d11b0da5431 Mon Sep 17 00:00:00 2001 From: Chr1st1anG <73988455+Chr1st1anG@users.noreply.github.com> Date: Wed, 11 Feb 2026 23:22:51 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(figgos):=20AI=20generation=20p?= =?UTF-8?q?ipeline=20+=20frontend=20API=20integration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: - Add Gemini-powered profile generation (text + image) - Add image processing with background removal (sharp) - Add S3 storage service for figure images - Extend figures schema with generatedProfile, language, status columns - Wire up synchronous generation pipeline on POST /api/v1/figures Frontend (Mobile + Web): - Replace all mock data with real API calls - Show generatedProfile data (subtitle, backstory, stats, items, specialAttack) - Display generated images from S3 or name placeholders - Create web API service ($lib/api.ts) - Delete mock cards data files Infrastructure: - Add CORS origin for web dev port (5196) - Add GEMINI_API_KEY + FIGGOS_STORAGE_PUBLIC_URL to env generation - Add figgos-storage bucket to shared-storage factory - Add .gitignore for workbench/ Co-Authored-By: Claude Opus 4.6 --- apps/figgos/.gitignore | 1 + apps/figgos/apps/backend/package.json | 6 +- .../backend/src/db/schema/figures.schema.ts | 12 +- .../src/figures/dto/create-figure.dto.ts | 8 +- .../backend/src/figures/figures.controller.ts | 7 +- .../backend/src/figures/figures.module.ts | 2 + .../backend/src/figures/figures.service.ts | 37 +- .../backend/src/generation/gemini.service.ts | 138 ++++ .../src/generation/generation.module.ts | 12 + .../src/generation/generation.service.ts | 120 ++++ .../generation/image-processing.service.ts | 92 +++ .../apps/backend/src/generation/prompts.ts | 228 +++++++ apps/figgos/apps/backend/src/main.ts | 2 +- .../backend/src/storage/storage.module.ts | 8 + .../backend/src/storage/storage.service.ts | 30 + .../apps/mobile/app/(tabs)/carousel.tsx | 85 ++- .../apps/mobile/app/(tabs)/collection.tsx | 103 ++- apps/figgos/apps/mobile/app/(tabs)/index.tsx | 186 +++-- apps/figgos/apps/mobile/app/(tabs)/shelf.tsx | 85 ++- apps/figgos/apps/mobile/app/(tabs)/stack.tsx | 187 +++-- apps/figgos/apps/mobile/app/card/[id].tsx | 177 ++--- apps/figgos/apps/mobile/app/card/v2/[id].tsx | 189 +++-- apps/figgos/apps/mobile/data/cards.ts | 65 -- apps/figgos/apps/mobile/services/api.ts | 4 +- apps/figgos/apps/web/src/lib/api.ts | 29 + apps/figgos/apps/web/src/lib/data/cards.ts | 64 -- apps/figgos/apps/web/src/routes/+page.svelte | 144 +++- .../web/src/routes/card/[id]/+page.svelte | 188 +++-- .../web/src/routes/collection/+page.svelte | 76 ++- .../apps/web/src/routes/showcase/+page.svelte | 114 +++- apps/figgos/package.json | 4 + apps/figgos/packages/shared/src/index.ts | 81 +++ packages/shared-storage/src/factory.ts | 10 + packages/shared-storage/src/index.ts | 1 + packages/shared-storage/src/types.ts | 1 + pnpm-lock.yaml | 643 +++++++++++++++++- scripts/generate-env.mjs | 4 +- 37 files changed, 2521 insertions(+), 622 deletions(-) create mode 100644 apps/figgos/.gitignore create mode 100644 apps/figgos/apps/backend/src/generation/gemini.service.ts create mode 100644 apps/figgos/apps/backend/src/generation/generation.module.ts create mode 100644 apps/figgos/apps/backend/src/generation/generation.service.ts create mode 100644 apps/figgos/apps/backend/src/generation/image-processing.service.ts create mode 100644 apps/figgos/apps/backend/src/generation/prompts.ts create mode 100644 apps/figgos/apps/backend/src/storage/storage.module.ts create mode 100644 apps/figgos/apps/backend/src/storage/storage.service.ts delete mode 100644 apps/figgos/apps/mobile/data/cards.ts create mode 100644 apps/figgos/apps/web/src/lib/api.ts delete mode 100644 apps/figgos/apps/web/src/lib/data/cards.ts diff --git a/apps/figgos/.gitignore b/apps/figgos/.gitignore new file mode 100644 index 000000000..188387c04 --- /dev/null +++ b/apps/figgos/.gitignore @@ -0,0 +1 @@ +workbench/ diff --git a/apps/figgos/apps/backend/package.json b/apps/figgos/apps/backend/package.json index 8dabcd4e5..ce863b751 100644 --- a/apps/figgos/apps/backend/package.json +++ b/apps/figgos/apps/backend/package.json @@ -18,11 +18,14 @@ }, "dependencies": { "@figgos/shared": "workspace:*", + "@google/genai": "^1.14.0", + "@huggingface/transformers": "^3.8.1", "@manacore/shared-drizzle-config": "workspace:*", "@manacore/shared-nestjs-auth": "workspace:*", "@manacore/shared-nestjs-health": "workspace:*", "@manacore/shared-nestjs-metrics": "workspace:*", "@manacore/shared-nestjs-setup": "workspace:*", + "@manacore/shared-storage": "workspace:*", "@nestjs/common": "^10.4.15", "@nestjs/config": "^3.3.0", "@nestjs/core": "^10.4.15", @@ -35,7 +38,8 @@ "postgres": "^3.4.5", "prom-client": "^15.1.0", "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1" + "rxjs": "^7.8.1", + "sharp": "^0.34.5" }, "devDependencies": { "@nestjs/cli": "^10.4.9", diff --git a/apps/figgos/apps/backend/src/db/schema/figures.schema.ts b/apps/figgos/apps/backend/src/db/schema/figures.schema.ts index 10b2269d0..ad16676ac 100644 --- a/apps/figgos/apps/backend/src/db/schema/figures.schema.ts +++ b/apps/figgos/apps/backend/src/db/schema/figures.schema.ts @@ -8,7 +8,13 @@ import { jsonb, index, } from 'drizzle-orm/pg-core'; -import type { FigureRarity, FigureUserInput } from '@figgos/shared'; +import type { + FigureRarity, + FigureUserInput, + GeneratedProfile, + FigureLanguage, + FigureStatus, +} from '@figgos/shared'; export const figures = pgTable( 'figures', @@ -17,8 +23,12 @@ export const figures = pgTable( userId: text('user_id').notNull(), name: varchar('name', { length: 200 }).notNull(), userInput: jsonb('user_input').$type().notNull(), + generatedProfile: jsonb('generated_profile').$type(), imageUrl: text('image_url'), rarity: varchar('rarity', { length: 20 }).default('common').notNull().$type(), + language: varchar('language', { length: 5 }).default('en').notNull().$type(), + status: varchar('status', { length: 20 }).default('pending').notNull().$type(), + errorMessage: text('error_message'), isPublic: boolean('is_public').default(false).notNull(), isArchived: boolean('is_archived').default(false).notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), diff --git a/apps/figgos/apps/backend/src/figures/dto/create-figure.dto.ts b/apps/figgos/apps/backend/src/figures/dto/create-figure.dto.ts index a7c22d4d7..080f2604e 100644 --- a/apps/figgos/apps/backend/src/figures/dto/create-figure.dto.ts +++ b/apps/figgos/apps/backend/src/figures/dto/create-figure.dto.ts @@ -1,4 +1,5 @@ -import { IsString, IsNotEmpty, MaxLength, MinLength } from 'class-validator'; +import { IsString, IsNotEmpty, MaxLength, MinLength, IsOptional, IsIn } from 'class-validator'; +import type { FigureLanguage } from '@figgos/shared'; export class CreateFigureDto { @IsString() @@ -12,4 +13,9 @@ export class CreateFigureDto { @MinLength(1) @MaxLength(2000) description!: string; + + @IsOptional() + @IsString() + @IsIn(['en', 'de']) + language?: FigureLanguage = 'en'; } diff --git a/apps/figgos/apps/backend/src/figures/figures.controller.ts b/apps/figgos/apps/backend/src/figures/figures.controller.ts index 2f09e015e..4faf29b33 100644 --- a/apps/figgos/apps/backend/src/figures/figures.controller.ts +++ b/apps/figgos/apps/backend/src/figures/figures.controller.ts @@ -19,7 +19,12 @@ export class FiguresController { @Post() async create(@CurrentUser() user: CurrentUserData, @Body() dto: CreateFigureDto) { - const figure = await this.figuresService.create(user.userId, dto.name, dto.description); + const figure = await this.figuresService.create( + user.userId, + dto.name, + dto.description, + dto.language || 'en' + ); return { figure }; } diff --git a/apps/figgos/apps/backend/src/figures/figures.module.ts b/apps/figgos/apps/backend/src/figures/figures.module.ts index 7d9f13892..a7c4d22f4 100644 --- a/apps/figgos/apps/backend/src/figures/figures.module.ts +++ b/apps/figgos/apps/backend/src/figures/figures.module.ts @@ -1,8 +1,10 @@ import { Module } from '@nestjs/common'; import { FiguresController } from './figures.controller'; import { FiguresService } from './figures.service'; +import { GenerationModule } from '../generation/generation.module'; @Module({ + imports: [GenerationModule], controllers: [FiguresController], providers: [FiguresService], exports: [FiguresService], diff --git a/apps/figgos/apps/backend/src/figures/figures.service.ts b/apps/figgos/apps/backend/src/figures/figures.service.ts index c8709faff..6da0cac83 100644 --- a/apps/figgos/apps/backend/src/figures/figures.service.ts +++ b/apps/figgos/apps/backend/src/figures/figures.service.ts @@ -4,11 +4,20 @@ import { DATABASE_CONNECTION } from '../db/database.module'; import type { Database } from '../db/connection'; import { figures } from '../db/schema'; import type { Figure } from '../db/schema'; -import { RARITY_WEIGHTS, type FigureRarity } from '@figgos/shared'; +import { + RARITY_WEIGHTS, + getCardStyle, + type FigureRarity, + type FigureLanguage, +} from '@figgos/shared'; +import { GenerationService } from '../generation/generation.service'; @Injectable() export class FiguresService { - constructor(@Inject(DATABASE_CONNECTION) private db: Database) {} + constructor( + @Inject(DATABASE_CONNECTION) private db: Database, + private readonly generationService: GenerationService + ) {} rollRarity(): FigureRarity { const total = Object.values(RARITY_WEIGHTS).reduce((sum, w) => sum + w, 0); @@ -20,20 +29,38 @@ export class FiguresService { return 'common'; } - async create(userId: string, name: string, description: string): Promise
{ + async create( + userId: string, + name: string, + description: string, + language: FigureLanguage = 'en' + ): Promise
{ const rarity = this.rollRarity(); + const cardStyle = getCardStyle(rarity); + // Insert with status pending const [figure] = await this.db .insert(figures) .values({ userId, name, - userInput: { description }, rarity, + language, + userInput: { description, language }, + status: 'pending', }) .returning(); - return figure; + // Run full generation pipeline (synchronous) + const completed = await this.generationService.generateFigure(figure.id, userId, { + name, + description, + language, + rarity, + cardStyle, + }); + + return completed; } async findByUserId(userId: string): Promise { diff --git a/apps/figgos/apps/backend/src/generation/gemini.service.ts b/apps/figgos/apps/backend/src/generation/gemini.service.ts new file mode 100644 index 000000000..5ab7fb909 --- /dev/null +++ b/apps/figgos/apps/backend/src/generation/gemini.service.ts @@ -0,0 +1,138 @@ +import type { CardStyle, FigureLanguage, FigureRarity, GeneratedProfile } from '@figgos/shared'; +import { STAT_RANGES } from '@figgos/shared'; +import { GoogleGenAI } from '@google/genai'; +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { + PROFILE_JSON_SCHEMA, + PROFILE_SYSTEM_PROMPT, + buildImagePrompt, + buildProfileUserPrompt, +} from './prompts'; + +const TEXT_MODEL = 'gemini-3-flash-preview'; +const IMAGE_MODEL = 'gemini-2.5-flash-image'; + +@Injectable() +export class GeminiService { + private readonly logger = new Logger(GeminiService.name); + private readonly client: GoogleGenAI; + + constructor(private config: ConfigService) { + const apiKey = this.config.get('GEMINI_API_KEY'); + if (!apiKey) { + this.logger.warn('GEMINI_API_KEY not set — generation will fail'); + } + this.client = new GoogleGenAI({ apiKey: apiKey || '' }); + } + + async generateProfile( + name: string, + description: string, + rarity: FigureRarity, + language: FigureLanguage + ): Promise { + const statRange = STAT_RANGES[rarity]; + const userPrompt = buildProfileUserPrompt(name, description, rarity, statRange, language); + + this.logger.log(`Generating profile for "${name}" (${rarity})...`); + + const response = await this.client.models.generateContent({ + model: TEXT_MODEL, + contents: userPrompt, + config: { + systemInstruction: PROFILE_SYSTEM_PROMPT, + responseMimeType: 'application/json', + responseSchema: PROFILE_JSON_SCHEMA, + temperature: 1.0, + thinkingConfig: { thinkingBudget: 0 }, + }, + }); + + const text = response.text; + if (!text) { + throw new Error('Gemini returned empty text response'); + } + + const parsed = JSON.parse(text); + + // Validate required fields + if (!parsed.subtitle || !parsed.backstory || !parsed.visualDescription) { + throw new Error('Profile missing required text fields'); + } + if (!Array.isArray(parsed.items) || parsed.items.length !== 3) { + throw new Error(`Expected 3 items, got ${parsed.items?.length}`); + } + if (!parsed.stats || typeof parsed.stats.attack !== 'number') { + throw new Error('Profile has invalid stats'); + } + if (!parsed.specialAttack?.name || !parsed.specialAttack?.description) { + throw new Error('Profile missing specialAttack'); + } + + // Clamp stats to valid range + const clamp = (v: number) => Math.max(statRange.min, Math.min(statRange.max, v)); + parsed.stats.attack = clamp(parsed.stats.attack); + parsed.stats.defense = clamp(parsed.stats.defense); + parsed.stats.special = clamp(parsed.stats.special); + + const profile = parsed as GeneratedProfile; + this.logger.log(`Profile generated: "${profile.subtitle}"`); + return profile; + } + + async generateImage( + name: string, + subtitle: string, + visualDescription: string, + items: string[], + cardStyle: CardStyle, + faceImageUrl?: string | null + ): Promise { + const prompt = buildImagePrompt( + name, + subtitle, + visualDescription, + items, + cardStyle, + !!faceImageUrl + ); + + this.logger.log(`Generating image for "${name}" (${cardStyle})...`); + + // Build contents array — if face image provided, include it + const contents: Array = []; + + if (faceImageUrl) { + // TODO: Download face image from S3, convert to base64, add as inline data + // For now, face transfer is not yet supported in the backend + this.logger.warn('Face transfer not yet implemented in backend'); + } + + contents.push(prompt); + + const response = await this.client.models.generateContent({ + model: IMAGE_MODEL, + contents, + config: { + responseModalities: ['IMAGE', 'TEXT'], + }, + }); + + // Extract image from response + const parts = response.candidates?.[0]?.content?.parts; + if (!parts) { + throw new Error('Gemini returned no content parts'); + } + + for (const part of parts) { + if (part.inlineData?.data) { + const buffer = Buffer.from(part.inlineData.data, 'base64'); + this.logger.log(`Image generated: ${(buffer.length / 1024).toFixed(0)} KB`); + return buffer; + } + } + + throw new Error('Gemini returned no image data'); + } +} diff --git a/apps/figgos/apps/backend/src/generation/generation.module.ts b/apps/figgos/apps/backend/src/generation/generation.module.ts new file mode 100644 index 000000000..bedf52081 --- /dev/null +++ b/apps/figgos/apps/backend/src/generation/generation.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { GenerationService } from './generation.service'; +import { GeminiService } from './gemini.service'; +import { ImageProcessingService } from './image-processing.service'; +import { StorageModule } from '../storage/storage.module'; + +@Module({ + imports: [StorageModule], + providers: [GenerationService, GeminiService, ImageProcessingService], + exports: [GenerationService], +}) +export class GenerationModule {} diff --git a/apps/figgos/apps/backend/src/generation/generation.service.ts b/apps/figgos/apps/backend/src/generation/generation.service.ts new file mode 100644 index 000000000..f47e93b97 --- /dev/null +++ b/apps/figgos/apps/backend/src/generation/generation.service.ts @@ -0,0 +1,120 @@ +import { Injectable, Inject, Logger } from '@nestjs/common'; +import { eq } from 'drizzle-orm'; +import { DATABASE_CONNECTION } from '../db/database.module'; +import type { Database } from '../db/connection'; +import { figures } from '../db/schema'; +import type { Figure } from '../db/schema'; +import type { + FigureLanguage, + FigureRarity, + FigureStatus, + CardStyle, + GeneratedProfile, +} from '@figgos/shared'; +import { GeminiService } from './gemini.service'; +import { ImageProcessingService } from './image-processing.service'; +import { StorageService } from '../storage/storage.service'; + +@Injectable() +export class GenerationService { + private readonly logger = new Logger(GenerationService.name); + + constructor( + @Inject(DATABASE_CONNECTION) private db: Database, + private readonly geminiService: GeminiService, + private readonly imageProcessingService: ImageProcessingService, + private readonly storageService: StorageService + ) {} + + async generateFigure( + figureId: string, + userId: string, + input: { + name: string; + description: string; + language: FigureLanguage; + rarity: FigureRarity; + cardStyle: CardStyle; + } + ): Promise
{ + try { + // Phase 1: Generate profile via LLM + await this.updateStatus(figureId, 'generating_profile'); + const profile = await this.geminiService.generateProfile( + input.name, + input.description, + input.rarity, + input.language + ); + + // Save profile immediately (even if image gen fails, we keep the text) + await this.updateProfile(figureId, profile); + await this.updateStatus(figureId, 'generating_image'); + + // Phase 2: Generate image + const itemLabels = profile.items.map((item) => `${item.name} — ${item.description}`); + const pngBuffer = await this.geminiService.generateImage( + input.name, + profile.subtitle, + profile.visualDescription, + itemLabels, + input.cardStyle + ); + + // Phase 3: Process image (bg removal + WebP) + await this.updateStatus(figureId, 'processing'); + const webpBuffer = await this.imageProcessingService.removeBackground(pngBuffer); + + // Phase 4: Upload to S3 + const imageUrl = await this.storageService.uploadFigureImage(userId, figureId, webpBuffer); + + // Phase 5: Finalize + const [completed] = await this.db + .update(figures) + .set({ + imageUrl, + status: 'completed', + updatedAt: new Date(), + }) + .where(eq(figures.id, figureId)) + .returning(); + + this.logger.log(`Figure "${input.name}" generation completed`); + return completed; + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error'; + this.logger.error(`Generation failed for "${input.name}": ${message}`); + + try { + const [failed] = await this.db + .update(figures) + .set({ + status: 'failed', + errorMessage: message, + updatedAt: new Date(), + }) + .where(eq(figures.id, figureId)) + .returning(); + + return failed; + } catch (dbError) { + this.logger.error(`Failed to update error status for figure ${figureId}`, dbError); + throw error; + } + } + } + + private async updateStatus(figureId: string, status: FigureStatus): Promise { + await this.db + .update(figures) + .set({ status, updatedAt: new Date() }) + .where(eq(figures.id, figureId)); + } + + private async updateProfile(figureId: string, profile: GeneratedProfile): Promise { + await this.db + .update(figures) + .set({ generatedProfile: profile, updatedAt: new Date() }) + .where(eq(figures.id, figureId)); + } +} diff --git a/apps/figgos/apps/backend/src/generation/image-processing.service.ts b/apps/figgos/apps/backend/src/generation/image-processing.service.ts new file mode 100644 index 000000000..d6b31b387 --- /dev/null +++ b/apps/figgos/apps/backend/src/generation/image-processing.service.ts @@ -0,0 +1,92 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import sharp from 'sharp'; + +type BgRemovalMethod = 'feathered' | 'rmbg'; + +@Injectable() +export class ImageProcessingService implements OnModuleInit { + private readonly logger = new Logger(ImageProcessingService.name); + private method: BgRemovalMethod; + private segmenter: any = null; + + constructor(private config: ConfigService) { + this.method = (this.config.get('BG_REMOVAL_METHOD') || 'feathered') as BgRemovalMethod; + } + + onModuleInit() { + this.logger.log(`Background removal method: ${this.method}`); + if (this.method === 'rmbg') { + this.logger.log('RMBG-1.4 model will be lazy-loaded on first use'); + } + } + + async removeBackground(pngBuffer: Buffer): Promise { + if (this.method === 'rmbg') { + return this.removeWithRmbg(pngBuffer); + } + return this.removeWithThreshold(pngBuffer); + } + + /** + * Feathered threshold background removal (~77ms). + * Removes near-white pixels with a soft edge transition. + * T=240, feather=10. Output: WebP quality 85. + */ + private async removeWithThreshold(inputBuffer: Buffer): Promise { + const { data, info } = await sharp(inputBuffer) + .ensureAlpha() + .raw() + .toBuffer({ resolveWithObject: true }); + + const T = 240; + const F = 10; + const low = T - F; + + for (let i = 0; i < data.length; i += 4) { + const m = Math.min(data[i], data[i + 1], data[i + 2]); + if (m > T) { + data[i + 3] = 0; + } else if (m > low) { + data[i + 3] = Math.min(data[i + 3], Math.round((255 * (T - m)) / F)); + } + } + + return sharp(data, { raw: { width: info.width, height: info.height, channels: 4 } }) + .webp({ quality: 85 }) + .toBuffer(); + } + + /** + * RMBG-1.4 AI background removal (~1s). + * Uses @huggingface/transformers pipeline. Model is lazy-loaded and cached. + */ + private async removeWithRmbg(inputBuffer: Buffer): Promise { + if (!this.segmenter) { + this.logger.log('Loading RMBG-1.4 model (first use)...'); + const { pipeline } = await import('@huggingface/transformers'); + this.segmenter = await pipeline('background-removal', 'briaai/RMBG-1.4'); + this.logger.log('RMBG-1.4 model loaded'); + } + + // HF transformers pipeline needs a file path or URL, not a buffer + // Write to a temp file, process, then clean up + const { writeFile, unlink } = await import('node:fs/promises'); + const { join } = await import('node:path'); + const { tmpdir } = await import('node:os'); + const { randomUUID } = await import('node:crypto'); + const tmpPath = join(tmpdir(), `figgos-rmbg-${randomUUID()}.png`); + + try { + await writeFile(tmpPath, inputBuffer); + const result = await this.segmenter(tmpPath); + const img = Array.isArray(result) ? result[0] : result; + const { data, width, height, channels } = img; + const buf = Buffer.from(data); + + return sharp(buf, { raw: { width, height, channels } }).webp({ quality: 85 }).toBuffer(); + } finally { + await unlink(tmpPath).catch(() => {}); + } + } +} diff --git a/apps/figgos/apps/backend/src/generation/prompts.ts b/apps/figgos/apps/backend/src/generation/prompts.ts new file mode 100644 index 000000000..5671b9db1 --- /dev/null +++ b/apps/figgos/apps/backend/src/generation/prompts.ts @@ -0,0 +1,228 @@ +import type { CardStyle, FigureLanguage } from '@figgos/shared'; + +// ══════════════════════════════════════════════════════════════ +// Profile Generation — System Prompt +// ══════════════════════════════════════════════════════════════ + +export const PROFILE_SYSTEM_PROMPT = `You are the creative engine behind FIGGOS — a collectible action figure game. Users give you a name and a short description of a character, and you generate a full character profile for their collectible figure card. + +The figure will be rendered as a hyperrealistic miniature sculpture (6 inches tall) inside toy blister packaging. Your job is to flesh out the character with personality, lore, and visual detail. + +IMPORTANT RULES: +- The \`visualDescription\` must describe ONLY the figure itself (person, clothing, pose, expression). NOT the packaging, card, or background. It will be inserted into an image prompt after "In the left compartment stands the figure:". +- The \`visualDescription\` should be grounded and specific — real fabrics, real colors, real materials. Avoid vague fantasy language. Think "rumpled beige linen trenchcoat" not "magical cloak of mystery". +- IMPORTANT: Never use pure white for clothing, accessories, or any part of the figure. Use off-white, cream, ivory, light gray, or eggshell instead. The figure will be photographed on a white background, so pure white areas would blend into the background. +- Items must be concrete physical objects that can be shown as miniature accessories in blister compartments. Think action figure accessories: weapons, tools, personal objects. NOT abstract concepts. +- The backstory should be dramatic but concise. Written like the back of a trading card. +- Stats must be within the provided range for the rarity level. +- The \`specialAttack\` is the figure's signature move. It should connect thematically to the character and can reference one of their items (e.g. a chef might hurl their rolling pin). Higher rarity figures should have more dramatic, over-the-top special attacks. Common = practical and grounded. Legendary = cinematic and awe-inspiring.`; + +export function buildProfileUserPrompt( + name: string, + description: string, + rarity: string, + statRange: { min: number; max: number }, + language: FigureLanguage +): string { + const langInstruction = + language === 'de' + ? '\n\nIMPORTANT: Generate ALL text content (subtitle, backstory, items, specialAttack) in German. Only the visualDescription should remain in English (it feeds into an image generation prompt).' + : ''; + + return `Generate a full character profile for this collectible figure: + +**Name:** ${name} +**Description:** ${description} +**Rarity:** ${rarity.toUpperCase()} + +**Stats range for ${rarity}:** each stat (attack, defense, special) must be between ${statRange.min} and ${statRange.max}. + +Generate: subtitle, 3 items, backstory, stats, special attack, and a detailed visual description of the figure.${langInstruction}`; +} + +// ══════════════════════════════════════════════════════════════ +// Gemini JSON Schema for structured output +// ══════════════════════════════════════════════════════════════ + +export const PROFILE_JSON_SCHEMA = { + type: 'object' as const, + properties: { + subtitle: { + type: 'string' as const, + description: 'A short role/title for the figure, 2-6 words. Like a job title or faction.', + }, + items: { + type: 'array' as const, + items: { + type: 'object' as const, + properties: { + name: { + type: 'string' as const, + description: 'Short punchy item name, 1-4 words.', + }, + description: { + type: 'string' as const, + description: "One sentence describing the item's appearance.", + }, + lore: { + type: 'string' as const, + description: 'One sentence of flavor text.', + }, + }, + required: ['name', 'description', 'lore'] as const, + }, + description: 'Exactly 3 physical accessories/items the figure carries.', + }, + backstory: { + type: 'string' as const, + description: '2-3 sentences of character lore.', + }, + stats: { + type: 'object' as const, + properties: { + attack: { type: 'integer' as const }, + defense: { type: 'integer' as const }, + special: { type: 'integer' as const }, + }, + required: ['attack', 'defense', 'special'] as const, + }, + specialAttack: { + type: 'object' as const, + properties: { + name: { + type: 'string' as const, + description: 'Short punchy attack name, 2-4 words.', + }, + description: { + type: 'string' as const, + description: '1-2 sentences describing the attack.', + }, + }, + required: ['name', 'description'] as const, + }, + visualDescription: { + type: 'string' as const, + description: + 'Detailed physical description of the figure as a miniature collectible sculpture. 3-5 sentences.', + }, + }, + required: [ + 'subtitle', + 'items', + 'backstory', + 'stats', + 'specialAttack', + 'visualDescription', + ] as const, +}; + +// ══════════════════════════════════════════════════════════════ +// Image Generation — Rarity Styles +// ══════════════════════════════════════════════════════════════ + +interface RarityStyle { + card: string; + textStyle: string; + tag: string; + vibe: string; +} + +export const RARITY_STYLES: Record = { + // -- Common: Everyday packaging, several sub-types -- + common_kraft: { + card: 'Warm tan kraft cardboard backing card with natural paper fiber texture.', + textStyle: 'bold black uppercase', + tag: 'A small rounded rectangular tag in the top-right corner of the card reading "COMMON" in dark gray text on a light gray background.', + vibe: '', + }, + common_white: { + card: 'Clean white matte cardstock backing card with a subtle linen texture.', + textStyle: 'bold black uppercase', + tag: 'A small rounded rectangular tag in the top-right corner of the card reading "COMMON" in dark gray text on a light gray background.', + vibe: '', + }, + common_mint: { + card: 'Matte mint-green cardstock backing card with a clean modern feel.', + textStyle: 'bold dark green uppercase', + tag: 'A small rounded rectangular tag in the top-right corner of the card reading "COMMON" in dark green text on a pale green background.', + vibe: '', + }, + common_warm: { + card: 'Warm terracotta/clay-colored matte cardstock backing card.', + textStyle: 'bold dark brown uppercase', + tag: 'A small rounded rectangular tag in the top-right corner of the card reading "COMMON" in brown text on a pale peach background.', + vibe: '', + }, + + // -- Rare: Blue/Silver premium -- + rare: { + card: 'Dark navy blue matte cardstock backing card with a subtle brushed metal texture.', + textStyle: 'silver metallic uppercase', + tag: 'A metallic blue tag with silver border in the top-right corner reading "RARE" in silver text. The tag has a subtle shine.', + vibe: 'The overall packaging has a premium, curated feel — a step above standard retail.', + }, + + // -- Epic: Neon/Holographic, high energy -- + epic: { + card: 'Glossy black cardstock backing card covered in a vivid holographic rainbow foil pattern that shifts between electric purple, hot pink, and cyan as light hits it. The entire card surface shimmers and refracts light like an oil slick. Bright neon purple geometric accent lines and circuit-board-style patterns are printed over the holographic surface.', + textStyle: + 'neon purple glowing uppercase text that appears to emit light, with a bright pink-to-cyan gradient and a visible luminous halo around each letter', + tag: 'A large holographic tag in the top-right corner reading "EPIC" in bold glowing white text on a shifting purple-pink-cyan iridescent background. The tag is eye-catching and impossible to miss — it pulses with energy.', + vibe: 'This packaging is LOUD and electric. It demands attention. The holographic surface throws rainbow light everywhere. The neon accents make it feel like it belongs in an arcade or a cyberpunk display case. Nothing about this is subtle.', + }, + + // -- Legendary: Ultra-luxury black & gold, museum piece -- + legendary: { + card: 'Ultra-premium heavyweight matte black cardstock with a wide ornate gold foil border featuring intricate filigree scrollwork patterns embossed into the card. Gold foil decorative corner pieces with Art Deco geometric designs. A subtle gold foil crest or emblem is centered above the figure name. The card itself has a soft-touch velvet-like texture.', + textStyle: + 'large gold foil embossed uppercase with deep dimensional relief — the letters are stamped into the card and filled with brilliant reflective gold that catches studio light with a mirror-like gleam', + tag: 'An oversized ornate gold foil tag in the top-right corner with decorative Art Deco border reading "LEGENDARY" in bold black serif text on a brilliant gold background. The tag has visible embossed texture and gleams like real jewelry under the light.', + vibe: "This is a once-in-a-lifetime collector's grail piece. The gold foil work is exquisite and covers significant portions of the card — borders, corners, crest, lettering. It looks like it belongs in a glass display case in a luxury boutique, not on a shelf. The contrast of deep matte black and brilliant gold is stunning. Every detail communicates exclusivity and extreme rarity. This packaging alone is worth collecting.", + }, +}; + +// ══════════════════════════════════════════════════════════════ +// Image Generation — Prompt Builder +// ══════════════════════════════════════════════════════════════ + +const REALISM_BLOCK = `This is not a plastic toy — it is a perfect miniature real human being, 6 inches tall, frozen in place. The skin has actual pores and subtle color variation. The clothing is real miniaturized fabric with natural drape, wrinkles, and weave texture. The materials are genuine — real leather grain, real fabric thread, real hair strands catching the light.`; + +export function buildImagePrompt( + name: string, + subtitle: string, + visualDescription: string, + items: string[], + cardStyle: CardStyle, + hasFace: boolean = false +): string { + const style = RARITY_STYLES[cardStyle]; + const itemsText = items + .slice(0, 3) + .map((item) => ` - ${item}`) + .join('\n'); + + const faceInstruction = hasFace + ? `\n\nCRITICAL — FACE TRANSFER: The provided reference photo shows the person's real face. The miniature figure MUST have this EXACT face — same facial structure, same features, same expression — but rendered in the miniature figure style. Preserve the likeness perfectly while matching the figure's aesthetic.` + : ''; + + return `Product photograph of a premium collectible figure in sealed blister packaging on a pure white background. Package fills 95% of frame. + +${style.card} Hanging hole at top center. Clear plastic blister with molded compartments. + +${style.tag} + +Name in ${style.textStyle}: "${name.toUpperCase()}" large at the top. "${subtitle.toUpperCase()}" in smaller text below. + +In the left compartment stands the figure: ${visualDescription} + +${REALISM_BLOCK} +${style.vibe}${faceInstruction} + +Three accessories in separate molded blister compartments on the right side, stacked vertically: +${itemsText} +Each accessory is detailed, clearly visible, and generously sized. + +IMPORTANT: The figure and accessories must NOT contain pure white (#FFFFFF) areas. Use off-white, cream, ivory, or light gray instead of white for any clothing, skin highlights, or materials. Pure white is reserved for the background only. + +Pure white background, soft even studio lighting, product catalog quality. 85mm lens, sharp focus.`; +} diff --git a/apps/figgos/apps/backend/src/main.ts b/apps/figgos/apps/backend/src/main.ts index 6ef4d049f..cee04a4e3 100644 --- a/apps/figgos/apps/backend/src/main.ts +++ b/apps/figgos/apps/backend/src/main.ts @@ -4,5 +4,5 @@ import { AppModule } from './app.module'; bootstrapApp(AppModule, { defaultPort: 3025, serviceName: 'Figgos', - additionalCorsOrigins: ['http://localhost:5181'], + additionalCorsOrigins: ['http://localhost:5196'], }); diff --git a/apps/figgos/apps/backend/src/storage/storage.module.ts b/apps/figgos/apps/backend/src/storage/storage.module.ts new file mode 100644 index 000000000..2e07e3a15 --- /dev/null +++ b/apps/figgos/apps/backend/src/storage/storage.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { StorageService } from './storage.service'; + +@Module({ + providers: [StorageService], + exports: [StorageService], +}) +export class StorageModule {} diff --git a/apps/figgos/apps/backend/src/storage/storage.service.ts b/apps/figgos/apps/backend/src/storage/storage.service.ts new file mode 100644 index 000000000..f2eaddbca --- /dev/null +++ b/apps/figgos/apps/backend/src/storage/storage.service.ts @@ -0,0 +1,30 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { createFiggosStorage, type StorageClient } from '@manacore/shared-storage'; + +@Injectable() +export class StorageService implements OnModuleInit { + private readonly logger = new Logger(StorageService.name); + private storage!: StorageClient; + + constructor(private config: ConfigService) {} + + onModuleInit() { + const publicUrl = this.config.get('FIGGOS_STORAGE_PUBLIC_URL'); + this.storage = createFiggosStorage(publicUrl); + this.logger.log('Storage initialized (bucket: figgos-storage)'); + } + + async uploadFigureImage(userId: string, figureId: string, buffer: Buffer): Promise { + const key = `${userId}/${figureId}.webp`; + + const result = await this.storage.upload(key, buffer, { + contentType: 'image/webp', + public: true, + }); + + const url = result.url || this.storage.getPublicUrl(key); + this.logger.log(`Uploaded figure image: ${key}`); + return url || key; + } +} diff --git a/apps/figgos/apps/mobile/app/(tabs)/carousel.tsx b/apps/figgos/apps/mobile/app/(tabs)/carousel.tsx index 3ab0ffa89..aedfb0d17 100644 --- a/apps/figgos/apps/mobile/app/(tabs)/carousel.tsx +++ b/apps/figgos/apps/mobile/app/(tabs)/carousel.tsx @@ -1,9 +1,17 @@ -import { useRef } from 'react'; -import { View, Text, Image, Pressable, Animated, Dimensions } from 'react-native'; +import { useRef, useState, useCallback } from 'react'; +import { + View, + Text, + Image, + Pressable, + Animated, + Dimensions, + ActivityIndicator, +} from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; -import { useRouter } from 'expo-router'; -import { CARDS } from '../../data/cards'; -import type { FigureRarity } from '@figgos/shared'; +import { useRouter, useFocusEffect } from 'expo-router'; +import type { FigureResponse, FigureRarity } from '@figgos/shared'; +import { api } from '../../services/api'; const { width: SCREEN_WIDTH } = Dimensions.get('window'); const CARD_WIDTH = SCREEN_WIDTH * 0.65; @@ -21,6 +29,35 @@ const RARITY_COLORS: Record = { export default function CarouselScreen() { const router = useRouter(); const scrollX = useRef(new Animated.Value(0)).current; + const [figures, setFigures] = useState([]); + const [loading, setLoading] = useState(true); + + useFocusEffect( + useCallback(() => { + api.figures + .list() + .then(({ figures }) => setFigures(figures)) + .finally(() => setLoading(false)); + }, []) + ); + + if (loading) { + return ( + + + + ); + } + + if (figures.length === 0) { + return ( + + + No figures yet + + + ); + } return ( @@ -35,7 +72,7 @@ export default function CarouselScreen() { item.id} horizontal showsHorizontalScrollIndicator={false} @@ -94,11 +131,31 @@ export default function CarouselScreen() { overflow: 'hidden', }} > - + {item.imageUrl ? ( + + ) : ( + + + {item.name} + + + )} @@ -108,7 +165,7 @@ export default function CarouselScreen() { - {CARDS.map((card, i) => { + {figures.map((figure, i) => { const inputRange = [ (i - 1) * (CARD_WIDTH + SPACING), i * (CARD_WIDTH + SPACING), @@ -129,12 +186,12 @@ export default function CarouselScreen() { return ( router.push(`/card/v2/${card.id}` as any)} + onPress={() => router.push(`/card/v2/${figure.id}` as any)} style={{ width: CARD_WIDTH }} className="active:opacity-80" > @@ -27,13 +37,52 @@ function CardThumbnail({ card }: { card: CardData }) { overflow: 'hidden', }} > - + {figure.imageUrl ? ( + + ) : ( + + + {figure.name} + + + )} ); } export default function CollectionScreen() { + const [figures, setFigures] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useFocusEffect( + useCallback(() => { + setLoading(true); + api.figures + .list() + .then(({ figures }) => { + setFigures(figures); + setError(null); + }) + .catch((e) => setError(e.message)) + .finally(() => setLoading(false)); + }, []) + ); + return ( @@ -44,18 +93,42 @@ export default function CollectionScreen() { Collection - {CARDS.length} {CARDS.length === 1 ? 'Figgo' : 'Figgos'} + {loading ? '...' : `${figures.length} ${figures.length === 1 ? 'Figgo' : 'Figgos'}`} - item.id} - contentContainerStyle={{ paddingHorizontal: PADDING, paddingBottom: 40 }} - columnWrapperStyle={{ gap: GAP, marginBottom: GAP }} - renderItem={({ item }) => } - /> + {loading ? ( + + + + ) : error ? ( + + + {error} + + + ) : figures.length === 0 ? ( + + + No figures yet + + + Create your first Figgo! + + + ) : ( + item.id} + contentContainerStyle={{ paddingHorizontal: PADDING, paddingBottom: 40 }} + columnWrapperStyle={{ gap: GAP, marginBottom: GAP }} + renderItem={({ item }) => } + /> + )} ); } diff --git a/apps/figgos/apps/mobile/app/(tabs)/index.tsx b/apps/figgos/apps/mobile/app/(tabs)/index.tsx index 974bd8006..bdd9209ca 100644 --- a/apps/figgos/apps/mobile/app/(tabs)/index.tsx +++ b/apps/figgos/apps/mobile/app/(tabs)/index.tsx @@ -8,9 +8,11 @@ import { KeyboardAvoidingView, Platform, ActivityIndicator, + Image, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import type { FigureResponse, FigureRarity } from '@figgos/shared'; +import { api } from '../../services/api'; // ── Rarity ── @@ -58,6 +60,46 @@ function RarityBadge({ rarity }: { rarity: FigureRarity }) { ); } +// ── Stat Bar ── + +const STAT_COLORS = { + attack: 'rgb(255, 51, 102)', + defense: 'rgb(0, 210, 170)', + special: 'rgb(180, 130, 255)', +}; + +function StatBar({ label, value, color }: { label: string; value: number; color: string }) { + return ( + + + {label} + + + + + + {value} + + + ); +} + // ── Screen ── export default function CreateScreen() { @@ -75,28 +117,8 @@ export default function CreateScreen() { setLoading(true); setError(null); try { - await new Promise((r) => setTimeout(r, 1500)); - const rarities: FigureRarity[] = [ - 'common', - 'common', - 'common', - 'rare', - 'rare', - 'epic', - 'legendary', - ]; - setResult({ - id: 'mock-id', - userId: 'mock-user', - name: name.trim(), - userInput: { description: description.trim() }, - imageUrl: null, - rarity: rarities[Math.floor(Math.random() * rarities.length)], - isPublic: false, - isArchived: false, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }); + const { figure } = await api.figures.create(name.trim(), description.trim()); + setResult(figure); } catch (e: any) { setError(e.message || 'Something went wrong'); } finally { @@ -111,6 +133,8 @@ export default function CreateScreen() { setError(null); }; + const profile = result?.generatedProfile; + // ── Result ── if (result) { return ( @@ -150,19 +174,33 @@ export default function CreateScreen() { style={{ borderWidth: 3, borderColor: 'rgb(255, 204, 0)', padding: 24 }} > {/* Image */} - - - Image coming soon - - + {result.imageUrl ? ( + + ) : ( + + + {result.status === 'failed' ? 'Generation failed' : 'No image'} + + + )} {result.name} - - {result.userInput.description} - + + {profile?.subtitle && ( + + {profile.subtitle} + + )} + + {profile?.backstory && ( + + {profile.backstory} + + )} + + {/* Stats */} + {profile?.stats && ( + + + + + + )} + + {/* Special Attack */} + {profile?.specialAttack && ( + + + {profile.specialAttack.name} + + + {profile.specialAttack.description} + + + )} + + {/* Error message */} + {result.status === 'failed' && result.errorMessage && ( + + {result.errorMessage} + + )} @@ -385,7 +487,7 @@ export default function CreateScreen() { textTransform: 'uppercase', }} > - Rolling... + Generating... ) : ( diff --git a/apps/figgos/apps/mobile/app/(tabs)/shelf.tsx b/apps/figgos/apps/mobile/app/(tabs)/shelf.tsx index 872f5ece6..bb9a2f890 100644 --- a/apps/figgos/apps/mobile/app/(tabs)/shelf.tsx +++ b/apps/figgos/apps/mobile/app/(tabs)/shelf.tsx @@ -1,8 +1,17 @@ -import { View, Text, Image, Pressable, ScrollView, Dimensions } from 'react-native'; +import { useState, useCallback } from 'react'; +import { + View, + Text, + Image, + Pressable, + ScrollView, + Dimensions, + ActivityIndicator, +} from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; -import { useRouter } from 'expo-router'; -import { CARDS, type CardData } from '../../data/cards'; -import type { FigureRarity } from '@figgos/shared'; +import { useRouter, useFocusEffect } from 'expo-router'; +import type { FigureResponse, FigureRarity } from '@figgos/shared'; +import { api } from '../../services/api'; const { width: SCREEN_WIDTH } = Dimensions.get('window'); const CARD_WIDTH = SCREEN_WIDTH * 0.32; @@ -24,11 +33,11 @@ const RARITY_LABELS: Record = { common: 'COMMON', }; -function ShelfRow({ rarity, cards }: { rarity: FigureRarity; cards: CardData[] }) { +function ShelfRow({ rarity, figures }: { rarity: FigureRarity; figures: FigureResponse[] }) { const router = useRouter(); const color = RARITY_COLORS[rarity]; - if (cards.length === 0) return null; + if (figures.length === 0) return null; return ( @@ -51,12 +60,12 @@ function ShelfRow({ rarity, cards }: { rarity: FigureRarity; cards: CardData[] } showsHorizontalScrollIndicator={false} contentContainerStyle={{ paddingHorizontal: 20 }} > - {cards.map((card, i) => ( + {figures.map((figure, i) => ( router.push(`/card/v2/${card.id}` as any)} + key={figure.id} + onPress={() => router.push(`/card/v2/${figure.id}` as any)} className="active:opacity-80" - style={{ marginRight: i < cards.length - 1 ? OVERLAP : 0 }} + style={{ marginRight: i < figures.length - 1 ? OVERLAP : 0 }} > - + {figure.imageUrl ? ( + + ) : ( + + + {figure.name} + + + )} ))} @@ -91,11 +120,31 @@ function ShelfRow({ rarity, cards }: { rarity: FigureRarity; cards: CardData[] } } export default function ShelfScreen() { + const [figures, setFigures] = useState([]); + const [loading, setLoading] = useState(true); + + useFocusEffect( + useCallback(() => { + api.figures + .list() + .then(({ figures }) => setFigures(figures)) + .finally(() => setLoading(false)); + }, []) + ); + const grouped = RARITY_ORDER.map((rarity) => ({ rarity, - cards: CARDS.filter((c) => c.rarity === rarity), + figures: figures.filter((f) => f.rarity === rarity), })); + if (loading) { + return ( + + + + ); + } + return ( @@ -108,8 +157,8 @@ export default function ShelfScreen() { - {grouped.map(({ rarity, cards }) => ( - + {grouped.map(({ rarity, figures }) => ( + ))} diff --git a/apps/figgos/apps/mobile/app/(tabs)/stack.tsx b/apps/figgos/apps/mobile/app/(tabs)/stack.tsx index e95f90552..13e80f614 100644 --- a/apps/figgos/apps/mobile/app/(tabs)/stack.tsx +++ b/apps/figgos/apps/mobile/app/(tabs)/stack.tsx @@ -1,9 +1,18 @@ import { useState, useRef, useCallback } from 'react'; -import { View, Text, Image, Pressable, Animated, Dimensions, PanResponder } from 'react-native'; +import { + View, + Text, + Image, + Pressable, + Animated, + Dimensions, + PanResponder, + ActivityIndicator, +} from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; -import { useRouter } from 'expo-router'; -import { CARDS } from '../../data/cards'; -import type { FigureRarity } from '@figgos/shared'; +import { useRouter, useFocusEffect } from 'expo-router'; +import type { FigureResponse, FigureRarity } from '@figgos/shared'; +import { api } from '../../services/api'; const { width: SCREEN_WIDTH } = Dimensions.get('window'); const CARD_WIDTH = SCREEN_WIDTH * 0.72; @@ -21,13 +30,28 @@ const RARITY_COLORS: Record = { export default function StackScreen() { const router = useRouter(); - const [order, setOrder] = useState(() => CARDS.map((_, i) => i)); + const [figures, setFigures] = useState([]); + const [loading, setLoading] = useState(true); + const [order, setOrder] = useState([]); const [currentIndex, setCurrentIndex] = useState(0); const swipeY = useRef(new Animated.Value(0)).current; const isAnimating = useRef(false); + useFocusEffect( + useCallback(() => { + api.figures + .list() + .then(({ figures }) => { + setFigures(figures); + setOrder(figures.map((_, i) => i)); + setCurrentIndex(0); + }) + .finally(() => setLoading(false)); + }, []) + ); + const dismissTop = useCallback(() => { - if (isAnimating.current) return; + if (isAnimating.current || figures.length === 0) return; isAnimating.current = true; Animated.timing(swipeY, { @@ -36,11 +60,11 @@ export default function StackScreen() { useNativeDriver: true, }).start(() => { setOrder((prev) => [...prev.slice(1), prev[0]]); - setCurrentIndex((prev) => (prev + 1) % CARDS.length); + setCurrentIndex((prev) => (prev + 1) % figures.length); swipeY.setValue(0); isAnimating.current = false; }); - }, [swipeY]); + }, [swipeY, figures.length]); const snapBack = useCallback(() => { Animated.spring(swipeY, { @@ -70,7 +94,25 @@ export default function StackScreen() { }) ).current; - const topCard = CARDS[order[0]]; + if (loading) { + return ( + + + + ); + } + + if (figures.length === 0) { + return ( + + + No figures yet + + + ); + } + + const topFigure = figures[order[0]]; const topOpacity = swipeY.interpolate({ inputRange: [-200, 0], @@ -78,7 +120,6 @@ export default function StackScreen() { extrapolate: 'clamp', }); - // Total height needed: card + stack peek area const stackHeight = CARD_HEIGHT + (VISIBLE_STACK - 1) * STACK_OFFSET; return ( @@ -97,17 +138,17 @@ export default function StackScreen() { - {/* Background cards — each peeks out below the one above */} {order .slice(1, VISIBLE_STACK) .reverse() .map((cardIdx, reverseI) => { - const depth = VISIBLE_STACK - 1 - reverseI; // 3, 2, 1 - const card = CARDS[cardIdx]; - const shrink = depth * 8; // each card slightly narrower + const depth = VISIBLE_STACK - 1 - reverseI; + const figure = figures[cardIdx]; + if (!figure) return null; + const shrink = depth * 8; return ( - + {figure.imageUrl ? ( + + ) : ( + + + {figure.name} + + + )} ); })} - {/* Top card */} - - router.push(`/card/v2/${topCard.id}` as any)} - className="active:opacity-90" - style={{ width: '100%', height: '100%' }} + {topFigure && ( + - router.push(`/card/v2/${topFigure.id}` as any)} + className="active:opacity-90" + style={{ width: '100%', height: '100%' }} > - - - - + + {topFigure.imageUrl ? ( + + ) : ( + + + {topFigure.name} + + + )} + + + + )} - {/* Dot indicator */} - {CARDS.map((card, i) => ( + {figures.map((figure, i) => ( ))} diff --git a/apps/figgos/apps/mobile/app/card/[id].tsx b/apps/figgos/apps/mobile/app/card/[id].tsx index b77cc875b..88098c5cb 100644 --- a/apps/figgos/apps/mobile/app/card/[id].tsx +++ b/apps/figgos/apps/mobile/app/card/[id].tsx @@ -1,4 +1,5 @@ -import { View, Text, Pressable, Image, Dimensions } from 'react-native'; +import { useState, useEffect } from 'react'; +import { View, Text, Pressable, Image, Dimensions, ActivityIndicator } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useLocalSearchParams, useRouter } from 'expo-router'; import Animated, { @@ -8,8 +9,8 @@ import Animated, { interpolate, Easing, } from 'react-native-reanimated'; -import { CARDS } from '../../data/cards'; -import type { FigureRarity } from '@figgos/shared'; +import type { FigureResponse, FigureRarity } from '@figgos/shared'; +import { api } from '../../services/api'; const { width: SCREEN_WIDTH } = Dimensions.get('window'); const CARD_WIDTH = SCREEN_WIDTH - 48; @@ -31,7 +32,17 @@ const STAT_COLORS = { export default function CardDetailScreen() { const { id } = useLocalSearchParams<{ id: string }>(); const router = useRouter(); - const card = CARDS.find((c) => c.id === id); + const [figure, setFigure] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (!id) return; + api.figures + .get(id) + .then(({ figure }) => setFigure(figure)) + .catch(() => setFigure(null)) + .finally(() => setLoading(false)); + }, [id]); const rotation = useSharedValue(0); const isFlipped = useSharedValue(false); @@ -61,7 +72,15 @@ export default function CardDetailScreen() { }; }); - if (!card) { + if (loading) { + return ( + + + + ); + } + + if (!figure) { return ( @@ -71,9 +90,10 @@ export default function CardDetailScreen() { ); } + const profile = figure.generatedProfile; + return ( - {/* Back button */} router.back()} className="active:opacity-70" @@ -87,36 +107,37 @@ export default function CardDetailScreen() { - {/* ── Front: just the image ── */} + {/* Front */} - + {figure.imageUrl ? ( + + ) : ( + + + {figure.name} + + + )} - {/* ── Back ── */} + {/* Back */} - {/* Shadow layer */} - {/* Card back */} - {/* Header */} - {card.name} - - - {card.subtitle} + {figure.name} + {profile?.subtitle && ( + + {profile.subtitle} + + )} - {/* Description */} - {card.description} + {profile?.backstory || figure.userInput.description} - {/* Stats */} - - - Stats - - - - - + {profile?.stats && ( + + + Stats + + + + + + )} - {/* Bottom: rarity + ID */} - {card.rarity} + {figure.rarity} - #{card.id.split('-').pop()?.toUpperCase()} + #{figure.id.split('-').pop()?.toUpperCase()} @@ -260,12 +288,7 @@ function StatBar({ label, value, color }: { label: string; value: number; color: style={{ height: 10, borderWidth: 1, borderColor: 'rgb(50, 50, 80)' }} > (); const router = useRouter(); - const card = CARDS.find((c) => c.id === id); + const [figure, setFigure] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (!id) return; + api.figures + .get(id) + .then(({ figure }) => setFigure(figure)) + .catch(() => setFigure(null)) + .finally(() => setLoading(false)); + }, [id]); // Track the actual rendered image size const [imageSize, setImageSize] = useState({ width: CONTAINER_WIDTH, height: CONTAINER_HEIGHT }); - const handleImageLayout = (e: { nativeEvent: { layout: { width: number; height: number } } }) => { - // Image uses contain, so we can read the actual layout - // But we need the source dimensions to compute the real rendered area - }; - - // Use Image.resolveAssetSource to get original dimensions, then compute contain size const computeContainSize = (srcW: number, srcH: number) => { const ratio = Math.min(CONTAINER_WIDTH / srcW, CONTAINER_HEIGHT / srcH); setImageSize({ @@ -72,7 +76,6 @@ export default function CardDetailV2Screen() { rotateY.value = withSpring(snapTo, SPRING_CONFIG); }); - // Double tap to do a full flip const doubleTap = Gesture.Tap() .numberOfTaps(2) .onEnd(() => { @@ -93,7 +96,15 @@ export default function CardDetailV2Screen() { backfaceVisibility: 'hidden' as const, })); - if (!card) { + if (loading) { + return ( + + + + ); + } + + if (!figure) { return ( @@ -103,6 +114,8 @@ export default function CardDetailV2Screen() { ); } + const profile = figure.generatedProfile; + return ( {/* Back button */} @@ -116,14 +129,14 @@ export default function CardDetailV2Screen() { - V2 — Gesture 3D + Drag to rotate · Double-tap to flip - {/* ── Front: just the image ── */} + {/* ── Front: the image ── */} - { - const { width: srcW, height: srcH } = e.nativeEvent.source; - computeContainSize(srcW, srcH); - }} - /> + {figure.imageUrl ? ( + { + const { width: srcW, height: srcH } = e.nativeEvent.source; + computeContainSize(srcW, srcH); + }} + /> + ) : ( + + + {figure.name} + + + No image + + + )} {/* ── Back ── */} @@ -165,7 +197,7 @@ export default function CardDetailV2Screen() { right: -5, bottom: -5, borderRadius: 16, - backgroundColor: RARITY_COLORS[card.rarity], + backgroundColor: RARITY_COLORS[figure.rarity], opacity: 0.3, }} /> @@ -175,7 +207,7 @@ export default function CardDetailV2Screen() { style={{ flex: 1, borderWidth: 3, - borderColor: RARITY_COLORS[card.rarity], + borderColor: RARITY_COLORS[figure.rarity], padding: 20, justifyContent: 'space-between', }} @@ -206,20 +238,22 @@ export default function CardDetailV2Screen() { className="text-foreground" style={{ fontSize: 20, fontWeight: '900', letterSpacing: -0.5 }} > - {card.name} - - - {card.subtitle} + {figure.name} + {profile?.subtitle && ( + + {profile.subtitle} + + )} {/* Description */} @@ -228,27 +262,54 @@ export default function CardDetailV2Screen() { style={{ fontSize: 13, lineHeight: 20 }} numberOfLines={5} > - {card.description} + {profile?.backstory || figure.userInput.description} {/* Stats */} - - - Stats - - - - - + {profile?.stats && ( + + + Stats + + + + + + )} + + {/* Special Attack */} + {profile?.specialAttack && ( + + + ⚡ {profile.specialAttack.name} + + + )} {/* Bottom: rarity + ID */} @@ -257,7 +318,7 @@ export default function CardDetailV2Screen() { style={{ paddingHorizontal: 10, paddingVertical: 3, - backgroundColor: RARITY_COLORS[card.rarity], + backgroundColor: RARITY_COLORS[figure.rarity], }} > - {card.rarity} + {figure.rarity} - #{card.id.split('-').pop()?.toUpperCase()} + #{figure.id.split('-').pop()?.toUpperCase()} - - {/* Hint */} - - Drag to rotate · Double-tap to flip - ); diff --git a/apps/figgos/apps/mobile/data/cards.ts b/apps/figgos/apps/mobile/data/cards.ts deleted file mode 100644 index 2f4707e47..000000000 --- a/apps/figgos/apps/mobile/data/cards.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { ImageSourcePropType } from 'react-native'; -import type { FigureRarity } from '@figgos/shared'; - -export interface CardData { - id: string; - name: string; - subtitle: string; - description: string; - rarity: FigureRarity; - image: ImageSourcePropType; - stats: { attack: number; defense: number; special: number }; -} - -export const CARDS: CardData[] = [ - { - id: 'cole-epic', - name: 'Detective Cole', - subtitle: 'Noir City Homicide Division', - description: - 'A hardboiled detective who has seen it all. Armed with nothing but a trench coat, a sharp mind, and an unhealthy coffee addiction. Solves impossible cases in the rain-soaked streets of Noir City.', - rarity: 'epic', - image: require('../assets/images/cole-epic.png'), - stats: { attack: 42, defense: 68, special: 75 }, - }, - { - id: 'cole-rare', - name: 'Detective Cole', - subtitle: 'Noir City Homicide Division', - description: - 'Fresh off his first big case, Cole is making a name for himself in the precinct. His instincts are sharp, but he still has a lot to learn about the darker side of Noir City.', - rarity: 'rare', - image: require('../assets/images/cole-rare.png'), - stats: { attack: 35, defense: 52, special: 60 }, - }, - { - id: 'cole-legendary', - name: 'Detective Cole', - subtitle: 'Noir City Homicide Division', - description: - 'The legend of Noir City. After decades on the force, Cole has become the detective other detectives tell stories about. His case closure rate is unmatched in the history of the division.', - rarity: 'legendary', - image: require('../assets/images/cole-legendary.png'), - stats: { attack: 78, defense: 85, special: 95 }, - }, - { - id: 'cole-common', - name: 'Detective Cole', - subtitle: 'Noir City Homicide Division', - description: - 'A standard-issue detective doing his best in a tough city. Nothing fancy, but reliable. Shows up every day, drinks too much coffee, and gets the job done.', - rarity: 'common', - image: require('../assets/images/cole-common.png'), - stats: { attack: 22, defense: 30, special: 28 }, - }, - { - id: 'cole-kraft', - name: 'Detective Cole', - subtitle: 'Kraft Edition', - description: - "Limited kraft paper edition. A collector's item with a vintage feel. The same old Cole, but with that handmade, artisanal charm that cardboard enthusiasts crave.", - rarity: 'common', - image: require('../assets/images/cole-kraft.png'), - stats: { attack: 25, defense: 32, special: 30 }, - }, -]; diff --git a/apps/figgos/apps/mobile/services/api.ts b/apps/figgos/apps/mobile/services/api.ts index 248a32ac0..8b93d345f 100644 --- a/apps/figgos/apps/mobile/services/api.ts +++ b/apps/figgos/apps/mobile/services/api.ts @@ -26,10 +26,10 @@ export const api = { health: () => fetchApi('/health'), figures: { - create: (name: string, description: string) => + create: (name: string, description: string, language: string = 'en') => fetchApi<{ figure: FigureResponse }>('/api/v1/figures', { method: 'POST', - body: JSON.stringify({ name, description }), + body: JSON.stringify({ name, description, language }), }), list: () => fetchApi<{ figures: FigureResponse[] }>('/api/v1/figures'), diff --git a/apps/figgos/apps/web/src/lib/api.ts b/apps/figgos/apps/web/src/lib/api.ts new file mode 100644 index 000000000..f5b699e02 --- /dev/null +++ b/apps/figgos/apps/web/src/lib/api.ts @@ -0,0 +1,29 @@ +import type { FigureResponse } from '@figgos/shared'; + +const BACKEND_URL = 'http://localhost:3025'; + +async function fetchApi(path: string, options?: RequestInit): Promise { + const res = await fetch(`${BACKEND_URL}${path}`, { + headers: { 'Content-Type': 'application/json', ...options?.headers }, + ...options, + }); + if (!res.ok) { + const text = await res.text().catch(() => ''); + throw new Error(text || `API error: ${res.status}`); + } + return res.json(); +} + +export const api = { + figures: { + create: (name: string, description: string, language = 'en') => + fetchApi<{ figure: FigureResponse }>('/api/v1/figures', { + method: 'POST', + body: JSON.stringify({ name, description, language }), + }), + list: () => fetchApi<{ figures: FigureResponse[] }>('/api/v1/figures'), + get: (id: string) => fetchApi<{ figure: FigureResponse }>(`/api/v1/figures/${id}`), + delete: (id: string) => + fetchApi<{ success: boolean }>(`/api/v1/figures/${id}`, { method: 'DELETE' }), + }, +}; diff --git a/apps/figgos/apps/web/src/lib/data/cards.ts b/apps/figgos/apps/web/src/lib/data/cards.ts deleted file mode 100644 index ddf0edfc5..000000000 --- a/apps/figgos/apps/web/src/lib/data/cards.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { FigureRarity } from '@figgos/shared'; - -export interface CardData { - id: string; - name: string; - subtitle: string; - description: string; - rarity: FigureRarity; - image: string; - stats: { attack: number; defense: number; special: number }; -} - -export const CARDS: CardData[] = [ - { - id: 'cole-epic', - name: 'Detective Cole', - subtitle: 'Noir City Homicide Division', - description: - 'A hardboiled detective who has seen it all. Armed with nothing but a trench coat, a sharp mind, and an unhealthy coffee addiction. Solves impossible cases in the rain-soaked streets of Noir City.', - rarity: 'epic', - image: '/images/cole-epic.png', - stats: { attack: 42, defense: 68, special: 75 }, - }, - { - id: 'cole-rare', - name: 'Detective Cole', - subtitle: 'Noir City Homicide Division', - description: - 'Fresh off his first big case, Cole is making a name for himself in the precinct. His instincts are sharp, but he still has a lot to learn about the darker side of Noir City.', - rarity: 'rare', - image: '/images/cole-rare.png', - stats: { attack: 35, defense: 52, special: 60 }, - }, - { - id: 'cole-legendary', - name: 'Detective Cole', - subtitle: 'Noir City Homicide Division', - description: - 'The legend of Noir City. After decades on the force, Cole has become the detective other detectives tell stories about. His case closure rate is unmatched in the history of the division.', - rarity: 'legendary', - image: '/images/cole-legendary.png', - stats: { attack: 78, defense: 85, special: 95 }, - }, - { - id: 'cole-common', - name: 'Detective Cole', - subtitle: 'Noir City Homicide Division', - description: - 'A standard-issue detective doing his best in a tough city. Nothing fancy, but reliable. Shows up every day, drinks too much coffee, and gets the job done.', - rarity: 'common', - image: '/images/cole-common.png', - stats: { attack: 22, defense: 30, special: 28 }, - }, - { - id: 'cole-kraft', - name: 'Detective Cole', - subtitle: 'Kraft Edition', - description: - "Limited kraft paper edition. A collector's item with a vintage feel. The same old Cole, but with that handmade, artisanal charm that cardboard enthusiasts crave.", - rarity: 'common', - image: '/images/cole-kraft.png', - stats: { attack: 25, defense: 32, special: 30 }, - }, -]; diff --git a/apps/figgos/apps/web/src/routes/+page.svelte b/apps/figgos/apps/web/src/routes/+page.svelte index bc76f4b45..2e801c91d 100644 --- a/apps/figgos/apps/web/src/routes/+page.svelte +++ b/apps/figgos/apps/web/src/routes/+page.svelte @@ -1,5 +1,6 @@ {#if result} @@ -66,29 +63,100 @@
-
- -
- Image coming soon -
+
+ + {#if result.imageUrl} + {result.name} + {:else} +
+ + {result.status === 'failed' ? 'Generation failed' : 'No image'} + +
+ {/if}

{result.name}

-

- {result.userInput.description} -

+ + {#if profile?.subtitle} +

+ {profile.subtitle} +

+ {/if} + + {#if profile?.backstory} +

+ {profile.backstory} +

+ {/if} + + + {#if profile?.stats} +
+ {#each [{ label: 'ATK', value: profile.stats.attack, color: STAT_COLORS.attack }, { label: 'DEF', value: profile.stats.defense, color: STAT_COLORS.defense }, { label: 'SPL', value: profile.stats.special, color: STAT_COLORS.special }] as stat (stat.label)} +
+ + {stat.label} + +
+
+
+ + {stat.value} + +
+ {/each} +
+ {/if} + + + {#if profile?.specialAttack} +
+

+ ⚡ {profile.specialAttack.name} +

+

+ {profile.specialAttack.description} +

+
+ {/if} + + + {#if profile?.items && profile.items.length > 0} +
+ {#each profile.items as item (item.name)} +
+

+ {item.name} +

+

{item.description}

+
+ {/each} +
+ {/if}
+ + + {#if result.status === 'failed' && result.errorMessage} +

{result.errorMessage}

+ {/if}
@@ -198,10 +271,21 @@ {#if loading} - - + + - Rolling... + Generating... {:else} Generate Figgo diff --git a/apps/figgos/apps/web/src/routes/card/[id]/+page.svelte b/apps/figgos/apps/web/src/routes/card/[id]/+page.svelte index 4d00b35a9..49e4cd49d 100644 --- a/apps/figgos/apps/web/src/routes/card/[id]/+page.svelte +++ b/apps/figgos/apps/web/src/routes/card/[id]/+page.svelte @@ -1,7 +1,7 @@ -{#if card} +{#if loading} +
+ + + + +
+{:else if figure}

- {card.description} + {profile?.backstory || figure.userInput.description}

-
-

- Stats -

- {#each [ - { label: 'ATK', value: card.stats.attack, color: STAT_COLORS.attack }, - { label: 'DEF', value: card.stats.defense, color: STAT_COLORS.defense }, - { label: 'SPL', value: card.stats.special, color: STAT_COLORS.special }, - ] as stat (stat.label)} -
- - {stat.label} - -
+ {#if profile?.stats} +
+

+ Stats +

+ {#each [{ label: 'ATK', value: profile.stats.attack, color: STAT_COLORS.attack }, { label: 'DEF', value: profile.stats.defense, color: STAT_COLORS.defense }, { label: 'SPL', value: profile.stats.special, color: STAT_COLORS.special }] as stat (stat.label)} +
+ + {stat.label} +
+ class="h-3.5 flex-1 overflow-hidden rounded-full border border-border-muted bg-input" + > +
+
+ + {stat.value} +
- - {stat.value} - -
- {/each} -
+ {/each} +
+ {/if} + + + {#if profile?.specialAttack} +
+

+ ⚡ {profile.specialAttack.name} +

+
+ {/if}
- {card.rarity} + {figure.rarity} - #{card.id.split('-').pop()?.toUpperCase()} + #{figure.id.split('-').pop()?.toUpperCase()}
diff --git a/apps/figgos/apps/web/src/routes/collection/+page.svelte b/apps/figgos/apps/web/src/routes/collection/+page.svelte index 75c168c28..7dc393d56 100644 --- a/apps/figgos/apps/web/src/routes/collection/+page.svelte +++ b/apps/figgos/apps/web/src/routes/collection/+page.svelte @@ -1,24 +1,70 @@

Collection

- {CARDS.length} {CARDS.length === 1 ? 'Figgo' : 'Figgos'} + {loading ? '...' : `${figures.length} ${figures.length === 1 ? 'Figgo' : 'Figgos'}`}

-
+ {#if loading} +
+ + + + +
+ {:else if error} +
+

{error}

+
+ {:else if figures.length === 0} +
+

No figures yet

+

Create your first Figgo!

+
+ {:else} +
+ {#each figures as figure (figure.id)} + +
+ {#if figure.imageUrl} + {figure.name} + {:else} +
+ {figure.name} +
+ {/if} +
+
+ {/each} +
+ {/if}
diff --git a/apps/figgos/apps/web/src/routes/showcase/+page.svelte b/apps/figgos/apps/web/src/routes/showcase/+page.svelte index 330af22c1..f71be3345 100644 --- a/apps/figgos/apps/web/src/routes/showcase/+page.svelte +++ b/apps/figgos/apps/web/src/routes/showcase/+page.svelte @@ -1,5 +1,16 @@ +{#if loading} +
+ + + + +
+{:else if figures.length === 0} +
+

No figures yet

+
+{:else} + +
+ {#each figures as figure, i (figure.id)} + + {#if figure.imageUrl} + {figure.name} + {:else} +
+ {figure.name} + No image +
+ {/if} +
+ {/each} +
+{/if} + - - -
- {#each CARDS as card, i (card.id)} - - {card.name} - - {/each} -
diff --git a/apps/figgos/package.json b/apps/figgos/package.json index 023136a49..384bde95b 100644 --- a/apps/figgos/package.json +++ b/apps/figgos/package.json @@ -4,5 +4,9 @@ "private": true, "scripts": { "dev": "turbo run dev" + }, + "dependencies": { + "@imgly/background-removal-node": "^1.4.5", + "sharp": "^0.34.5" } } diff --git a/apps/figgos/packages/shared/src/index.ts b/apps/figgos/packages/shared/src/index.ts index 3a3492314..7b7eafb0d 100644 --- a/apps/figgos/packages/shared/src/index.ts +++ b/apps/figgos/packages/shared/src/index.ts @@ -1,3 +1,5 @@ +// ── Rarity ── + export type FigureRarity = 'common' | 'rare' | 'epic' | 'legendary'; export const RARITY_WEIGHTS: Record = { @@ -7,17 +9,96 @@ export const RARITY_WEIGHTS: Record = { legendary: 3, }; +export const STAT_RANGES: Record = { + common: { min: 10, max: 45 }, + rare: { min: 30, max: 65 }, + epic: { min: 50, max: 85 }, + legendary: { min: 75, max: 100 }, +}; + +// ── Card Styles (internal — used for image generation, not persisted) ── + +export type CardStyle = + | 'common_kraft' + | 'common_white' + | 'common_mint' + | 'common_warm' + | 'rare' + | 'epic' + | 'legendary'; + +const COMMON_STYLES: CardStyle[] = ['common_kraft', 'common_white', 'common_mint', 'common_warm']; + +/** Pick a card style for image generation. Common gets a random variant, others match rarity. */ +export function getCardStyle(rarity: FigureRarity): CardStyle { + if (rarity === 'common') { + return COMMON_STYLES[Math.floor(Math.random() * COMMON_STYLES.length)]; + } + return rarity as CardStyle; +} + +// ── Language ── + +export type FigureLanguage = 'en' | 'de'; + +// ── Generation Status ── + +export type FigureStatus = + | 'pending' + | 'generating_profile' + | 'generating_image' + | 'processing' + | 'completed' + | 'failed'; + +// ── User Input (what the user provides) ── + export interface FigureUserInput { description: string; + faceImageUrl?: string | null; + language: FigureLanguage; } +// ── Generated Profile (what the LLM produces) ── + +export interface FigureItem { + name: string; + description: string; + lore: string; +} + +export interface FigureStats { + attack: number; + defense: number; + special: number; +} + +export interface SpecialAttack { + name: string; + description: string; +} + +export interface GeneratedProfile { + subtitle: string; + backstory: string; + visualDescription: string; + items: FigureItem[]; + stats: FigureStats; + specialAttack: SpecialAttack; +} + +// ── API Response ── + export interface FigureResponse { id: string; userId: string; name: string; userInput: FigureUserInput; + generatedProfile: GeneratedProfile | null; imageUrl: string | null; rarity: FigureRarity; + language: FigureLanguage; + status: FigureStatus; isPublic: boolean; isArchived: boolean; createdAt: string; diff --git a/packages/shared-storage/src/factory.ts b/packages/shared-storage/src/factory.ts index c8a529587..e27eda434 100644 --- a/packages/shared-storage/src/factory.ts +++ b/packages/shared-storage/src/factory.ts @@ -147,3 +147,13 @@ export function createInventoryStorage(publicUrl?: string): StorageClient { publicUrl: publicUrl ?? process.env.INVENTORY_S3_PUBLIC_URL, }); } + +/** + * Create a storage client for the Figgos project + */ +export function createFiggosStorage(publicUrl?: string): StorageClient { + return createStorageClient({ + name: BUCKETS.FIGGOS, + publicUrl: publicUrl ?? process.env.FIGGOS_STORAGE_PUBLIC_URL, + }); +} diff --git a/packages/shared-storage/src/index.ts b/packages/shared-storage/src/index.ts index f3ef3c5be..9c15a667e 100644 --- a/packages/shared-storage/src/index.ts +++ b/packages/shared-storage/src/index.ts @@ -15,6 +15,7 @@ export { createStorageStorage, createMailStorage, createInventoryStorage, + createFiggosStorage, } from './factory'; // Utilities diff --git a/packages/shared-storage/src/types.ts b/packages/shared-storage/src/types.ts index 5fa3e43c2..c62179441 100644 --- a/packages/shared-storage/src/types.ts +++ b/packages/shared-storage/src/types.ts @@ -86,6 +86,7 @@ export const BUCKETS = { STORAGE: 'storage-storage', MAIL: 'mail-storage', INVENTORY: 'inventory-storage', + FIGGOS: 'figgos-storage', } as const; export type BucketName = (typeof BUCKETS)[keyof typeof BUCKETS]; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a31728ade..a0da5d2b6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1468,13 +1468,26 @@ importers: specifier: ^3.4.17 version: 3.4.18(tsx@4.20.6)(yaml@2.8.1) - apps/figgos: {} + apps/figgos: + dependencies: + '@imgly/background-removal-node': + specifier: ^1.4.5 + version: 1.4.5 + sharp: + specifier: ^0.34.5 + version: 0.34.5 apps/figgos/apps/backend: dependencies: '@figgos/shared': specifier: workspace:* version: link:../../packages/shared + '@google/genai': + specifier: ^1.14.0 + version: 1.30.0 + '@huggingface/transformers': + specifier: ^3.8.1 + version: 3.8.1 '@manacore/shared-drizzle-config': specifier: workspace:* version: link:../../../../packages/shared-drizzle-config @@ -1490,6 +1503,9 @@ importers: '@manacore/shared-nestjs-setup': specifier: workspace:* version: link:../../../../packages/shared-nestjs-setup + '@manacore/shared-storage': + specifier: workspace:* + version: link:../../../../packages/shared-storage '@nestjs/common': specifier: ^10.4.15 version: 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -1529,10 +1545,13 @@ importers: rxjs: specifier: ^7.8.1 version: 7.8.2 + sharp: + specifier: ^0.34.5 + version: 0.34.5 devDependencies: '@nestjs/cli': specifier: ^10.4.9 - version: 10.4.9(esbuild@0.27.0) + version: 10.4.9(esbuild@0.19.12) '@nestjs/schematics': specifier: ^10.2.3 version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3) @@ -1547,7 +1566,7 @@ importers: version: 0.5.21 ts-loader: specifier: ^9.5.1 - version: 9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.27.0)) + version: 9.5.4(typescript@5.9.3)(webpack@5.97.1(esbuild@0.19.12)) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) @@ -9876,6 +9895,13 @@ packages: '@hapi/topo@6.0.2': resolution: {integrity: sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==} + '@huggingface/jinja@0.5.5': + resolution: {integrity: sha512-xRlzazC+QZwr6z4ixEqYHo9fgwhTZ3xNSdljlKfUFGZSdlvt166DljRELFUfFytlYOYvo3vTisA/AFOuOAzFQQ==} + engines: {node: '>=18'} + + '@huggingface/transformers@3.8.1': + resolution: {integrity: sha512-tsTk4zVjImqdqjS8/AOZg2yNLd1z9S5v+7oUPpXaasDRwEDhB+xnglK1k5cad26lL5/ZIaeREgWWy0bs9y9pPA==} + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -10164,6 +10190,9 @@ packages: cpu: [x64] os: [win32] + '@imgly/background-removal-node@1.4.5': + resolution: {integrity: sha512-/s9K88qhKy1jPhrSkBxurUqCVqJ8KHWCc+5yWdppdC4fuSrGC8mK8WQtmULs2ASEr8naY1qpvZu0EL5jr2Hqtg==} + '@inquirer/ansi@1.0.2': resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} engines: {node: '>=18'} @@ -13069,6 +13098,9 @@ packages: '@types/jsonwebtoken@9.0.10': resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + '@types/lodash@4.14.202': + resolution: {integrity: sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==} + '@types/luxon@3.4.2': resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==} @@ -13096,6 +13128,9 @@ packages: '@types/multer@2.0.0': resolution: {integrity: sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==} + '@types/ndarray@1.0.14': + resolution: {integrity: sha512-oANmFZMnFQvb219SSBIhI1Ih/r4CvHDOzkWyJS/XRqkMrGH5/kaPSA1hQhdIBzouaE+5KpE/f5ylI9cujmckQg==} + '@types/nlcst@2.0.3': resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==} @@ -13114,6 +13149,9 @@ packages: '@types/node@20.19.25': resolution: {integrity: sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==} + '@types/node@20.3.3': + resolution: {integrity: sha512-wheIYdr4NYML61AjC8MKj/2jrR/kDQri/CIpVoZwldwhnIrD/j9jIU5bJ8yBKuB2VhpFV7Ab6G2XkBjv9r9Zzw==} + '@types/node@22.19.1': resolution: {integrity: sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==} @@ -14222,6 +14260,14 @@ packages: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} + b4a@1.7.3: + resolution: {integrity: sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==} + peerDependencies: + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true + babel-core@7.0.0-bridge.0: resolution: {integrity: sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==} peerDependencies: @@ -14355,6 +14401,44 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + bare-events@2.8.2: + resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==} + peerDependencies: + bare-abort-controller: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + + bare-fs@4.5.3: + resolution: {integrity: sha512-9+kwVx8QYvt3hPWnmb19tPnh38c6Nihz8Lx3t0g9+4GoIf3/fTgYwM4Z6NxgI+B9elLQA7mLE9PpqcWtOMRDiQ==} + engines: {bare: '>=1.16.0'} + peerDependencies: + bare-buffer: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + + bare-os@3.6.2: + resolution: {integrity: sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==} + engines: {bare: '>=1.14.0'} + + bare-path@3.0.0: + resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} + + bare-stream@2.7.0: + resolution: {integrity: sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==} + peerDependencies: + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + bare-events: + optional: true + + bare-url@2.3.2: + resolution: {integrity: sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==} + base-64@1.0.0: resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} @@ -14476,6 +14560,10 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + boolean@3.2.0: + resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + bowser@2.13.1: resolution: {integrity: sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==} @@ -14716,6 +14804,9 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + chownr@2.0.0: resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} engines: {node: '>=10'} @@ -15375,6 +15466,10 @@ packages: resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} engines: {node: '>=0.10'} + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + dedent@1.7.0: resolution: {integrity: sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==} peerDependencies: @@ -15469,6 +15564,9 @@ packages: detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + detect-node@2.1.0: + resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + deterministic-object-hash@2.0.2: resolution: {integrity: sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ==} engines: {node: '>=18'} @@ -15935,6 +16033,9 @@ packages: resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} engines: {node: '>=0.10'} + es6-error@4.1.1: + resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} + es6-iterator@2.0.3: resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} @@ -16427,6 +16528,9 @@ packages: eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + events-universal@1.0.1: + resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -16454,6 +16558,10 @@ packages: resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} engines: {node: '>= 0.8.0'} + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + expect-type@1.2.2: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} @@ -17178,6 +17286,9 @@ packages: fast-diff@1.3.0: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -17321,6 +17432,9 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} + flatbuffers@25.9.23: + resolution: {integrity: sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==} + flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} @@ -17427,6 +17541,9 @@ packages: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fs-extra@10.1.0: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} engines: {node: '>=12'} @@ -17581,6 +17698,9 @@ packages: getpass@0.1.7: resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + github-slugger@2.0.0: resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} @@ -17634,6 +17754,10 @@ packages: engines: {node: '>=16 || 14 >=14.17'} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + global-agent@3.0.0: + resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} + engines: {node: '>=10.0'} + global-dirs@0.1.1: resolution: {integrity: sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==} engines: {node: '>=4'} @@ -17714,6 +17838,9 @@ packages: resolution: {integrity: sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==} engines: {node: '>=18'} + guid-typescript@1.0.9: + resolution: {integrity: sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==} + h3@1.15.4: resolution: {integrity: sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==} @@ -18093,6 +18220,9 @@ packages: resolution: {integrity: sha512-tAAg/72/VxOUW7RQSX1pIxJVucYKcjFjfvj60L57jrZpYCHC3XN0WCQ3sNYL4Gmvv+7GPvTAjc+KSdeNuE8oWQ==} engines: {node: '>=12.22.0'} + iota-array@1.0.0: + resolution: {integrity: sha512-pZ2xT+LOHckCatGQ3DcG/a+QuEqvoxqkiL7tvE8nn3uuu+f6i1TtpB5/FtWFbxUuVr5PZCx8KskuGatbJDXOWA==} + ip-address@10.1.0: resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} engines: {node: '>= 12'} @@ -19291,6 +19421,10 @@ packages: marky@1.3.0: resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==} + matcher@3.0.0: + resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} + engines: {node: '>=10'} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -19817,6 +19951,10 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} @@ -19911,6 +20049,9 @@ packages: resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} engines: {node: '>= 18'} + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true @@ -20003,6 +20144,9 @@ packages: resolution: {integrity: sha512-yJBmDJr18xy47dbNVlHcgdPrulSn1nhSE6Ns9vTG+Nx9VPT6iV1MD6aQFp/t52zpf82FhLLTXAXr30NuCnxvwA==} engines: {node: ^20.0.0 || >=22.0.0} + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + napi-postinstall@0.3.4: resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -20017,6 +20161,9 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + ndarray@1.0.19: + resolution: {integrity: sha512-B4JHA4vdyZU30ELBw3g7/p9bZupyew5a7tX1Y/gGeF2hafrPaQZhgrGQfsvgfYbgdFZjYwuEcnaobeM/WMW+HQ==} + negotiator@0.6.3: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} @@ -20057,12 +20204,19 @@ packages: nlcst-to-string@4.0.0: resolution: {integrity: sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==} + node-abi@3.87.0: + resolution: {integrity: sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==} + engines: {node: '>=10'} + node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} node-addon-api@5.1.0: resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==} + node-addon-api@6.1.0: + resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==} + node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} @@ -20288,6 +20442,26 @@ packages: oniguruma-to-es@4.3.4: resolution: {integrity: sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==} + onnxruntime-common@1.17.3: + resolution: {integrity: sha512-IkbaDelNVX8cBfHFgsNADRIq2TlXMFWW+nG55mwWvQT4i0NZb32Jf35Pf6h9yjrnK78RjcnlNYaI37w394ovMw==} + + onnxruntime-common@1.21.0: + resolution: {integrity: sha512-Q632iLLrtCAVOTO65dh2+mNbQir/QNTVBG3h/QdZBpns7mZ0RYbLRBgGABPbpU9351AgYy7SJf1WaeVwMrBFPQ==} + + onnxruntime-common@1.22.0-dev.20250409-89f8206ba4: + resolution: {integrity: sha512-vDJMkfCfb0b1A836rgHj+ORuZf4B4+cc2bASQtpeoJLueuFc5DuYwjIZUBrSvx/fO5IrLjLz+oTrB3pcGlhovQ==} + + onnxruntime-node@1.17.3: + resolution: {integrity: sha512-NtbN1pfApTSEjVq46LrJ396aPP2Gjhy+oYZi5Bu1leDXAEvVap/BQ8CZELiLs7z0UnXy3xjJW23HiB4P3//FIw==} + os: [win32, darwin, linux] + + onnxruntime-node@1.21.0: + resolution: {integrity: sha512-NeaCX6WW2L8cRCSqy3bInlo5ojjQqu2fD3D+9W5qb5irwxhEyWKXeH2vZ8W9r6VxaMPUan+4/7NDwZMtouZxEw==} + os: [win32, darwin, linux] + + onnxruntime-web@1.22.0-dev.20250409-89f8206ba4: + resolution: {integrity: sha512-0uS76OPgH0hWCPrFKlL8kYVV7ckM7t/36HfbgoFw6Nd0CZVVbQC4PkrR8mBX8LtNUFZO25IQBqV2Hx2ho3FlbQ==} + open@7.4.2: resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} engines: {node: '>=8'} @@ -20585,6 +20759,9 @@ packages: resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} engines: {node: '>=8'} + platform@1.3.6: + resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==} + playwright-core@1.57.0: resolution: {integrity: sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==} engines: {node: '>=18'} @@ -20710,6 +20887,11 @@ packages: resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} engines: {node: '>=12'} + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + hasBin: true + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -21708,6 +21890,10 @@ packages: engines: {node: 20 || >=22} hasBin: true + roarr@2.15.4: + resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} + engines: {node: '>=8.0'} + robots-parser@3.0.1: resolution: {integrity: sha512-s+pyvQeIKIZ0dx5iJiQk1tPLJAWln39+MI5jtM8wnyws+G5azk+dMnMX0qfbqNetKKNgcWWOdi0sfm+FbQbgdQ==} engines: {node: '>=10.0.0'} @@ -21841,6 +22027,9 @@ packages: resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==} engines: {node: '>=10'} + semver-compare@1.0.0: + resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} + semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true @@ -21880,6 +22069,10 @@ packages: resolution: {integrity: sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==} engines: {node: '>=0.10.0'} + serialize-error@7.0.1: + resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} + engines: {node: '>=10'} + serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} @@ -21932,6 +22125,10 @@ packages: shallowequal@1.1.0: resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} + sharp@0.32.6: + resolution: {integrity: sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==} + engines: {node: '>=14.15.0'} + sharp@0.33.5: resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -21995,6 +22192,12 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + simple-plist@1.3.1: resolution: {integrity: sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw==} @@ -22104,6 +22307,9 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + sshpk@1.18.0: resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==} engines: {node: '>=0.10.0'} @@ -22187,6 +22393,9 @@ packages: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} + streamx@2.23.0: + resolution: {integrity: sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==} + strict-uri-encode@2.0.0: resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} engines: {node: '>=4'} @@ -22456,6 +22665,19 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} + tar-fs@2.1.4: + resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} + + tar-fs@3.1.1: + resolution: {integrity: sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tar-stream@3.1.7: + resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} @@ -22514,6 +22736,9 @@ packages: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} + text-decoder@1.2.3: + resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} + text-hex@1.0.0: resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} @@ -22846,6 +23071,10 @@ packages: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} + type-fest@0.13.1: + resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} + engines: {node: '>=10'} + type-fest@0.16.0: resolution: {integrity: sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==} engines: {node: '>=10'} @@ -24157,6 +24386,9 @@ packages: typescript: ^4.9.4 || ^5.0.2 zod: ^3 + zod@3.21.4: + resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==} + zod@3.22.3: resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==} @@ -28921,6 +29153,15 @@ snapshots: dependencies: '@hapi/hoek': 11.0.7 + '@huggingface/jinja@0.5.5': {} + + '@huggingface/transformers@3.8.1': + dependencies: + '@huggingface/jinja': 0.5.5 + onnxruntime-node: 1.21.0 + onnxruntime-web: 1.22.0-dev.20250409-89f8206ba4 + sharp: 0.34.5 + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -28985,8 +29226,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@img/colour@1.0.0': - optional: true + '@img/colour@1.0.0': {} '@img/sharp-darwin-arm64@0.33.5': optionalDependencies: @@ -29157,6 +29397,21 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true + '@imgly/background-removal-node@1.4.5': + dependencies: + '@types/lodash': 4.14.202 + '@types/ndarray': 1.0.14 + '@types/node': 20.3.3 + lodash: 4.17.21 + ndarray: 1.0.19 + onnxruntime-node: 1.17.3 + sharp: 0.32.6 + zod: 3.21.4 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + '@inquirer/ansi@1.0.2': {} '@inquirer/checkbox@4.3.2(@types/node@22.19.1)': @@ -30065,6 +30320,32 @@ snapshots: - uglify-js - webpack-cli + '@nestjs/cli@10.4.9(esbuild@0.19.12)': + dependencies: + '@angular-devkit/core': 17.3.11(chokidar@3.6.0) + '@angular-devkit/schematics': 17.3.11(chokidar@3.6.0) + '@angular-devkit/schematics-cli': 17.3.11(chokidar@3.6.0) + '@nestjs/schematics': 10.2.3(chokidar@3.6.0)(typescript@5.7.2) + chalk: 4.1.2 + chokidar: 3.6.0 + cli-table3: 0.6.5 + commander: 4.1.1 + fork-ts-checker-webpack-plugin: 9.0.2(typescript@5.7.2)(webpack@5.97.1(esbuild@0.19.12)) + glob: 10.4.5 + inquirer: 8.2.6 + node-emoji: 1.11.0 + ora: 5.4.1 + tree-kill: 1.2.2 + tsconfig-paths: 4.2.0 + tsconfig-paths-webpack-plugin: 4.2.0 + typescript: 5.7.2 + webpack: 5.97.1(esbuild@0.19.12) + webpack-node-externals: 3.0.0 + transitivePeerDependencies: + - esbuild + - uglify-js + - webpack-cli + '@nestjs/cli@10.4.9(esbuild@0.27.0)': dependencies: '@angular-devkit/core': 17.3.11(chokidar@3.6.0) @@ -30709,38 +30990,28 @@ snapshots: '@proload/core': 0.3.3 tsm: 2.3.0 - '@protobufjs/aspromise@1.1.2': - optional: true + '@protobufjs/aspromise@1.1.2': {} - '@protobufjs/base64@1.1.2': - optional: true + '@protobufjs/base64@1.1.2': {} - '@protobufjs/codegen@2.0.4': - optional: true + '@protobufjs/codegen@2.0.4': {} - '@protobufjs/eventemitter@1.1.0': - optional: true + '@protobufjs/eventemitter@1.1.0': {} '@protobufjs/fetch@1.1.0': dependencies: '@protobufjs/aspromise': 1.1.2 '@protobufjs/inquire': 1.1.0 - optional: true - '@protobufjs/float@1.0.2': - optional: true + '@protobufjs/float@1.0.2': {} - '@protobufjs/inquire@1.1.0': - optional: true + '@protobufjs/inquire@1.1.0': {} - '@protobufjs/path@1.1.2': - optional: true + '@protobufjs/path@1.1.2': {} - '@protobufjs/pool@1.1.0': - optional: true + '@protobufjs/pool@1.1.0': {} - '@protobufjs/utf8@1.1.0': - optional: true + '@protobufjs/utf8@1.1.0': {} '@radix-ui/primitive@1.1.3': {} @@ -34516,6 +34787,8 @@ snapshots: '@types/ms': 2.1.0 '@types/node': 22.19.1 + '@types/lodash@4.14.202': {} + '@types/luxon@3.4.2': {} '@types/mdast@4.0.4': @@ -34540,6 +34813,8 @@ snapshots: dependencies: '@types/express': 5.0.5 + '@types/ndarray@1.0.14': {} + '@types/nlcst@2.0.3': dependencies: '@types/unist': 3.0.3 @@ -34561,6 +34836,8 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/node@20.3.3': {} + '@types/node@22.19.1': dependencies: undici-types: 6.21.0 @@ -36495,6 +36772,8 @@ snapshots: axobject-query@4.1.0: {} + b4a@1.7.3: {} + babel-core@7.0.0-bridge.0(@babel/core@7.28.5): dependencies: '@babel/core': 7.28.5 @@ -36795,6 +37074,43 @@ snapshots: balanced-match@1.0.2: {} + bare-events@2.8.2: {} + + bare-fs@4.5.3: + dependencies: + bare-events: 2.8.2 + bare-path: 3.0.0 + bare-stream: 2.7.0(bare-events@2.8.2) + bare-url: 2.3.2 + fast-fifo: 1.3.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + optional: true + + bare-os@3.6.2: + optional: true + + bare-path@3.0.0: + dependencies: + bare-os: 3.6.2 + optional: true + + bare-stream@2.7.0(bare-events@2.8.2): + dependencies: + streamx: 2.23.0 + optionalDependencies: + bare-events: 2.8.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + optional: true + + bare-url@2.3.2: + dependencies: + bare-path: 3.0.0 + optional: true + base-64@1.0.0: {} base-x@5.0.1: {} @@ -36926,6 +37242,8 @@ snapshots: boolbase@1.0.0: {} + boolean@3.2.0: {} + bowser@2.13.1: {} boxen@5.1.2: @@ -37225,6 +37543,8 @@ snapshots: dependencies: readdirp: 4.1.2 + chownr@1.1.4: {} + chownr@2.0.0: {} chownr@3.0.0: {} @@ -37929,6 +38249,10 @@ snapshots: decode-uri-component@0.2.2: {} + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + dedent@1.7.0: {} deep-eql@5.0.2: {} @@ -38001,6 +38325,8 @@ snapshots: detect-node-es@1.1.0: {} + detect-node@2.1.0: {} + deterministic-object-hash@2.0.2: dependencies: base-64: 1.0.0 @@ -38378,6 +38704,8 @@ snapshots: esniff: 2.0.1 next-tick: 1.1.0 + es6-error@4.1.1: {} + es6-iterator@2.0.3: dependencies: d: 1.0.2 @@ -39515,6 +39843,12 @@ snapshots: eventemitter3@5.0.1: {} + events-universal@1.0.1: + dependencies: + bare-events: 2.8.2 + transitivePeerDependencies: + - bare-abort-controller + events@3.3.0: {} exec-async@2.2.0: {} @@ -39547,6 +39881,8 @@ snapshots: exit@0.1.2: {} + expand-template@2.0.3: {} + expect-type@1.2.2: {} expect@29.7.0: @@ -41573,6 +41909,8 @@ snapshots: fast-diff@1.3.0: {} + fast-fifo@1.3.2: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -41766,6 +42104,8 @@ snapshots: flatted: 3.3.3 keyv: 4.5.4 + flatbuffers@25.9.23: {} + flatted@3.3.3: {} flattie@1.1.1: {} @@ -41808,6 +42148,23 @@ snapshots: forever-agent@0.6.1: {} + fork-ts-checker-webpack-plugin@9.0.2(typescript@5.7.2)(webpack@5.97.1(esbuild@0.19.12)): + dependencies: + '@babel/code-frame': 7.27.1 + chalk: 4.1.2 + chokidar: 3.6.0 + cosmiconfig: 8.3.6(typescript@5.7.2) + deepmerge: 4.3.1 + fs-extra: 10.1.0 + memfs: 3.5.3 + minimatch: 3.1.2 + node-abort-controller: 3.1.1 + schema-utils: 3.3.0 + semver: 7.7.3 + tapable: 2.3.0 + typescript: 5.7.2 + webpack: 5.97.1(esbuild@0.19.12) + fork-ts-checker-webpack-plugin@9.0.2(typescript@5.7.2)(webpack@5.97.1(esbuild@0.27.0)): dependencies: '@babel/code-frame': 7.27.1 @@ -41925,6 +42282,8 @@ snapshots: fresh@2.0.0: {} + fs-constants@1.0.0: {} + fs-extra@10.1.0: dependencies: graceful-fs: 4.2.11 @@ -42117,6 +42476,8 @@ snapshots: dependencies: assert-plus: 1.0.0 + github-from-package@0.0.0: {} + github-slugger@2.0.0: {} glob-parent@5.1.2: @@ -42195,6 +42556,15 @@ snapshots: minipass: 4.2.8 path-scurry: 1.11.1 + global-agent@3.0.0: + dependencies: + boolean: 3.2.0 + es6-error: 4.1.1 + matcher: 3.0.0 + roarr: 2.15.4 + semver: 7.7.3 + serialize-error: 7.0.1 + global-dirs@0.1.1: dependencies: ini: 1.3.8 @@ -42235,7 +42605,7 @@ snapshots: gcp-metadata: 8.1.2 google-logging-utils: 1.1.3 gtoken: 8.0.0 - jws: 4.0.0 + jws: 4.0.1 transitivePeerDependencies: - supports-color @@ -42291,7 +42661,7 @@ snapshots: gtoken@7.1.0(encoding@0.1.13): dependencies: gaxios: 6.7.1(encoding@0.1.13) - jws: 4.0.0 + jws: 4.0.1 transitivePeerDependencies: - encoding - supports-color @@ -42299,10 +42669,12 @@ snapshots: gtoken@8.0.0: dependencies: gaxios: 7.1.3 - jws: 4.0.0 + jws: 4.0.1 transitivePeerDependencies: - supports-color + guid-typescript@1.0.9: {} + h3@1.15.4: dependencies: cookie-es: 1.2.2 @@ -42892,6 +43264,8 @@ snapshots: transitivePeerDependencies: - supports-color + iota-array@1.0.0: {} + ip-address@10.1.0: {} ip-regex@2.1.0: {} @@ -44810,8 +45184,7 @@ snapshots: loglevel@1.9.2: {} - long@5.3.2: - optional: true + long@5.3.2: {} longest-streak@3.1.0: {} @@ -44928,6 +45301,10 @@ snapshots: marky@1.3.0: {} + matcher@3.0.0: + dependencies: + escape-string-regexp: 4.0.0 + math-intrinsics@1.1.0: {} matrix-bot-sdk@0.7.1: @@ -46217,6 +46594,8 @@ snapshots: mimic-function@5.0.1: {} + mimic-response@3.1.0: {} + min-indent@1.0.1: optional: true @@ -46332,6 +46711,8 @@ snapshots: dependencies: minipass: 7.1.2 + mkdirp-classic@0.5.3: {} + mkdirp@0.5.6: dependencies: minimist: 1.2.8 @@ -46429,6 +46810,8 @@ snapshots: nanostores@1.1.0: {} + napi-build-utils@2.0.0: {} + napi-postinstall@0.3.4: {} nativewind@4.2.1(react-native-reanimated@3.16.2(@babel/core@7.28.5)(react-native@0.76.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.12.0(react-native@0.76.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1)): @@ -46531,6 +46914,11 @@ snapshots: natural-compare@1.4.0: {} + ndarray@1.0.19: + dependencies: + iota-array: 1.0.0 + is-buffer: 1.1.6 + negotiator@0.6.3: {} negotiator@0.6.4: {} @@ -46558,10 +46946,16 @@ snapshots: dependencies: '@types/nlcst': 2.0.3 + node-abi@3.87.0: + dependencies: + semver: 7.7.3 + node-abort-controller@3.1.1: {} node-addon-api@5.1.0: {} + node-addon-api@6.1.0: {} + node-addon-api@7.1.1: {} node-dir@0.1.17: @@ -46797,6 +47191,32 @@ snapshots: regex: 6.0.1 regex-recursion: 6.0.2 + onnxruntime-common@1.17.3: {} + + onnxruntime-common@1.21.0: {} + + onnxruntime-common@1.22.0-dev.20250409-89f8206ba4: {} + + onnxruntime-node@1.17.3: + dependencies: + onnxruntime-common: 1.17.3 + tar: 7.5.2 + + onnxruntime-node@1.21.0: + dependencies: + global-agent: 3.0.0 + onnxruntime-common: 1.21.0 + tar: 7.5.2 + + onnxruntime-web@1.22.0-dev.20250409-89f8206ba4: + dependencies: + flatbuffers: 25.9.23 + guid-typescript: 1.0.9 + long: 5.3.2 + onnxruntime-common: 1.22.0-dev.20250409-89f8206ba4 + platform: 1.3.6 + protobufjs: 7.5.4 + open@7.4.2: dependencies: is-docker: 2.2.1 @@ -47108,6 +47528,8 @@ snapshots: dependencies: find-up: 3.0.0 + platform@1.3.6: {} + playwright-core@1.57.0: {} playwright@1.57.0: @@ -47234,6 +47656,21 @@ snapshots: postgres@3.4.7: {} + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.87.0 + pump: 3.0.3 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.4 + tunnel-agent: 0.6.0 + prelude-ls@1.2.1: {} prettier-linter-helpers@1.0.0: @@ -47363,7 +47800,6 @@ snapshots: '@protobufjs/utf8': 1.1.0 '@types/node': 22.19.1 long: 5.3.2 - optional: true proxy-addr@2.0.7: dependencies: @@ -49308,6 +49744,15 @@ snapshots: glob: 13.0.0 package-json-from-dist: 1.0.1 + roarr@2.15.4: + dependencies: + boolean: 3.2.0 + detect-node: 2.1.0 + globalthis: 1.0.4 + json-stringify-safe: 5.0.1 + semver-compare: 1.0.0 + sprintf-js: 1.1.3 + robots-parser@3.0.1: {} robust-predicates@3.0.2: {} @@ -49475,6 +49920,8 @@ snapshots: '@types/node-forge': 1.3.14 node-forge: 1.3.2 + semver-compare@1.0.0: {} + semver@5.7.2: {} semver@6.3.1: {} @@ -49539,6 +49986,10 @@ snapshots: serialize-error@2.1.0: {} + serialize-error@7.0.1: + dependencies: + type-fest: 0.13.1 + serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 @@ -49603,6 +50054,21 @@ snapshots: shallowequal@1.1.0: {} + sharp@0.32.6: + dependencies: + color: 4.2.3 + detect-libc: 2.1.2 + node-addon-api: 6.1.0 + prebuild-install: 7.1.3 + semver: 7.7.3 + simple-get: 4.0.1 + tar-fs: 3.1.1 + tunnel-agent: 0.6.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + sharp@0.33.5: dependencies: color: 4.2.3 @@ -49659,7 +50125,6 @@ snapshots: '@img/sharp-win32-arm64': 0.34.5 '@img/sharp-win32-ia32': 0.34.5 '@img/sharp-win32-x64': 0.34.5 - optional: true shebang-command@1.2.0: dependencies: @@ -49742,6 +50207,14 @@ snapshots: signal-exit@4.1.0: {} + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + simple-plist@1.3.1: dependencies: bplist-creator: 0.1.0 @@ -49862,6 +50335,8 @@ snapshots: sprintf-js@1.0.3: {} + sprintf-js@1.1.3: {} + sshpk@1.18.0: dependencies: asn1: 0.2.6 @@ -49933,6 +50408,15 @@ snapshots: streamsearch@1.1.0: {} + streamx@2.23.0: + dependencies: + events-universal: 1.0.1 + fast-fifo: 1.3.2 + text-decoder: 1.2.3 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + strict-uri-encode@2.0.0: {} string-argv@0.3.2: {} @@ -50323,6 +50807,42 @@ snapshots: tapable@2.3.0: {} + tar-fs@2.1.4: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.3 + tar-stream: 2.2.0 + + tar-fs@3.1.1: + dependencies: + pump: 3.0.3 + tar-stream: 3.1.7 + optionalDependencies: + bare-fs: 4.5.3 + bare-path: 3.0.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tar-stream@3.1.7: + dependencies: + b4a: 1.7.3 + fast-fifo: 1.3.2 + streamx: 2.23.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + tar@6.2.1: dependencies: chownr: 2.0.0 @@ -50370,6 +50890,17 @@ snapshots: ansi-escapes: 4.3.2 supports-hyperlinks: 2.3.0 + terser-webpack-plugin@5.3.14(esbuild@0.19.12)(webpack@5.97.1(esbuild@0.19.12)): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + serialize-javascript: 6.0.2 + terser: 5.44.1 + webpack: 5.97.1(esbuild@0.19.12) + optionalDependencies: + esbuild: 0.19.12 + terser-webpack-plugin@5.3.14(esbuild@0.27.0)(webpack@5.100.2(esbuild@0.27.0)): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -50423,6 +50954,12 @@ snapshots: glob: 7.2.3 minimatch: 3.1.2 + text-decoder@1.2.3: + dependencies: + b4a: 1.7.3 + transitivePeerDependencies: + - react-native-b4a + text-hex@1.0.0: {} text-table@0.2.0: {} @@ -50639,6 +51176,16 @@ snapshots: typescript: 5.9.3 webpack: 5.100.2 + ts-loader@9.5.4(typescript@5.9.3)(webpack@5.97.1(esbuild@0.19.12)): + dependencies: + chalk: 4.1.2 + enhanced-resolve: 5.18.3 + micromatch: 4.0.8 + semver: 7.7.3 + source-map: 0.7.6 + typescript: 5.9.3 + webpack: 5.97.1(esbuild@0.19.12) + ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -50834,6 +51381,8 @@ snapshots: type-detect@4.0.8: {} + type-fest@0.13.1: {} + type-fest@0.16.0: {} type-fest@0.20.2: {} @@ -51969,6 +52518,36 @@ snapshots: - esbuild - uglify-js + webpack@5.97.1(esbuild@0.19.12): + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.8 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.15.0 + browserslist: 4.28.0 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.18.3 + es-module-lexer: 1.7.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.1 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 3.3.0 + tapable: 2.3.0 + terser-webpack-plugin: 5.3.14(esbuild@0.19.12)(webpack@5.97.1(esbuild@0.19.12)) + watchpack: 2.4.4 + webpack-sources: 3.3.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + webpack@5.97.1(esbuild@0.27.0): dependencies: '@types/eslint-scope': 3.7.7 @@ -52458,6 +53037,8 @@ snapshots: typescript: 5.9.3 zod: 3.25.76 + zod@3.21.4: {} + zod@3.22.3: {} zod@3.25.76: {} diff --git a/scripts/generate-env.mjs b/scripts/generate-env.mjs index 3abb5e398..440883630 100644 --- a/scripts/generate-env.mjs +++ b/scripts/generate-env.mjs @@ -616,7 +616,9 @@ const APP_CONFIGS = [ S3_ACCESS_KEY: (env) => env.S3_ACCESS_KEY || 'minioadmin', S3_SECRET_KEY: (env) => env.S3_SECRET_KEY || 'minioadmin', S3_BUCKET: () => 'figgos-storage', - CORS_ORIGINS: () => 'http://localhost:5181,http://localhost:8081', + CORS_ORIGINS: () => 'http://localhost:5196,http://localhost:8081', + GEMINI_API_KEY: (env) => env.GEMINI_API_KEY, + FIGGOS_STORAGE_PUBLIC_URL: () => 'http://localhost:9000/figgos-storage', }, },