611 lines
No EOL
20 KiB
JavaScript
611 lines
No EOL
20 KiB
JavaScript
// Global variables
|
|
let uploadedImages = [];
|
|
let keywords = [];
|
|
let generatedNames = [];
|
|
|
|
// DOM elements
|
|
const dropArea = document.getElementById('drop-area');
|
|
const fileInput = document.getElementById('file-input');
|
|
const browseBtn = document.getElementById('browse-btn');
|
|
const workflowSection = document.getElementById('workflow-section');
|
|
const keywordsSection = document.getElementById('keywords-section');
|
|
const keywordInput = document.getElementById('keyword-input');
|
|
const enhanceBtn = document.getElementById('enhance-btn');
|
|
const keywordsDisplay = document.getElementById('keywords-display');
|
|
const imagesPreview = document.getElementById('images-preview');
|
|
const imagesContainer = document.getElementById('images-container');
|
|
const downloadBtn = document.getElementById('download-btn');
|
|
|
|
// AI Configuration
|
|
const 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'
|
|
};
|
|
|
|
// Event listeners
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// Upload area event listeners
|
|
dropArea.addEventListener('click', () => fileInput.click());
|
|
browseBtn.addEventListener('click', () => fileInput.click());
|
|
fileInput.addEventListener('change', handleFileSelect);
|
|
|
|
// Drag and drop events
|
|
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
|
dropArea.addEventListener(eventName, preventDefaults, false);
|
|
});
|
|
|
|
['dragenter', 'dragover'].forEach(eventName => {
|
|
dropArea.addEventListener(eventName, highlight, false);
|
|
});
|
|
|
|
['dragleave', 'drop'].forEach(eventName => {
|
|
dropArea.addEventListener(eventName, unhighlight, false);
|
|
});
|
|
|
|
dropArea.addEventListener('drop', handleDrop, false);
|
|
|
|
// Keyword events
|
|
keywordInput.addEventListener('input', toggleEnhanceButton);
|
|
enhanceBtn.addEventListener('click', enhanceKeywords);
|
|
|
|
// Download button
|
|
downloadBtn.addEventListener('click', downloadImages);
|
|
});
|
|
|
|
// Prevent default drag behaviors
|
|
function preventDefaults(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
|
|
// Highlight drop area when item is dragged over it
|
|
function highlight() {
|
|
dropArea.classList.add('dragover');
|
|
}
|
|
|
|
// Remove highlight when item is dragged out of drop area
|
|
function unhighlight() {
|
|
dropArea.classList.remove('dragover');
|
|
}
|
|
|
|
// Handle dropped files
|
|
function handleDrop(e) {
|
|
const dt = e.dataTransfer;
|
|
const files = dt.files;
|
|
handleFiles(files);
|
|
}
|
|
|
|
// Handle file selection
|
|
function handleFileSelect(e) {
|
|
const files = e.target.files;
|
|
handleFiles(files);
|
|
}
|
|
|
|
// Process uploaded files
|
|
function handleFiles(files) {
|
|
if (files.length === 0) return;
|
|
|
|
// Convert FileList to Array
|
|
const filesArray = Array.from(files);
|
|
|
|
// Filter only image files
|
|
const imageFiles = filesArray.filter(file => file.type.startsWith('image/'));
|
|
|
|
if (imageFiles.length === 0) {
|
|
alert('Please select image files only.');
|
|
return;
|
|
}
|
|
|
|
// Clear previous images
|
|
uploadedImages = [];
|
|
|
|
// Process each image file
|
|
let processedCount = 0;
|
|
imageFiles.forEach(file => {
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
uploadedImages.push({
|
|
file: file,
|
|
name: file.name,
|
|
size: file.size,
|
|
type: file.type,
|
|
src: e.target.result,
|
|
newName: generateFileName(file.name),
|
|
visionKeywords: []
|
|
});
|
|
|
|
processedCount++;
|
|
// Show workflow section after all files are processed
|
|
if (processedCount === imageFiles.length) {
|
|
workflowSection.style.display = 'block';
|
|
keywordsSection.style.display = 'block';
|
|
imagesPreview.style.display = 'block';
|
|
updateImagesPreview();
|
|
|
|
// Smooth scroll to workflow section
|
|
workflowSection.scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'start'
|
|
});
|
|
}
|
|
};
|
|
reader.readAsDataURL(file);
|
|
});
|
|
}
|
|
|
|
// Generate a simple filename based on original name
|
|
function generateFileName(originalName) {
|
|
// Remove extension
|
|
const nameWithoutExt = originalName.substring(0, originalName.lastIndexOf('.'));
|
|
// Replace non-alphanumeric characters with spaces
|
|
const cleanName = nameWithoutExt.replace(/[^a-zA-Z0-9]/g, ' ');
|
|
// Capitalize first letter and make it SEO friendly
|
|
return cleanName.charAt(0).toUpperCase() + cleanName.slice(1);
|
|
}
|
|
|
|
// Toggle enhance button based on keyword input
|
|
function toggleEnhanceButton() {
|
|
enhanceBtn.disabled = keywordInput.value.trim() === '';
|
|
}
|
|
|
|
// Enhance keywords with AI
|
|
async function enhanceKeywords() {
|
|
const keywordText = keywordInput.value.trim();
|
|
if (keywordText === '') return;
|
|
|
|
// Show loading state
|
|
enhanceBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Enhancing...';
|
|
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 !== '');
|
|
|
|
// Add new keywords to the list
|
|
newKeywords.forEach(keyword => {
|
|
if (!keywords.includes(keyword)) {
|
|
keywords.push(keyword);
|
|
}
|
|
});
|
|
|
|
// Update keywords display
|
|
updateKeywordsDisplay();
|
|
|
|
// Clear input
|
|
keywordInput.value = '';
|
|
|
|
// Analyze images with vision AI and generate new filenames
|
|
await analyzeImagesAndGenerateNames();
|
|
} catch (error) {
|
|
console.error('Error enhancing keywords:', error);
|
|
alert('An error occurred while enhancing keywords. You may have hit rate limits. Please try again in a moment.');
|
|
|
|
// Fallback to simple keyword enhancement
|
|
fallbackToSimpleEnhancement(keywordText);
|
|
} finally {
|
|
// Reset button
|
|
enhanceBtn.innerHTML = '<i class="fas fa-magic"></i> Enhance with AI';
|
|
enhanceBtn.disabled = keywordInput.value.trim() === '';
|
|
}
|
|
}
|
|
|
|
// Fallback to simple keyword enhancement
|
|
function fallbackToSimpleEnhancement(keywordText) {
|
|
const newKeywords = keywordText.split(/[, ]+/).filter(k => k !== '');
|
|
newKeywords.forEach(keyword => {
|
|
if (!keywords.includes(keyword)) {
|
|
keywords.push(keyword);
|
|
}
|
|
});
|
|
|
|
// Add some simulated AI-enhanced keywords
|
|
const aiKeywords = [
|
|
'SEO optimized',
|
|
'high quality',
|
|
'professional',
|
|
'digital',
|
|
'modern'
|
|
];
|
|
|
|
aiKeywords.forEach(keyword => {
|
|
if (!keywords.includes(keyword)) {
|
|
keywords.push(keyword);
|
|
}
|
|
});
|
|
|
|
// Update keywords display
|
|
updateKeywordsDisplay();
|
|
|
|
// Generate new filenames for images
|
|
generateNewFileNames();
|
|
}
|
|
|
|
// 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 response = await fetch(AI_CONFIG.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'
|
|
},
|
|
body: JSON.stringify({
|
|
model: AI_CONFIG.DEEPSEEK_MODEL,
|
|
messages: [
|
|
{
|
|
role: "user",
|
|
content: prompt
|
|
}
|
|
]
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.error('API Error Response:', errorText);
|
|
throw new Error(`API request failed with status ${response.status}: ${errorText}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
return data.choices[0].message.content.trim();
|
|
}
|
|
|
|
// Analyze images with vision AI and generate new filenames
|
|
async function analyzeImagesAndGenerateNames() {
|
|
if (keywords.length === 0) return;
|
|
|
|
// Show loading state for each image
|
|
document.querySelectorAll('.new-name-input').forEach(input => {
|
|
input.disabled = true;
|
|
input.placeholder = 'Analyzing image and generating filename...';
|
|
});
|
|
|
|
try {
|
|
// Analyze each image with vision AI and generate unique filenames
|
|
const usedNames = new Set();
|
|
|
|
for (let i = 0; i < uploadedImages.length; i++) {
|
|
const image = uploadedImages[i];
|
|
|
|
// Analyze image with vision AI
|
|
const visionKeywords = await analyzeImageWithVisionAI(image.src);
|
|
image.visionKeywords = visionKeywords;
|
|
|
|
// Generate unique filename
|
|
let newName;
|
|
let attempts = 0;
|
|
do {
|
|
newName = await generateUniqueFilename(image, visionKeywords, usedNames);
|
|
attempts++;
|
|
} while (usedNames.has(newName.toLowerCase()) && attempts < 10);
|
|
|
|
// Add to used names
|
|
usedNames.add(newName.toLowerCase());
|
|
|
|
// Update the image with the new name
|
|
const extension = image.name.substring(image.name.lastIndexOf('.'));
|
|
image.newName = `${newName.substring(0, 50)}${extension}`;
|
|
}
|
|
|
|
// Update images preview
|
|
updateImagesPreview();
|
|
|
|
// Enable download button
|
|
downloadBtn.disabled = false;
|
|
} catch (error) {
|
|
console.error('Error analyzing images:', error);
|
|
alert('An error occurred while analyzing images. You may have hit rate limits or the AI service is temporarily unavailable. Please try again in a moment.');
|
|
|
|
// Revert to simple filename generation
|
|
fallbackToSimpleNaming();
|
|
} finally {
|
|
// Re-enable inputs
|
|
document.querySelectorAll('.new-name-input').forEach(input => {
|
|
input.disabled = false;
|
|
input.placeholder = '';
|
|
});
|
|
}
|
|
}
|
|
|
|
// Analyze image with 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 response = await fetch(AI_CONFIG.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'
|
|
},
|
|
body: JSON.stringify({
|
|
model: AI_CONFIG.VISION_MODEL,
|
|
messages: [
|
|
{
|
|
role: "user",
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: prompt
|
|
},
|
|
{
|
|
type: "image_url",
|
|
image_url: {
|
|
url: imageSrc
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
})
|
|
});
|
|
|
|
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}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
const visionResponse = data.choices[0].message.content.trim();
|
|
return visionResponse.split(' ').slice(0, 2); // Get up to 2 keywords
|
|
}
|
|
|
|
// Generate unique filename
|
|
async function generateUniqueFilename(image, visionKeywords, usedNames) {
|
|
const keywordString = keywords.slice(0, 5).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}
|
|
|
|
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.`;
|
|
|
|
const response = await fetch(AI_CONFIG.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'
|
|
},
|
|
body: JSON.stringify({
|
|
model: AI_CONFIG.DEEPSEEK_MODEL,
|
|
messages: [
|
|
{
|
|
role: "user",
|
|
content: prompt
|
|
}
|
|
]
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.error('Filename generation API Error Response:', errorText);
|
|
throw new Error(`Filename generation API request failed with status ${response.status}: ${errorText}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
return data.choices[0].message.content.trim();
|
|
}
|
|
|
|
// Fallback to simple naming
|
|
function fallbackToSimpleNaming() {
|
|
if (keywords.length === 0) return;
|
|
|
|
const usedNames = new Set();
|
|
|
|
uploadedImages.forEach((image, index) => {
|
|
// Combine keywords with the original filename
|
|
const keywordString = keywords.slice(0, 3).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);
|
|
|
|
// Ensure uniqueness
|
|
let counter = 1;
|
|
let uniqueName = newName;
|
|
while (usedNames.has(uniqueName.toLowerCase())) {
|
|
uniqueName = `${newName} ${counter}`;
|
|
counter++;
|
|
}
|
|
|
|
usedNames.add(uniqueName.toLowerCase());
|
|
image.newName = uniqueName + extension;
|
|
});
|
|
|
|
// Update images preview
|
|
updateImagesPreview();
|
|
|
|
// Enable download button
|
|
downloadBtn.disabled = false;
|
|
}
|
|
|
|
// Fallback function to generate new filenames without AI
|
|
function generateNewFileNames() {
|
|
if (keywords.length === 0) return;
|
|
|
|
const usedNames = new Set();
|
|
|
|
uploadedImages.forEach((image, index) => {
|
|
// Combine keywords with the original filename
|
|
const keywordString = keywords.slice(0, 3).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);
|
|
|
|
// Ensure uniqueness
|
|
let counter = 1;
|
|
let uniqueName = newName;
|
|
while (usedNames.has(uniqueName.toLowerCase())) {
|
|
uniqueName = `${newName} ${counter}`;
|
|
counter++;
|
|
}
|
|
|
|
usedNames.add(uniqueName.toLowerCase());
|
|
image.newName = uniqueName + extension;
|
|
});
|
|
|
|
// Update images preview
|
|
updateImagesPreview();
|
|
|
|
// Enable download button
|
|
downloadBtn.disabled = false;
|
|
}
|
|
|
|
// Highlight vision keywords in filename
|
|
function highlightVisionKeywords(filename, visionKeywords) {
|
|
if (!visionKeywords || visionKeywords.length === 0) {
|
|
return filename;
|
|
}
|
|
|
|
let highlightedName = filename;
|
|
visionKeywords.forEach(keyword => {
|
|
if (keyword && keyword.trim()) {
|
|
const regex = new RegExp(`\\b${keyword.trim()}\\b`, 'gi');
|
|
highlightedName = highlightedName.replace(regex, `<span class="vision-highlight">${keyword}</span>`);
|
|
}
|
|
});
|
|
|
|
return highlightedName;
|
|
}
|
|
|
|
// Update keywords display
|
|
function updateKeywordsDisplay() {
|
|
keywordsDisplay.innerHTML = '';
|
|
|
|
keywords.forEach((keyword, index) => {
|
|
const keywordChip = document.createElement('div');
|
|
keywordChip.className = 'keyword-chip';
|
|
keywordChip.innerHTML = `
|
|
<span>${keyword}</span>
|
|
<button class="remove-keyword" data-index="${index}">×</button>
|
|
`;
|
|
keywordsDisplay.appendChild(keywordChip);
|
|
});
|
|
|
|
// Add event listeners to remove buttons
|
|
document.querySelectorAll('.remove-keyword').forEach(button => {
|
|
button.addEventListener('click', (e) => {
|
|
const index = parseInt(e.target.getAttribute('data-index'));
|
|
keywords.splice(index, 1);
|
|
updateKeywordsDisplay();
|
|
generateNewFileNames();
|
|
});
|
|
});
|
|
}
|
|
|
|
// Update images preview
|
|
function updateImagesPreview() {
|
|
imagesContainer.innerHTML = '';
|
|
|
|
uploadedImages.forEach((image, index) => {
|
|
const imageCard = document.createElement('div');
|
|
imageCard.className = 'image-card';
|
|
|
|
// Get filename without extension for highlighting
|
|
const nameWithoutExt = image.newName.substring(0, image.newName.lastIndexOf('.'));
|
|
const extension = image.newName.substring(image.newName.lastIndexOf('.'));
|
|
const highlightedName = highlightVisionKeywords(nameWithoutExt, image.visionKeywords);
|
|
|
|
imageCard.innerHTML = `
|
|
<img src="${image.src}" alt="${image.name}" class="image-thumbnail">
|
|
<div class="image-info">
|
|
<div class="original-name">Original: ${image.name}</div>
|
|
${image.visionKeywords && image.visionKeywords.length > 0 ?
|
|
`<div class="vision-keywords">Vision AI: <span class="vision-tags">${image.visionKeywords.join(', ')}</span></div>` : ''}
|
|
<div class="new-name-container">
|
|
<label>New name:</label>
|
|
<div class="filename-display">${highlightedName}${extension}</div>
|
|
<input type="text" class="new-name-input" value="${image.newName}" data-index="${index}">
|
|
</div>
|
|
</div>
|
|
`;
|
|
imagesContainer.appendChild(imageCard);
|
|
});
|
|
|
|
// Add event listeners to name inputs
|
|
document.querySelectorAll('.new-name-input').forEach(input => {
|
|
input.addEventListener('input', (e) => {
|
|
const index = parseInt(e.target.getAttribute('data-index'));
|
|
uploadedImages[index].newName = e.target.value;
|
|
});
|
|
});
|
|
}
|
|
|
|
// Download images as ZIP
|
|
async function downloadImages() {
|
|
if (uploadedImages.length === 0) return;
|
|
|
|
// Show loading state
|
|
downloadBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Preparing Download...';
|
|
downloadBtn.disabled = true;
|
|
|
|
try {
|
|
// Create a new ZIP file
|
|
const zip = new JSZip();
|
|
|
|
// Add each image to the ZIP with its new name
|
|
for (const image of uploadedImages) {
|
|
// Convert data URL to blob
|
|
const blob = await fetch(image.src).then(res => res.blob());
|
|
zip.file(image.newName, blob);
|
|
}
|
|
|
|
// Generate the ZIP file
|
|
const content = await zip.generateAsync({type: "blob"});
|
|
|
|
// Create download link
|
|
const url = URL.createObjectURL(content);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'renamed-images.zip';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
|
|
// Clean up
|
|
setTimeout(() => {
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
}, 100);
|
|
|
|
// Reset button
|
|
downloadBtn.innerHTML = '<i class="fas fa-download"></i> Download Renamed Images as ZIP';
|
|
downloadBtn.disabled = false;
|
|
} catch (error) {
|
|
console.error('Error creating ZIP file:', error);
|
|
alert('An error occurred while creating the ZIP file. Please try again.');
|
|
|
|
// Reset button
|
|
downloadBtn.innerHTML = '<i class="fas fa-download"></i> Download Renamed Images as ZIP';
|
|
downloadBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
// Initialize the page
|
|
function init() {
|
|
// Set up any initial state
|
|
downloadBtn.disabled = true;
|
|
}
|
|
|
|
// Call init when page loads
|
|
init(); |