UNPKG

supa-seed

Version:

A constraint-aware, framework-agnostic database seeding framework with deep PostgreSQL business logic discovery and MakerKit integration support

632 lines 23.6 kB
"use strict"; /** * Image Generator * Handles image generation from external APIs (Unsplash, Pixabay) and mock images */ Object.defineProperty(exports, "__esModule", { value: true }); exports.ImageGenerator = void 0; const logger_1 = require("../../../core/utils/logger"); const storage_types_1 = require("./storage-types"); class ImageGenerator { constructor() { this.providers = new Map(); this.cache = new Map(); this.rateLimitCache = new Map(); this.initializeProviders(); } /** * Initialize image service providers */ initializeProviders() { // Register Unsplash provider const unsplashConfig = { accessKey: process.env.UNSPLASH_ACCESS_KEY || '', apiUrl: 'https://api.unsplash.com', rateLimit: 50, // requests per hour for demo cacheDuration: 3600000, // 1 hour fallbackToMock: true }; if (unsplashConfig.accessKey) { this.providers.set('unsplash', new UnsplashProvider(unsplashConfig)); } // Register Pixabay provider const pixabayConfig = { apiKey: process.env.PIXABAY_API_KEY || '', apiUrl: 'https://pixabay.com/api', rateLimit: 100, // requests per hour cacheDuration: 3600000, // 1 hour fallbackToMock: true }; if (pixabayConfig.apiKey) { this.providers.set('pixabay', new PixabayProvider(pixabayConfig)); } // Always register mock provider as fallback const mockConfig = { baseUrl: 'https://picsum.photos', categories: { 'outdoor-adventure': ['#2F4F4F', '#228B22', '#8FBC8F'], 'saas-tools': ['#4682B4', '#708090', '#B0C4DE'], 'ecommerce': ['#FF6347', '#FFA500', '#FFD700'], 'general': ['#696969', '#A9A9A9', '#D3D3D3'] }, dimensions: [ { width: 800, height: 600 }, { width: 1024, height: 768 }, { width: 1200, height: 800 } ], formats: ['jpg', 'png'], generateRealistic: true }; this.providers.set('mock', new MockImageProvider(mockConfig)); } /** * Generate images based on options */ async generateImages(options) { logger_1.Logger.info(`🖼️ Generating ${options.count} images for domain: ${options.domain}`); try { const cacheKey = this.getCacheKey(options); // Check cache first if (this.cache.has(cacheKey)) { logger_1.Logger.debug('Using cached images'); return this.cache.get(cacheKey).slice(0, options.count); } const provider = this.selectProvider(options); const images = []; // Generate search terms from categories const searchTerms = this.generateSearchTerms(options.categories, options.domain); for (let i = 0; i < options.count && i < searchTerms.length; i++) { const searchTerm = searchTerms[i % searchTerms.length]; try { // Check rate limit await this.checkRateLimit(provider.name); // Search for images const searchResult = await provider.searchImages(searchTerm, { query: searchTerm, category: options.categories[i % options.categories.length], count: 1, dimensions: options.dimensions, safeSearch: true }); if (searchResult.success && searchResult.images.length > 0) { const imageResult = searchResult.images[0]; // Download the image const image = await provider.downloadImage(imageResult.downloadUrl); // Enhance with additional metadata image.metadata = { ...image.metadata, category: options.categories[i % options.categories.length], source: provider.name, tags: imageResult.tags, generatedAt: new Date().toISOString() }; images.push(image); logger_1.Logger.debug(`✅ Generated image: ${image.filename}`); // Respect rate limits if (options.rateLimitDelay > 0) { await new Promise(resolve => setTimeout(resolve, options.rateLimitDelay)); } } else { logger_1.Logger.warn(`Failed to find image for term: ${searchTerm}`); // Fallback to mock if enabled if (options.fallbackToMock && provider.name !== 'mock') { const mockProvider = this.providers.get('mock'); if (mockProvider) { const mockImage = await this.generateMockImage(searchTerm, options); if (mockImage) { images.push(mockImage); } } } } } catch (imageError) { logger_1.Logger.warn(`Failed to generate image ${i + 1}:`, imageError.message); // Try fallback to mock if (options.fallbackToMock && provider.name !== 'mock') { try { const mockImage = await this.generateMockImage(searchTerms[i], options); if (mockImage) { images.push(mockImage); } } catch (mockError) { logger_1.Logger.warn(`Mock image generation also failed:`, mockError.message); } } continue; } } // Cache results this.cache.set(cacheKey, images); logger_1.Logger.success(`🖼️ Generated ${images.length}/${options.count} images successfully`); return images; } catch (error) { logger_1.Logger.error('Image generation failed:', error); // Fallback to all mock images if (options.fallbackToMock) { return await this.generateAllMockImages(options); } throw error; } } /** * Generate mock image as fallback */ async generateMockImage(searchTerm, options) { const mockProvider = this.providers.get('mock'); if (!mockProvider) return null; try { const searchResult = await mockProvider.searchImages(searchTerm, { query: searchTerm, count: 1, dimensions: options.dimensions, safeSearch: true }); if (searchResult.success && searchResult.images.length > 0) { return await mockProvider.downloadImage(searchResult.images[0].downloadUrl); } } catch (error) { logger_1.Logger.warn('Mock image generation failed:', error.message); } return null; } /** * Generate all mock images as final fallback */ async generateAllMockImages(options) { logger_1.Logger.info('🔄 Falling back to mock images for all requests'); const mockProvider = this.providers.get('mock'); if (!mockProvider) return []; const images = []; const searchTerms = this.generateSearchTerms(options.categories, options.domain); for (let i = 0; i < options.count; i++) { const searchTerm = searchTerms[i % searchTerms.length]; try { const mockImage = await this.generateMockImage(searchTerm, options); if (mockImage) { images.push(mockImage); } } catch (error) { logger_1.Logger.warn(`Failed to generate mock image ${i + 1}:`, error.message); } } return images; } /** * Generate search terms from categories and domain */ generateSearchTerms(categories, domain) { const terms = []; // Add category-specific terms for (const category of categories) { if (storage_types_1.CATEGORY_SEARCH_TERMS[category]) { terms.push(...storage_types_1.CATEGORY_SEARCH_TERMS[category]); } else { terms.push(category); } } // Add domain-specific terms if (storage_types_1.CATEGORY_SEARCH_TERMS[domain]) { terms.push(...storage_types_1.CATEGORY_SEARCH_TERMS[domain]); } // Remove duplicates and shuffle const uniqueTerms = [...new Set(terms)]; return this.shuffleArray(uniqueTerms); } /** * Select the best available provider */ selectProvider(options) { // Try to use requested service first (if specified, though not in current interface) // For now, we'll use a priority-based selection // Try providers in order of preference const preferenceOrder = ['unsplash', 'pixabay', 'mock']; for (const providerName of preferenceOrder) { if (this.providers.has(providerName)) { return this.providers.get(providerName); } } // Fallback to mock (should always be available) return this.providers.get('mock'); } /** * Check rate limit for provider */ async checkRateLimit(providerName) { const cached = this.rateLimitCache.get(providerName); if (cached && cached.isLimited) { const waitTime = cached.resetTime.getTime() - Date.now(); if (waitTime > 0) { logger_1.Logger.warn(`Rate limited for ${providerName}, waiting ${waitTime}ms`); await new Promise(resolve => setTimeout(resolve, Math.min(waitTime, 60000))); } } } /** * Generate cache key for options */ getCacheKey(options) { return `${options.domain}-${options.categories.join(',')}-${options.count}-${options.dimensions.width}x${options.dimensions.height}`; } /** * Shuffle array utility */ shuffleArray(array) { const shuffled = [...array]; for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } return shuffled; } /** * Clear cache */ clearCache() { this.cache.clear(); this.rateLimitCache.clear(); logger_1.Logger.debug('Image generator cache cleared'); } /** * Get provider status */ async getProviderStatus() { const status = {}; for (const [name, provider] of this.providers) { try { const isValid = await provider.validateApiKey(); const rateLimit = await provider.getRateLimit(); status[name] = { available: isValid, rateLimit: rateLimit, lastError: null }; } catch (error) { status[name] = { available: false, rateLimit: null, lastError: error.message }; } } return status; } } exports.ImageGenerator = ImageGenerator; /** * Mock Image Provider - Always available fallback */ class MockImageProvider { constructor(config) { this.config = config; this.name = 'mock'; } async searchImages(query, options) { // Generate mock search results const images = []; for (let i = 0; i < options.count; i++) { const dimensions = this.config.dimensions[i % this.config.dimensions.length]; const seed = this.generateSeed(query, i); images.push({ id: `mock-${seed}-${i}`, url: `${this.config.baseUrl}/${dimensions.width}/${dimensions.height}?random=${seed}`, downloadUrl: `${this.config.baseUrl}/${dimensions.width}/${dimensions.height}?random=${seed}`, thumbnailUrl: `${this.config.baseUrl}/200/150?random=${seed}`, width: dimensions.width, height: dimensions.height, description: `Mock ${query} image ${i + 1}`, tags: [query, 'mock', 'generated'], author: { name: 'Mock Image Provider', url: undefined }, license: 'Public Domain', source: 'mock' }); } return { success: true, images, totalCount: images.length, rateLimitRemaining: 999, errors: [] }; } async downloadImage(imageUrl) { try { // For mock, we'll generate a simple colored rectangle const response = await fetch(imageUrl); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const blob = await response.blob(); const urlParts = imageUrl.split('/'); const dimensions = { width: parseInt(urlParts[urlParts.length - 2]) || 800, height: parseInt(urlParts[urlParts.length - 1]?.split('?')[0]) || 600 }; const filename = `mock-image-${Date.now()}-${Math.random().toString(36).substr(2, 9)}.jpg`; return { filename, blob, url: imageUrl, type: 'image/jpeg', size: blob.size, dimensions, description: 'Mock generated image', altText: 'Mock image for testing purposes', metadata: { source: 'mock', tags: ['mock', 'generated', 'placeholder'], colors: ['#cccccc'], generatedAt: new Date().toISOString(), category: 'mock' } }; } catch (error) { logger_1.Logger.error('Mock image download failed:', error); throw error; } } async validateApiKey() { return true; // Mock provider is always available } async getRateLimit() { return { remaining: 999, resetTime: new Date(Date.now() + 3600000), // 1 hour from now limit: 1000, isLimited: false }; } generateSeed(query, index) { // Generate a consistent seed for the same query and index let hash = 0; const str = query + index.toString(); for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32-bit integer } return Math.abs(hash).toString(); } } /** * Unsplash Provider - Real images from Unsplash API */ class UnsplashProvider { constructor(config) { this.config = config; this.name = 'unsplash'; } async searchImages(query, options) { try { const params = new URLSearchParams({ query, per_page: options.count.toString(), orientation: options.orientation || 'landscape', content_filter: options.safeSearch ? 'high' : 'low' }); const response = await fetch(`${this.config.apiUrl}/search/photos?${params}`, { headers: { 'Authorization': `Client-ID ${this.config.accessKey}` } }); if (!response.ok) { throw new Error(`Unsplash API error: ${response.status}`); } const data = await response.json(); const images = data.results.map((item) => ({ id: item.id, url: item.urls.regular, downloadUrl: item.urls.raw, thumbnailUrl: item.urls.thumb, width: item.width, height: item.height, description: item.description || item.alt_description || 'Unsplash image', tags: item.tags?.map((tag) => tag.title) || [], author: { name: item.user.name, url: item.user.links.html }, license: 'Unsplash License', source: 'unsplash' })); return { success: true, images, totalCount: data.total, rateLimitRemaining: parseInt(response.headers.get('X-Ratelimit-Remaining') || '50'), errors: [] }; } catch (error) { logger_1.Logger.error('Unsplash search failed:', error); return { success: false, images: [], totalCount: 0, rateLimitRemaining: 0, errors: [error.message] }; } } async downloadImage(imageUrl) { try { const response = await fetch(imageUrl); if (!response.ok) { throw new Error(`Download failed: ${response.status}`); } const blob = await response.blob(); const filename = `unsplash-${Date.now()}-${Math.random().toString(36).substr(2, 9)}.jpg`; return { filename, blob, url: imageUrl, type: blob.type, size: blob.size, dimensions: { width: 0, height: 0 }, // Would need to read from blob description: 'Unsplash image', altText: 'High-quality stock photo from Unsplash', metadata: { source: 'unsplash', originalUrl: imageUrl, tags: [], colors: [], generatedAt: new Date().toISOString(), category: 'photography' } }; } catch (error) { logger_1.Logger.error('Unsplash image download failed:', error); throw error; } } async validateApiKey() { try { const response = await fetch(`${this.config.apiUrl}/me`, { headers: { 'Authorization': `Client-ID ${this.config.accessKey}` } }); return response.ok; } catch { return false; } } async getRateLimit() { // This would typically be cached from the last API call return { remaining: this.config.rateLimit, resetTime: new Date(Date.now() + 3600000), limit: this.config.rateLimit, isLimited: false }; } } /** * Pixabay Provider - Alternative image source */ class PixabayProvider { constructor(config) { this.config = config; this.name = 'pixabay'; } async searchImages(query, options) { try { const params = new URLSearchParams({ key: this.config.apiKey, q: query, per_page: options.count.toString(), image_type: 'photo', safesearch: options.safeSearch ? 'true' : 'false', category: options.category || '' }); const response = await fetch(`${this.config.apiUrl}/?${params}`); if (!response.ok) { throw new Error(`Pixabay API error: ${response.status}`); } const data = await response.json(); const images = data.hits.map((item) => ({ id: item.id.toString(), url: item.webformatURL, downloadUrl: item.largeImageURL, thumbnailUrl: item.previewURL, width: item.imageWidth, height: item.imageHeight, description: item.tags, tags: item.tags.split(', '), author: { name: item.user, url: `https://pixabay.com/users/${item.user}-${item.user_id}/` }, license: 'Pixabay License', source: 'pixabay' })); return { success: true, images, totalCount: data.total, rateLimitRemaining: this.config.rateLimit, // Pixabay doesn't provide this in headers errors: [] }; } catch (error) { logger_1.Logger.error('Pixabay search failed:', error); return { success: false, images: [], totalCount: 0, rateLimitRemaining: 0, errors: [error.message] }; } } async downloadImage(imageUrl) { try { const response = await fetch(imageUrl); if (!response.ok) { throw new Error(`Download failed: ${response.status}`); } const blob = await response.blob(); const filename = `pixabay-${Date.now()}-${Math.random().toString(36).substr(2, 9)}.jpg`; return { filename, blob, url: imageUrl, type: blob.type, size: blob.size, dimensions: { width: 0, height: 0 }, // Would need to read from blob description: 'Pixabay image', altText: 'Stock photo from Pixabay', metadata: { source: 'pixabay', originalUrl: imageUrl, tags: [], colors: [], generatedAt: new Date().toISOString(), category: 'photography' } }; } catch (error) { logger_1.Logger.error('Pixabay image download failed:', error); throw error; } } async validateApiKey() { try { const params = new URLSearchParams({ key: this.config.apiKey, q: 'test', per_page: '1' }); const response = await fetch(`${this.config.apiUrl}/?${params}`); return response.ok; } catch { return false; } } async getRateLimit() { return { remaining: this.config.rateLimit, resetTime: new Date(Date.now() + 3600000), limit: this.config.rateLimit, isLimited: false }; } } //# sourceMappingURL=image-generator.js.map