piclets / src /lib /services /canonicalService.ts
Fraser's picture
fingers crossed
d009748
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<CanonicalSearchResult | null> {
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<any> {
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<any> {
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<void> {
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';
}
}