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
JavaScript
;
/**
* 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