piclets / src /lib /services /canonicalService.ts
Fraser's picture
RESET TO MONSTER DISCOVERY SYSTEM
565e57b
raw
history blame
6.39 kB
import type { PicletInstance, DiscoveryStatus } from '$lib/db/schema';
const SERVER_URL = import.meta.env.DEV
? 'http://localhost:3000'
: 'https://piclets-server.herokuapp.com';
export interface CanonicalSearchResult {
status: DiscoveryStatus;
piclet: PicletInstance;
canonicalId?: string;
matchedAttributes?: string[];
suggestedVariation?: 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
*/
static async searchCanonical(
objectName: string,
attributes: string[]
): Promise<CanonicalSearchResult | null> {
try {
const response = await fetch(`${SERVER_URL}/api/piclets/search`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ object: objectName, attributes })
});
if (!response.ok) {
console.error('Server search failed:', response.status);
return null;
}
return await response.json();
} catch (error) {
console.error('Failed to search canonical:', error);
return null;
}
}
/**
* Create a new canonical Piclet
*/
static async createCanonical(
piclet: Partial<PicletInstance>,
discovererName: string
): Promise<PicletInstance | null> {
try {
const response = await fetch(`${SERVER_URL}/api/piclets/canonical`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...piclet,
discoveredBy: discovererName,
discoveredAt: new Date(),
isCanonical: true,
scanCount: 1
})
});
if (!response.ok) {
console.error('Failed to create canonical:', response.status);
return null;
}
return await response.json();
} catch (error) {
console.error('Failed to create canonical:', error);
return null;
}
}
/**
* Create a variation of existing canonical Piclet
*/
static async createVariation(
canonicalId: string,
variation: Partial<PicletInstance>,
discovererName: string
): Promise<PicletInstance | null> {
try {
const response = await fetch(`${SERVER_URL}/api/piclets/variation`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
canonicalId,
...variation,
discoveredBy: discovererName,
discoveredAt: new Date(),
isCanonical: false,
scanCount: 1
})
});
if (!response.ok) {
console.error('Failed to create variation:', response.status);
return null;
}
return await response.json();
} catch (error) {
console.error('Failed to create variation:', error);
return null;
}
}
/**
* Increment scan count for existing Piclet
*/
static async incrementScanCount(picletId: string): Promise<void> {
try {
await fetch(`${SERVER_URL}/api/piclets/${picletId}/scan`, {
method: 'POST'
});
} 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';
}
}