214 lines
7.2 KiB
JavaScript
214 lines
7.2 KiB
JavaScript
/**
|
|
* Image Generation API Bridge for Goose Ultra
|
|
*
|
|
* Implements multimodal image generation for Chat Mode.
|
|
* Supports multiple providers: Pollinations.ai (free), DALL-E, Stability AI
|
|
*/
|
|
|
|
import https from 'https';
|
|
import http from 'http';
|
|
import crypto from 'crypto';
|
|
|
|
// Provider: Pollinations.ai (Free, no API key required)
|
|
// Generates images from text prompts using Stable Diffusion XL
|
|
const POLLINATIONS_BASE = 'https://image.pollinations.ai/prompt/';
|
|
|
|
// Image cache directory
|
|
import path from 'path';
|
|
import fs from 'fs';
|
|
import os from 'os';
|
|
|
|
const getCacheDir = () => {
|
|
const dir = path.join(os.homedir(), '.goose-ultra', 'image-cache');
|
|
if (!fs.existsSync(dir)) {
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
}
|
|
return dir;
|
|
};
|
|
|
|
/**
|
|
* Generate an image from a text prompt using Pollinations.ai (free)
|
|
* @param {string} prompt - The image description
|
|
* @param {object} options - Optional settings
|
|
* @returns {Promise<{url: string, localPath: string, prompt: string}>}
|
|
*/
|
|
export async function generateImage(prompt, options = {}) {
|
|
const {
|
|
width = 1024,
|
|
height = 1024,
|
|
seed = Math.floor(Math.random() * 1000000),
|
|
model = 'flux', // 'flux' or 'turbo'
|
|
nologo = true
|
|
} = options;
|
|
|
|
console.log('[ImageAPI] Generating image for prompt:', prompt.substring(0, 100) + '...');
|
|
|
|
// Build Pollinations URL
|
|
const encodedPrompt = encodeURIComponent(prompt);
|
|
const params = new URLSearchParams({
|
|
width: String(width),
|
|
height: String(height),
|
|
seed: String(seed),
|
|
model: model,
|
|
nologo: String(nologo)
|
|
});
|
|
|
|
const imageUrl = `${POLLINATIONS_BASE}${encodedPrompt}?${params.toString()}`;
|
|
|
|
// Download and cache image
|
|
const imageId = crypto.createHash('md5').update(prompt + seed).digest('hex');
|
|
const localPath = path.join(getCacheDir(), `${imageId}.png`);
|
|
|
|
try {
|
|
await downloadImage(imageUrl, localPath);
|
|
console.log('[ImageAPI] Image saved to:', localPath);
|
|
|
|
return {
|
|
url: imageUrl,
|
|
localPath: localPath,
|
|
prompt: prompt,
|
|
width,
|
|
height,
|
|
seed
|
|
};
|
|
} catch (error) {
|
|
console.error('[ImageAPI] Generation failed:', error.message);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Download an image from URL to local path
|
|
*/
|
|
function downloadImage(url, destPath) {
|
|
return new Promise((resolve, reject) => {
|
|
const urlObj = new URL(url);
|
|
const client = urlObj.protocol === 'https:' ? https : http;
|
|
|
|
const file = fs.createWriteStream(destPath);
|
|
|
|
const request = client.get(url, { timeout: 60000 }, (response) => {
|
|
// Handle redirects
|
|
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
file.close();
|
|
fs.unlinkSync(destPath);
|
|
return downloadImage(response.headers.location, destPath).then(resolve).catch(reject);
|
|
}
|
|
|
|
if (response.statusCode !== 200) {
|
|
file.close();
|
|
fs.unlinkSync(destPath);
|
|
reject(new Error(`HTTP ${response.statusCode}: Failed to download image`));
|
|
return;
|
|
}
|
|
|
|
response.pipe(file);
|
|
|
|
file.on('finish', () => {
|
|
file.close();
|
|
resolve(destPath);
|
|
});
|
|
});
|
|
|
|
request.on('error', (err) => {
|
|
file.close();
|
|
if (fs.existsSync(destPath)) fs.unlinkSync(destPath);
|
|
reject(err);
|
|
});
|
|
|
|
request.on('timeout', () => {
|
|
request.destroy();
|
|
file.close();
|
|
if (fs.existsSync(destPath)) fs.unlinkSync(destPath);
|
|
reject(new Error('Image download timeout'));
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Detect if a user message is requesting image generation
|
|
* @param {string} message - User message
|
|
* @returns {{isImageRequest: boolean, prompt: string | null}}
|
|
*/
|
|
export function detectImageRequest(message) {
|
|
const lower = message.toLowerCase();
|
|
|
|
// Common image generation patterns
|
|
const patterns = [
|
|
/^(generate|create|make|draw|design|paint|illustrate|render|produce)\s+(an?\s+)?(image|picture|photo|illustration|artwork|art|graphic|visual|drawing|painting)\s+(of|showing|depicting|with|about|for)?\s*/i,
|
|
/^(show me|give me|i want|can you (make|create|generate)|please (make|create|generate))\s+(an?\s+)?(image|picture|photo|illustration|artwork)\s+(of|showing|depicting|with|about|for)?\s*/i,
|
|
/image\s+of\s+/i,
|
|
/picture\s+of\s+/i,
|
|
/draw\s+(me\s+)?(a|an)\s+/i,
|
|
/visualize\s+/i,
|
|
/create\s+art\s+(of|for|showing)\s*/i
|
|
];
|
|
|
|
for (const pattern of patterns) {
|
|
if (pattern.test(lower)) {
|
|
// Extract the actual image description
|
|
let prompt = message;
|
|
|
|
// Remove the command prefix to get just the description
|
|
prompt = prompt.replace(/^(generate|create|make|draw|design|paint|illustrate|render|produce)\s+(an?\s+)?(image|picture|photo|illustration|artwork|art|graphic|visual|drawing|painting)\s+(of|showing|depicting|with|about|for)?\s*/i, '');
|
|
prompt = prompt.replace(/^(show me|give me|i want|can you (make|create|generate)|please (make|create|generate))\s+(an?\s+)?(image|picture|photo|illustration|artwork)\s+(of|showing|depicting|with|about|for)?\s*/i, '');
|
|
prompt = prompt.replace(/^image\s+of\s+/i, '');
|
|
prompt = prompt.replace(/^picture\s+of\s+/i, '');
|
|
prompt = prompt.replace(/^draw\s+(me\s+)?(a|an)\s+/i, '');
|
|
prompt = prompt.replace(/^visualize\s+/i, '');
|
|
prompt = prompt.replace(/^create\s+art\s+(of|for|showing)\s*/i, '');
|
|
|
|
prompt = prompt.trim();
|
|
|
|
// If we couldn't extract a clean prompt, use original
|
|
if (prompt.length < 3) prompt = message;
|
|
|
|
return { isImageRequest: true, prompt: prompt };
|
|
}
|
|
}
|
|
|
|
// Check for explicit "image:" prefix
|
|
if (lower.startsWith('image:') || lower.startsWith('/image ') || lower.startsWith('/imagine ')) {
|
|
const prompt = message.replace(/^(image:|\/image\s+|\/imagine\s+)/i, '').trim();
|
|
return { isImageRequest: true, prompt };
|
|
}
|
|
|
|
return { isImageRequest: false, prompt: null };
|
|
}
|
|
|
|
/**
|
|
* Get a list of cached images
|
|
*/
|
|
export function getCachedImages() {
|
|
const cacheDir = getCacheDir();
|
|
try {
|
|
const files = fs.readdirSync(cacheDir);
|
|
return files.filter(f => f.endsWith('.png')).map(f => path.join(cacheDir, f));
|
|
} catch (e) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear old cached images (older than 7 days)
|
|
*/
|
|
export function cleanupCache(maxAgeDays = 7) {
|
|
const cacheDir = getCacheDir();
|
|
const maxAge = maxAgeDays * 24 * 60 * 60 * 1000;
|
|
const now = Date.now();
|
|
|
|
try {
|
|
const files = fs.readdirSync(cacheDir);
|
|
for (const file of files) {
|
|
const filePath = path.join(cacheDir, file);
|
|
const stat = fs.statSync(filePath);
|
|
if (now - stat.mtimeMs > maxAge) {
|
|
fs.unlinkSync(filePath);
|
|
console.log('[ImageAPI] Cleaned up:', file);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error('[ImageAPI] Cache cleanup error:', e.message);
|
|
}
|
|
}
|