Front end with upload and vision features working (open ai r1 and grok vision with void ai

This commit is contained in:
Jeen Koster 2025-08-05 21:46:06 +02:00
parent a829770c7e
commit d9226251e9
5 changed files with 283 additions and 50 deletions

19
.env
View file

@ -1,13 +1,24 @@
# === AI CONFIGURATION ===
# Your API key or access token
# OpenRouter API key for text generation (DeepSeek)
AI_API_KEY=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
# Name or ID of the AI model
# DeepSeek model for text/keyword generation
AI_MODEL_NAME=deepseek/deepseek-chat-v3-0324:free
# Endpoint or base URL for DeepSeek API (update if different)
AI_API_URL=https://api.deepseek.com/v1
# OpenRouter API URL
AI_API_URL=https://openrouter.ai/api/v1/chat/completions
# === GROK VISION API CONFIGURATION ===
# Grok API key for vision analysis (add your Grok API key here)
GROK_API_KEY=sk-voidai-vVU2HHiq1txTNmXdZOP98LzARmi4HsptTixMqFSX4yBbw8ogvmKlJEPeKrH1hwEd6j6AnED9LsR6ztPtRMT7UzeLOyxQkasbwKow
# Grok-2-Vision model
GROK_VISION_MODEL=grok-2-vision-1212
# Grok API URL
GROK_API_URL=https://api.voidai.app/v1/
# Optional: AI task-specific configuration
AI_TASK=keyword_generation

75
GROK_SETUP.md Normal file
View file

@ -0,0 +1,75 @@
# Grok-2-Vision Integration Setup
## How to Configure Your Grok API Key
The SEO Image Renamer now supports Grok-2-Vision for enhanced image analysis. Follow these steps to set up your API key:
### Step 1: Get Your Grok API Key
1. Visit [https://console.x.ai/](https://console.x.ai/)
2. Sign up or log in to your account
3. Navigate to API Keys section
4. Create a new API key for your project
5. Copy the generated API key
### Step 2: Update the .env File
1. Open the `.env` file in your project directory
2. Find the line that says: `GROK_API_KEY=your_grok_api_key_here`
3. Replace `your_grok_api_key_here` with your actual Grok API key
4. Save the file
Example:
```env
GROK_API_KEY=gsk-proj-abc123def456ghi789jkl012mno345pqr678stu901vwx234yz567
```
### Step 3: Test the Integration
1. Open your SEO Image Renamer website
2. Upload some images
3. Add keywords and click "Generate SEO Keywords"
4. Check the browser console for the message: "Grok API Key configured: Yes"
## What's New with Grok-2-Vision
### Enhanced Image Analysis
- **Better Keyword Extraction**: Grok-2-Vision provides more accurate and contextual keywords from your images
- **SEO-Focused Analysis**: The AI is specifically prompted to identify keywords useful for SEO image naming
- **Improved Accuracy**: Better object and action recognition compared to previous vision models
### Technical Improvements
- **Dedicated Vision API**: Separate API endpoint optimized for vision tasks
- **Enhanced Prompts**: Specialized prompts for extracting SEO-relevant keywords
- **Better Error Handling**: Improved fallback mechanisms if vision analysis fails
- **Cleaner Responses**: Better parsing and filtering of AI responses
## Troubleshooting
### Common Issues
**Issue**: "Grok API Key configured: No" in console
**Solution**: Make sure you've replaced `sk-voidai-vVU2HHiq1txTNmXdZOP98LzARmi4HsptTixMqFSX4yBbw8ogvmKlJEPeKrH1hwEd6j6AnED9LsR6ztPtRMT7UzeLOyxQkasbwKow` with your actual API key in the .env file
**Issue**: Vision analysis fails or returns generic keywords
**Solution**:
1. Check that your API key is valid and has sufficient credits
2. Ensure your API key has access to Grok-2-Vision model
3. Check browser console for specific error messages
**Issue**: .env file not loading
**Solution**:
1. Make sure the .env file is in the same directory as index.html
2. Ensure your web server allows access to .env files (may need to serve from a local server)
3. For production, you may need to hardcode the API key in the JavaScript file
### API Limits
- Grok-2-Vision has rate limits and usage quotas
- If you hit limits, the system will fallback to generic keywords
- Monitor your usage in the X.AI console
## Security Note
Keep your API key secure and never commit it to public repositories. The .env file should be added to your .gitignore file.
## Support
If you encounter issues with the Grok-2-Vision integration, check:
1. X.AI API documentation: [https://docs.x.ai/](https://docs.x.ai/)
2. Your API key permissions and quotas
3. Browser console for detailed error messages

20
config.js Normal file
View file

@ -0,0 +1,20 @@
// Configuration file for SEO Image Renamer
// Update your API keys here
window.APP_CONFIG = {
// OpenRouter API for text generation (DeepSeek) - already configured
OPENROUTER_API_KEY: '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',
DEEPSEEK_MODEL: 'deepseek/deepseek-chat-v3-0324:free',
OPENROUTER_API_URL: 'https://openrouter.ai/api/v1/chat/completions',
// Grok-2-Vision API - ADD YOUR GROK API KEY HERE
GROK_API_KEY: 'sk-voidai-vVU2HHiq1txTNmXdZOP98LzARmi4HsptTixMqFSX4yBbw8ogvmKlJEPeKrH1hwEd6j6AnED9LsR6ztPtRMT7UzeLOyxQkasbwKow'
GROK_VISION_MODEL: 'grok-2-vision-1212',
GROK_API_URL: 'https://api.x.ai/v1/chat/completions'
};
// Instructions:
// 1. Get your Grok API key from https://console.x.ai/
// 2. Replace 'your_grok_api_key_here' above with your actual API key
// 3. Save this file
// 4. The website will automatically use your new API key

View file

@ -100,9 +100,9 @@
</div>
<div class="keywords-input">
<input type="text" id="keyword-input" placeholder="Enter keywords (e.g., beach vacation, summer party)">
<input type="text" id="keyword-input" placeholder="Enter keywords or phrases (e.g., african barbershop amsterdam, professional wedding photography)">
<button id="enhance-btn" class="btn btn-primary" disabled>
<i class="fas fa-magic"></i> Enhance with AI
<i class="fas fa-magic"></i> Generate SEO Keywords
</button>
</div>
@ -303,6 +303,8 @@
<!-- Include JSZip library for ZIP functionality -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<!-- Include configuration file -->
<script src="config.js"></script>
<script src="script.js"></script>
</body>
</html>

207
script.js
View file

@ -16,13 +16,46 @@ const imagesPreview = document.getElementById('images-preview');
const imagesContainer = document.getElementById('images-container');
const downloadBtn = document.getElementById('download-btn');
// AI Configuration
const AI_CONFIG = {
// AI Configuration - loaded from config.js
let AI_CONFIG = {};
// Initialize configuration on page load
function initializeConfig() {
// Load configuration from window.APP_CONFIG (set by config.js)
if (window.APP_CONFIG) {
AI_CONFIG = {
// OpenRouter API for text generation (DeepSeek)
API_KEY: window.APP_CONFIG.OPENROUTER_API_KEY,
DEEPSEEK_MODEL: window.APP_CONFIG.DEEPSEEK_MODEL,
API_URL: window.APP_CONFIG.OPENROUTER_API_URL,
// Grok-2-Vision API for image analysis
GROK_API_KEY: window.APP_CONFIG.GROK_API_KEY,
GROK_VISION_MODEL: window.APP_CONFIG.GROK_VISION_MODEL,
GROK_API_URL: window.APP_CONFIG.GROK_API_URL
};
console.log('✅ Configuration loaded successfully from config.js');
console.log('🔑 Grok API Key configured:', AI_CONFIG.GROK_API_KEY !== 'your_grok_api_key_here' ? '✅ Yes' : '❌ No - Please add your API key to config.js');
if (AI_CONFIG.GROK_API_KEY === 'your_grok_api_key_here') {
console.warn('⚠️ SETUP REQUIRED: Please add your Grok API key to config.js file');
console.log('📖 Instructions: Open config.js and replace "your_grok_api_key_here" with your actual Grok API key from https://console.x.ai/');
}
} else {
console.error('❌ Configuration file (config.js) not loaded properly');
// Fallback configuration
AI_CONFIG = {
API_KEY: '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',
DEEPSEEK_MODEL: 'deepseek/deepseek-chat-v3-0324:free',
VISION_MODEL: 'meta-llama/llama-3.2-11b-vision-instruct:free',
API_URL: 'https://openrouter.ai/api/v1/chat/completions'
};
API_URL: 'https://openrouter.ai/api/v1/chat/completions',
GROK_API_KEY: 'your_grok_api_key_here',
GROK_VISION_MODEL: 'grok-2-vision-1212',
GROK_API_URL: 'https://api.x.ai/v1/chat/completions'
};
}
}
// Event listeners
document.addEventListener('DOMContentLoaded', () => {
@ -156,19 +189,27 @@ async function enhanceKeywords() {
if (keywordText === '') return;
// Show loading state
enhanceBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Enhancing...';
enhanceBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Generating SEO Keywords...';
enhanceBtn.disabled = true;
try {
// Call AI API to enhance keywords
const enhancedKeywords = await callAIKeywordEnhancement(keywordText);
// Split keywords by comma or space
const newKeywords = enhancedKeywords.split(/[, ]+/).filter(k => k !== '');
// Better handling for keyphrases - split by comma primarily, preserve phrases
const newKeywords = enhancedKeywords.split(',').map(k => k.trim()).filter(k => k !== '');
// Add new keywords to the list
// Add new keywords/keyphrases to the list
newKeywords.forEach(keyword => {
if (!keywords.includes(keyword)) {
// Avoid duplicates by checking both exact match and similarity
const lowercaseKeyword = keyword.toLowerCase();
const isDuplicate = keywords.some(existing =>
existing.toLowerCase() === lowercaseKeyword ||
existing.toLowerCase().includes(lowercaseKeyword) ||
lowercaseKeyword.includes(existing.toLowerCase())
);
if (!isDuplicate) {
keywords.push(keyword);
}
});
@ -189,7 +230,7 @@ async function enhanceKeywords() {
fallbackToSimpleEnhancement(keywordText);
} finally {
// Reset button
enhanceBtn.innerHTML = '<i class="fas fa-magic"></i> Enhance with AI';
enhanceBtn.innerHTML = '<i class="fas fa-magic"></i> Generate SEO Keywords';
enhanceBtn.disabled = keywordInput.value.trim() === '';
}
}
@ -225,9 +266,55 @@ function fallbackToSimpleEnhancement(keywordText) {
generateNewFileNames();
}
// Detect language from provided keywords
function detectLanguage(keywords) {
const lowerKeywords = keywords.toLowerCase();
// Common patterns for different languages
const languagePatterns = {
dutch: /\b(de|het|en|in|van|met|bij|naar|voor|amsterdam|nederland|barbier|kapper|winkel|kapperszaak|amsterdam|rotterdam|den haag|utrecht)\b/,
german: /\b(der|die|das|und|in|von|mit|bei|nach|für|berlin|münchen|deutschland|friseur|laden|geschäft|hamburg|köln)\b/,
french: /\b(le|la|les|de|du|des|et|dans|avec|chez|vers|pour|paris|france|coiffeur|magasin|boutique|lyon|marseille)\b/,
spanish: /\b(el|la|los|las|de|del|y|en|con|para|hacia|madrid|españa|peluquero|tienda|negocio|barcelona|valencia)\b/,
italian: /\b(il|la|lo|gli|le|di|del|e|in|con|per|verso|roma|italia|parrucchiere|negozio|bottega|milano|napoli)\b/,
};
for (const [lang, pattern] of Object.entries(languagePatterns)) {
if (pattern.test(lowerKeywords)) {
return lang;
}
}
return 'english'; // Default to English
}
// Call AI API to enhance keywords
async function callAIKeywordEnhancement(keywords) {
const prompt = `Enhance these keywords for SEO image optimization. Provide 10 additional related keywords that would help images rank better in search engines. Return only the keywords separated by commas, nothing else. Keywords: ${keywords}`;
const detectedLanguage = detectLanguage(keywords);
const languageInstruction = detectedLanguage === 'english' ? '' : `Respond in ${detectedLanguage} language. `;
const prompt = `You are an expert SEO consultant specializing in high-performing search keywords and keyphrases that people actually search for on Google.
${languageInstruction}Based on these seed keywords: "${keywords}"
Generate 8-12 SEO-optimized keywords and keyphrases that:
1. Are commonly searched terms people actually use on Google
2. Include both short keywords (1-2 words) and longer keyphrases (3-5 words)
3. Focus on commercial intent and local SEO when relevant
4. Include location-specific terms if a location is mentioned
5. Consider trending and high-volume search terms
6. Think like a customer searching for these services/products
Examples of good SEO keyphrases:
- "african barbershop amsterdam"
- "professional hair cutting services"
- "luxury wedding photography"
- "modern interior design ideas"
- "organic food delivery service"
Return only the keywords and keyphrases separated by commas, no explanations or extra text.
Seed keywords: ${keywords}`;
const response = await fetch(AI_CONFIG.API_URL, {
method: 'POST',
@ -315,20 +402,18 @@ async function analyzeImagesAndGenerateNames() {
}
}
// Analyze image with vision AI
// Analyze image with Grok-2-Vision AI
async function analyzeImageWithVisionAI(imageSrc) {
const prompt = "Analyze this image and provide exactly 2 keywords that describe the main subject or action in the image. Return only the two keywords separated by a space, nothing else.";
const prompt = "Analyze this image and provide exactly 2 descriptive keywords that describe the main subject, object, or action in the image. Focus on what would be useful for SEO image naming. Return only the two keywords separated by a space, nothing else.";
const response = await fetch(AI_CONFIG.API_URL, {
const response = await fetch(AI_CONFIG.GROK_API_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${AI_CONFIG.API_KEY}`,
'Content-Type': 'application/json',
'HTTP-Referer': window.location.href,
'X-Title': 'SEO Image Renamer'
'Authorization': `Bearer ${AI_CONFIG.GROK_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: AI_CONFIG.VISION_MODEL,
model: AI_CONFIG.GROK_VISION_MODEL,
messages: [
{
role: "user",
@ -345,35 +430,62 @@ async function analyzeImageWithVisionAI(imageSrc) {
}
]
}
]
],
max_tokens: 50,
temperature: 0.3
})
});
if (!response.ok) {
const errorText = await response.text();
console.error('Vision API Error Response:', errorText);
throw new Error(`Vision API request failed with status ${response.status}: ${errorText}`);
console.error('Grok Vision API Error Response:', errorText);
throw new Error(`Grok Vision API request failed with status ${response.status}: ${errorText}`);
}
const data = await response.json();
const visionResponse = data.choices[0].message.content.trim();
return visionResponse.split(' ').slice(0, 2); // Get up to 2 keywords
// Clean up response and extract keywords
const keywords = visionResponse.split(' ').filter(word =>
word.length > 2 &&
!word.includes('.') &&
!word.includes(',') &&
/^[a-zA-Z]+$/.test(word)
).slice(0, 2);
return keywords.length >= 2 ? keywords : ['image', 'content']; // Fallback if extraction fails
}
// Generate unique filename
async function generateUniqueFilename(image, visionKeywords, usedNames) {
const keywordString = keywords.slice(0, 5).join(', ');
const detectedLanguage = detectLanguage(keywords.join(' '));
const languageInstruction = detectedLanguage === 'english' ? '' : `Generate the filename in ${detectedLanguage} language. `;
// Ensure we use ALL keywords, not just first 5
const keywordString = keywords.join(', ');
const visionString = visionKeywords.join(' ');
const prompt = `Create a natural, descriptive filename for an image.
User keywords: ${keywordString}
Image content keywords: ${visionString}
Original filename: ${image.name}
const prompt = `You are an SEO filename expert. Create an SEO-optimized filename for this image that will rank well in search engines.
Create a human-readable phrase that combines these elements naturally, like "burning car at a carshow in france".
Use 3-7 words, start with a capital letter, and use spaces between words.
Do not include the file extension.
Return only the filename, nothing else.`;
${languageInstruction}REQUIREMENTS:
1. MUST include at least 2-3 of these user keywords: ${keywordString}
2. MUST incorporate image content: ${visionString}
3. Create natural, searchable phrases people actually type into Google
4. Use 4-8 words maximum
5. Focus on commercial intent and local SEO when applicable
6. Think like someone searching for this image online
EXAMPLES of good SEO filenames:
- "professional african barbershop amsterdam"
- "luxury wedding photography netherlands"
- "modern restaurant interior design"
- "organic food delivery service menu"
- "vintage car classic automobile show"
Original filename reference: ${image.name}
Create ONE natural filename that combines the keywords strategically for maximum SEO impact.
Return only the filename with spaces between words, no punctuation, no file extension.`;
const response = await fetch(AI_CONFIG.API_URL, {
method: 'POST',
@ -411,13 +523,18 @@ function fallbackToSimpleNaming() {
const usedNames = new Set();
uploadedImages.forEach((image, index) => {
// Combine keywords with the original filename
const keywordString = keywords.slice(0, 3).join(' ');
// Use more keywords for better SEO - cycle through all keywords
const keywordCount = Math.min(keywords.length, 4); // Use up to 4 keywords
const startIndex = index % Math.max(1, keywords.length - 2); // Rotate starting point
const selectedKeywords = keywords.slice(startIndex, startIndex + keywordCount);
const keywordString = selectedKeywords.join(' ');
const nameWithoutExt = image.name.substring(0, image.name.lastIndexOf('.'));
const extension = image.name.substring(image.name.lastIndexOf('.'));
// Create new name
let newName = `${keywordString} ${nameWithoutExt}`.substring(0, 50);
// Create SEO-focused name - prioritize keywords over original filename
let newName = keywordString.length > 0 ? keywordString : nameWithoutExt;
newName = newName.substring(0, 45); // Leave room for counter
// Ensure uniqueness
let counter = 1;
@ -445,13 +562,18 @@ function generateNewFileNames() {
const usedNames = new Set();
uploadedImages.forEach((image, index) => {
// Combine keywords with the original filename
const keywordString = keywords.slice(0, 3).join(' ');
// Use more keywords for better SEO - cycle through all keywords
const keywordCount = Math.min(keywords.length, 4); // Use up to 4 keywords
const startIndex = index % Math.max(1, keywords.length - 2); // Rotate starting point
const selectedKeywords = keywords.slice(startIndex, startIndex + keywordCount);
const keywordString = selectedKeywords.join(' ');
const nameWithoutExt = image.name.substring(0, image.name.lastIndexOf('.'));
const extension = image.name.substring(image.name.lastIndexOf('.'));
// Create new name
let newName = `${keywordString} ${nameWithoutExt}`.substring(0, 50);
// Create SEO-focused name - prioritize keywords over original filename
let newName = keywordString.length > 0 ? keywordString : nameWithoutExt;
newName = newName.substring(0, 45); // Leave room for counter
// Ensure uniqueness
let counter = 1;
@ -603,6 +725,9 @@ async function downloadImages() {
// Initialize the page
function init() {
// Initialize configuration first
initializeConfig();
// Set up any initial state
downloadBtn.disabled = true;
}