import type { PicletInstance, DiscoveryStatus } from '$lib/db/schema'; import type { GradioClient } from '$lib/types'; export interface CanonicalSearchResult { status: DiscoveryStatus; piclet: PicletInstance | null; canonicalId?: string; matchedAttributes?: string[]; suggestedVariation?: string[]; message?: string; } export interface ObjectExtractionResult { primaryObject: string; attributes: string[]; visualDetails: string; } export class CanonicalService { /** * Extract object and attributes from image caption * Focuses on identifying the primary object and its variations */ static extractObjectFromCaption(caption: string): ObjectExtractionResult { // Clean and normalize caption const normalized = caption.toLowerCase().trim(); // Common patterns to identify the main object // Priority: noun after article (a/an/the), first noun, or key noun phrases const objectPatterns = [ /(?:a|an|the)\s+(\w+(?:\s+\w+)?)\s+(?:is|sits|stands|lies|rests)/i, /(?:a|an|the)\s+([\w\s]+?)(?:\s+with|\s+that|\s+in|\s+on|,|\.|$)/i, /^([\w\s]+?)(?:\s+with|\s+that|\s+in|\s+on|,|\.|$)/i, ]; let primaryObject = ''; for (const pattern of objectPatterns) { const match = caption.match(pattern); if (match && match[1]) { // Clean up the captured object primaryObject = match[1] .trim() .replace(/\s+/g, ' ') .split(' ') .filter(word => !['very', 'quite', 'rather', 'extremely'].includes(word)) .pop() || ''; // Get the last word as the core object if (primaryObject) break; } } // Fallback: take first noun-like word if (!primaryObject) { const words = normalized.split(/\s+/); primaryObject = words.find(w => w.length > 3 && !['with', 'that', 'this', 'from'].includes(w)) || 'object'; } // Extract descriptive attributes (limit to 2-3 most relevant) const attributeWords = [ // Materials 'wooden', 'metal', 'plastic', 'glass', 'leather', 'velvet', 'silk', 'cotton', 'stone', 'marble', 'gold', 'silver', 'bronze', 'copper', 'steel', 'iron', 'aluminum', 'ceramic', 'porcelain', // Styles 'modern', 'vintage', 'antique', 'rustic', 'minimalist', 'ornate', 'gothic', 'art deco', 'retro', // Colors (basic only) 'red', 'blue', 'green', 'yellow', 'purple', 'orange', 'black', 'white', 'gray', 'brown', // Patterns 'striped', 'polka dot', 'floral', 'geometric', 'plaid', 'checkered', // Conditions 'old', 'new', 'worn', 'shiny', 'matte', 'glossy', 'rough', 'smooth' ]; const attributes: string[] = []; const lowerCaption = caption.toLowerCase(); for (const attr of attributeWords) { if (lowerCaption.includes(attr) && attributes.length < 3) { attributes.push(attr); } } // Extract visual details for monster generation (everything else interesting) const visualDetails = caption .replace(new RegExp(primaryObject, 'gi'), '') .replace(new RegExp(attributes.join('|'), 'gi'), '') .replace(/(?:a|an|the)\s+/gi, '') .replace(/\s+/g, ' ') .trim(); return { primaryObject: primaryObject.toLowerCase(), attributes, visualDetails }; } /** * Search for canonical Piclet or variations using Gradio Client */ static async searchCanonical( client: GradioClient, objectName: string, attributes: string[] ): Promise { try { const result = await client.predict("/search_piclet", { object_name: objectName, attributes: attributes }); // Gradio returns { data: [...] } format const data = result.data?.[0]; return data || null; } catch (error) { console.error('Failed to search canonical:', error); return null; } } /** * Create a new canonical Piclet using Gradio Client * Sends complete piclet data to be stored in dataset */ static async createCanonical( client: GradioClient, objectName: string, piclet: PicletInstance, accessToken: string ): Promise { try { const result = await client.predict("/create_canonical", { object_name: objectName, piclet_data: JSON.stringify(piclet), // Send COMPLETE piclet data token_or_username: accessToken // OAuth token for verification }); // Gradio returns { data: [...] } format return result.data?.[0]; } catch (error) { console.error('Failed to create canonical:', error); return null; } } /** * Create a variation of existing canonical Piclet using Gradio Client * Sends complete variation data to be stored in dataset */ static async createVariation( client: GradioClient, canonicalId: string, objectName: string, attributes: string[], piclet: PicletInstance, accessToken: string ): Promise { try { const result = await client.predict("/create_variation", { canonical_id: canonicalId, attributes: attributes, piclet_data: JSON.stringify(piclet), // Send COMPLETE piclet data token_or_username: accessToken, // OAuth token for verification object_name: objectName }); // Gradio returns { data: [...] } format return result.data?.[0]; } catch (error) { console.error('Failed to create variation:', error); return null; } } /** * Increment scan count for existing Piclet using Gradio Client */ static async incrementScanCount( client: GradioClient, picletId: string, objectName: string ): Promise { try { await client.predict("/increment_scan_count", { piclet_id: picletId, object_name: objectName }); } catch (error) { console.error('Failed to increment scan count:', error); } } /** * Calculate rarity based on scan count */ static calculateRarity(scanCount: number): string { if (scanCount <= 5) return 'legendary'; if (scanCount <= 20) return 'epic'; if (scanCount <= 50) return 'rare'; if (scanCount <= 100) return 'uncommon'; return 'common'; } }