feat(gallery): Add auto tag feature for images
This commit is contained in:
+645
-4
@@ -183,10 +183,14 @@
|
||||
class="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white text-sm font-medium rounded-lg transition-colors">
|
||||
☰ List View
|
||||
</button>
|
||||
<button id="settingsBtn"
|
||||
<button id="settingsBtn"
|
||||
class="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white text-sm font-medium rounded-lg transition-colors">
|
||||
⚙️ Settings
|
||||
</button>
|
||||
<button id="autoTagBtn"
|
||||
class="px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white text-sm font-medium rounded-lg transition-colors">
|
||||
🏷️ Auto Tag
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -553,6 +557,243 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Auto Tag Modal -->
|
||||
<div id="autoTagModal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
|
||||
<div class="bg-gray-800 rounded-xl p-6 max-w-lg w-full mx-4 border border-gray-700">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="text-xl font-semibold text-gray-100">🏷️ Auto Tag Images</h3>
|
||||
<button onclick="hideModal('autoTagModal')" class="p-2 rounded-lg hover:bg-gray-700 transition-colors">
|
||||
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Model Selection -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-400 mb-3">Model Selection</label>
|
||||
<div class="space-y-3">
|
||||
<label class="flex items-center justify-between p-4 bg-gray-700 rounded-lg cursor-pointer hover:bg-gray-650 border border-gray-600">
|
||||
<div class="flex items-center">
|
||||
<input type="radio" name="autoTagModel" value="gguf" checked class="w-4 h-4 text-purple-600 bg-gray-700 border-gray-600 focus:ring-purple-500">
|
||||
<div class="ml-3">
|
||||
<span class="text-gray-100 font-medium">GGUF Model</span>
|
||||
<span class="text-gray-400 text-sm ml-2">(Recommended, ~4.5GB)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="ggufModelStatus" class="flex items-center space-x-2">
|
||||
<span class="text-gray-400 text-sm">Checking...</span>
|
||||
</div>
|
||||
</label>
|
||||
<label class="flex items-center justify-between p-4 bg-gray-700 rounded-lg cursor-pointer hover:bg-gray-650 border border-gray-600">
|
||||
<div class="flex items-center">
|
||||
<input type="radio" name="autoTagModel" value="hf" class="w-4 h-4 text-purple-600 bg-gray-700 border-gray-600 focus:ring-purple-500">
|
||||
<div class="ml-3">
|
||||
<span class="text-gray-100 font-medium">HuggingFace Model</span>
|
||||
<span class="text-gray-400 text-sm ml-2">(~16GB)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="hfModelStatus" class="flex items-center space-x-2">
|
||||
<span class="text-gray-400 text-sm">Checking...</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Prompt Template -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-400 mb-2">Tag Generation Prompt</label>
|
||||
<textarea id="autoTagPrompt" rows="6"
|
||||
class="w-full px-4 py-3 bg-gray-700 border border-gray-600 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 text-gray-100 text-sm font-mono resize-none custom-scrollbar"
|
||||
placeholder="Enter prompt template...">Generate only comma-separated Danbooru tags (lowercase_underscores, no spaces).
|
||||
Include: subject_count (1girl/1boy/etc), gender, ethnicity (asian, caucasian, etc), age_approx (teen, young_woman), body_attributes, face_features, hairstyle, appearance, clothing, accessories, pose, expression, action, camera_angle, camera_shot, scene_composition (wide_shot, long_shot, dutch_angle), environment, background_elements, lighting, mood.
|
||||
Prioritize visual accuracy.
|
||||
No sentences, no commentary, no captions. Only tags. Keep length short but descriptive.</textarea>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button onclick="hideModal('autoTagModal')"
|
||||
class="px-6 py-2 bg-gray-600 hover:bg-gray-700 text-white font-medium rounded-lg transition-colors">
|
||||
Cancel
|
||||
</button>
|
||||
<button id="startReviewBtn"
|
||||
class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors">
|
||||
👁️ Review
|
||||
</button>
|
||||
<button id="startAutoTagBtn"
|
||||
class="px-6 py-2 bg-purple-600 hover:bg-purple-700 text-white font-medium rounded-lg transition-colors">
|
||||
🏷️ Auto Tag
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Auto Tag Review Modal -->
|
||||
<div id="autoTagReviewModal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
|
||||
<div class="bg-gray-800 rounded-xl p-6 max-w-4xl w-full mx-4 border border-gray-700">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="text-xl font-semibold text-gray-100">
|
||||
👁️ Review Tags
|
||||
<span class="text-gray-400 text-base font-normal">(<span id="reviewCurrentIndex">1</span> of <span id="reviewTotalCount">0</span>)</span>
|
||||
</h3>
|
||||
<button onclick="window.gallery.cancelReview()" class="p-2 rounded-lg hover:bg-gray-700 transition-colors">
|
||||
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Image Preview -->
|
||||
<div class="bg-gray-900 rounded-lg p-4">
|
||||
<img id="reviewImage" src="" alt="Review image" class="w-full h-64 object-contain rounded">
|
||||
<p id="reviewImageName" class="text-gray-400 text-sm mt-2 text-center truncate">image_name.png</p>
|
||||
</div>
|
||||
|
||||
<!-- Generated Tags -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-400 mb-3">Generated Tags (click to remove)</label>
|
||||
<div id="reviewTagsContainer" class="flex flex-wrap gap-2 p-4 bg-gray-900 rounded-lg min-h-[200px] max-h-[250px] overflow-y-auto custom-scrollbar">
|
||||
<!-- Tag chips will be inserted here -->
|
||||
</div>
|
||||
<p class="text-gray-500 text-xs mt-2">Click the × on tags to remove them before applying</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex justify-between mt-6">
|
||||
<button id="skipReviewBtn"
|
||||
class="px-6 py-2 bg-gray-600 hover:bg-gray-700 text-white font-medium rounded-lg transition-colors">
|
||||
⏭️ Skip
|
||||
</button>
|
||||
<div class="flex space-x-3">
|
||||
<button onclick="window.gallery.cancelReview()"
|
||||
class="px-6 py-2 bg-red-600 hover:bg-red-700 text-white font-medium rounded-lg transition-colors">
|
||||
Cancel All
|
||||
</button>
|
||||
<button id="applyReviewBtn"
|
||||
class="px-6 py-2 bg-green-600 hover:bg-green-700 text-white font-medium rounded-lg transition-colors">
|
||||
✅ Apply & Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Auto Tag Progress Modal -->
|
||||
<div id="autoTagProgressModal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
|
||||
<div class="bg-gray-800 rounded-xl p-6 max-w-md w-full mx-4 border border-gray-700">
|
||||
<h3 class="text-xl font-semibold text-gray-100 mb-6">🏷️ Auto Tagging Progress</h3>
|
||||
|
||||
<!-- Current File -->
|
||||
<div class="mb-4">
|
||||
<p class="text-gray-400 text-sm">Processing:</p>
|
||||
<p id="autoTagCurrentFile" class="text-gray-100 font-medium truncate">Initializing...</p>
|
||||
</div>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<div class="mb-6">
|
||||
<div class="flex justify-between text-sm text-gray-400 mb-2">
|
||||
<span>Progress</span>
|
||||
<span id="autoTagProgressPercent">0%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-700 rounded-full h-3">
|
||||
<div id="autoTagProgressBar" class="bg-purple-600 h-3 rounded-full transition-all duration-300" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="grid grid-cols-3 gap-4 mb-6">
|
||||
<div class="bg-gray-700 rounded-lg p-3 text-center">
|
||||
<div class="text-2xl font-bold text-purple-400" id="autoTagProcessed">0</div>
|
||||
<div class="text-xs text-gray-400">Processed</div>
|
||||
</div>
|
||||
<div class="bg-gray-700 rounded-lg p-3 text-center">
|
||||
<div class="text-2xl font-bold text-green-400" id="autoTagApplied">0</div>
|
||||
<div class="text-xs text-gray-400">Tags Applied</div>
|
||||
</div>
|
||||
<div class="bg-gray-700 rounded-lg p-3 text-center">
|
||||
<div class="text-2xl font-bold text-yellow-400" id="autoTagSkipped">0</div>
|
||||
<div class="text-xs text-gray-400">Skipped</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cancel Button -->
|
||||
<div class="flex justify-center">
|
||||
<button id="cancelAutoTagBtn"
|
||||
class="px-6 py-2 bg-red-600 hover:bg-red-700 text-white font-medium rounded-lg transition-colors">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Auto Tag Download Modal -->
|
||||
<div id="autoTagDownloadModal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
|
||||
<div class="bg-gray-800 rounded-xl p-6 max-w-md w-full mx-4 border border-gray-700">
|
||||
<h3 class="text-xl font-semibold text-gray-100 mb-6">📥 Downloading Model</h3>
|
||||
|
||||
<!-- Model Name -->
|
||||
<div class="mb-4">
|
||||
<p class="text-gray-400 text-sm">Downloading:</p>
|
||||
<p id="downloadModelName" class="text-gray-100 font-medium">GGUF Model</p>
|
||||
</div>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<div class="mb-6">
|
||||
<div class="flex justify-between text-sm text-gray-400 mb-2">
|
||||
<span id="downloadStatus">Preparing...</span>
|
||||
<span id="downloadProgressPercent">0%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-700 rounded-full h-3">
|
||||
<div id="downloadProgressBar" class="bg-blue-600 h-3 rounded-full transition-all duration-300" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cancel Button -->
|
||||
<div class="flex justify-center">
|
||||
<button id="cancelDownloadBtn"
|
||||
class="px-6 py-2 bg-red-600 hover:bg-red-700 text-white font-medium rounded-lg transition-colors">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Tag chip styles for Auto Tag feature */
|
||||
.tag-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background-color: #374151;
|
||||
border: 1px solid #4B5563;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
color: #D1D5DB;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.tag-chip:hover {
|
||||
background-color: #4B5563;
|
||||
}
|
||||
.tag-chip .tag-remove {
|
||||
margin-left: 0.375rem;
|
||||
padding: 0.125rem;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
color: #9CA3AF;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.tag-chip .tag-remove:hover {
|
||||
color: #EF4444;
|
||||
background-color: rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
.hover\:bg-gray-650:hover {
|
||||
background-color: #525252;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
class GalleryManager {
|
||||
constructor() {
|
||||
@@ -576,6 +817,7 @@
|
||||
document.getElementById('gridViewBtn').addEventListener('click', () => this.setViewMode('grid'));
|
||||
document.getElementById('listViewBtn').addEventListener('click', () => this.setViewMode('list'));
|
||||
document.getElementById('settingsBtn').addEventListener('click', () => this.showSettings());
|
||||
document.getElementById('autoTagBtn').addEventListener('click', () => this.showAutoTagModal());
|
||||
document.getElementById('retryBtn').addEventListener('click', () => this.loadImages());
|
||||
document.getElementById('prevPageBtn').addEventListener('click', () => this.previousPage());
|
||||
document.getElementById('nextPageBtn').addEventListener('click', () => this.nextPage());
|
||||
@@ -612,6 +854,23 @@
|
||||
this.hideDuplicatesModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Auto Tag modal event listeners
|
||||
document.getElementById('startAutoTagBtn').addEventListener('click', () => this.startAutoTag());
|
||||
document.getElementById('startReviewBtn').addEventListener('click', () => this.startReview());
|
||||
document.getElementById('skipReviewBtn').addEventListener('click', () => this.skipReviewImage());
|
||||
document.getElementById('applyReviewBtn').addEventListener('click', () => this.applyReviewTags());
|
||||
document.getElementById('cancelAutoTagBtn').addEventListener('click', () => this.cancelAutoTag());
|
||||
document.getElementById('cancelDownloadBtn').addEventListener('click', () => this.cancelDownload());
|
||||
|
||||
// Close AutoTag modals on outside click
|
||||
['autoTagModal', 'autoTagReviewModal', 'autoTagProgressModal', 'autoTagDownloadModal'].forEach(modalId => {
|
||||
document.getElementById(modalId).addEventListener('click', (e) => {
|
||||
if (e.target.id === modalId) {
|
||||
this.hideModal(modalId);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async loadImages() {
|
||||
@@ -3273,22 +3532,404 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`;
|
||||
addStatusMessage('Connection to server lost', 'error');
|
||||
progressBar.classList.add('bg-red-600');
|
||||
progressBar.classList.remove('bg-violet-600');
|
||||
|
||||
|
||||
// Clean up
|
||||
eventSource.close();
|
||||
|
||||
|
||||
if (!resultData) {
|
||||
reject(new Error('Connection to server lost during thumbnail generation'));
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Auto Tag Feature Methods
|
||||
// ==========================================
|
||||
|
||||
// State for AutoTag feature
|
||||
autoTagState = {
|
||||
eventSource: null,
|
||||
downloadEventSource: null,
|
||||
reviewImages: [],
|
||||
reviewIndex: 0,
|
||||
currentTags: [],
|
||||
cancelled: false
|
||||
};
|
||||
|
||||
showModal(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
hideModal(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
async showAutoTagModal() {
|
||||
this.showModal("autoTagModal");
|
||||
await this.checkAutoTagModels();
|
||||
}
|
||||
|
||||
async checkAutoTagModels() {
|
||||
const ggufStatus = document.getElementById("ggufModelStatus");
|
||||
const hfStatus = document.getElementById("hfModelStatus");
|
||||
|
||||
ggufStatus.innerHTML = '<span class="text-gray-400 text-sm">Checking...</span>';
|
||||
hfStatus.innerHTML = '<span class="text-gray-400 text-sm">Checking...</span>';
|
||||
|
||||
try {
|
||||
const response = await fetch('/prompt_manager/autotag/models');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// Update GGUF status
|
||||
if (data.models.gguf.downloaded) {
|
||||
ggufStatus.innerHTML = '<span class="text-green-400 text-sm">✓ Downloaded</span>';
|
||||
} else {
|
||||
ggufStatus.innerHTML = `<button onclick="window.gallery.downloadModel('gguf')" class="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white text-xs rounded transition-colors">Download</button>`;
|
||||
}
|
||||
|
||||
// Update HF status
|
||||
if (data.models.hf.downloaded) {
|
||||
hfStatus.innerHTML = '<span class="text-green-400 text-sm">✓ Downloaded</span>';
|
||||
} else {
|
||||
hfStatus.innerHTML = `<button onclick="window.gallery.downloadModel('hf')" class="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white text-xs rounded transition-colors">Download</button>`;
|
||||
}
|
||||
} else {
|
||||
ggufStatus.innerHTML = '<span class="text-red-400 text-sm">Error</span>';
|
||||
hfStatus.innerHTML = '<span class="text-red-400 text-sm">Error</span>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking models:', error);
|
||||
ggufStatus.innerHTML = '<span class="text-red-400 text-sm">Error</span>';
|
||||
hfStatus.innerHTML = '<span class="text-red-400 text-sm">Error</span>';
|
||||
}
|
||||
}
|
||||
|
||||
async downloadModel(modelType) {
|
||||
const modelName = modelType === 'gguf' ? 'GGUF Model' : 'HuggingFace Model';
|
||||
document.getElementById('downloadModelName').textContent = modelName;
|
||||
document.getElementById('downloadStatus').textContent = 'Preparing...';
|
||||
document.getElementById('downloadProgressPercent').textContent = '0%';
|
||||
document.getElementById('downloadProgressBar').style.width = '0%';
|
||||
|
||||
this.hideModal("autoTagModal");
|
||||
this.showModal("autoTagDownloadModal");
|
||||
|
||||
try {
|
||||
this.autoTagState.downloadEventSource = new EventSource(`/prompt_manager/autotag/download/${modelType}`);
|
||||
|
||||
this.autoTagState.downloadEventSource.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
if (data.status === 'downloading') {
|
||||
const percent = Math.round(data.progress * 100);
|
||||
document.getElementById('downloadStatus').textContent = data.message || 'Downloading...';
|
||||
document.getElementById('downloadProgressPercent').textContent = `${percent}%`;
|
||||
document.getElementById('downloadProgressBar').style.width = `${percent}%`;
|
||||
} else if (data.status === 'complete') {
|
||||
this.autoTagState.downloadEventSource.close();
|
||||
this.hideModal("autoTagDownloadModal");
|
||||
this.showModal("autoTagModal");
|
||||
this.checkAutoTagModels();
|
||||
this.showNotification('Model downloaded successfully!', 'success');
|
||||
} else if (data.status === 'error') {
|
||||
this.autoTagState.downloadEventSource.close();
|
||||
this.hideModal("autoTagDownloadModal");
|
||||
this.showModal("autoTagModal");
|
||||
this.showNotification(`Download failed: ${data.message}`, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
this.autoTagState.downloadEventSource.onerror = () => {
|
||||
this.autoTagState.downloadEventSource.close();
|
||||
this.hideModal("autoTagDownloadModal");
|
||||
this.showModal("autoTagModal");
|
||||
this.showNotification('Download connection lost', 'error');
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Download error:', error);
|
||||
this.hideModal("autoTagDownloadModal");
|
||||
this.showModal("autoTagModal");
|
||||
this.showNotification('Failed to start download', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
cancelDownload() {
|
||||
if (this.autoTagState.downloadEventSource) {
|
||||
this.autoTagState.downloadEventSource.close();
|
||||
this.autoTagState.downloadEventSource = null;
|
||||
}
|
||||
this.hideModal("autoTagDownloadModal");
|
||||
this.showModal("autoTagModal");
|
||||
}
|
||||
|
||||
async startAutoTag() {
|
||||
const modelType = document.querySelector('input[name="autoTagModel"]:checked').value;
|
||||
const prompt = document.getElementById('autoTagPrompt').value;
|
||||
|
||||
// Check if model is downloaded
|
||||
const response = await fetch('/prompt_manager/autotag/models');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && !data.models[modelType].downloaded) {
|
||||
this.showNotification('Please download the selected model first', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
this.hideModal("autoTagModal");
|
||||
this.showModal("autoTagProgressModal");
|
||||
|
||||
// Reset progress display
|
||||
document.getElementById('autoTagCurrentFile').textContent = 'Loading model...';
|
||||
document.getElementById('autoTagProgressPercent').textContent = '0%';
|
||||
document.getElementById('autoTagProgressBar').style.width = '0%';
|
||||
document.getElementById('autoTagProcessed').textContent = '0';
|
||||
document.getElementById('autoTagApplied').textContent = '0';
|
||||
document.getElementById('autoTagSkipped').textContent = '0';
|
||||
|
||||
this.autoTagState.cancelled = false;
|
||||
|
||||
try {
|
||||
const formData = new URLSearchParams();
|
||||
formData.append('model_type', modelType);
|
||||
formData.append('prompt', prompt);
|
||||
|
||||
this.autoTagState.eventSource = new EventSource(`/prompt_manager/autotag/start?${formData.toString()}`);
|
||||
|
||||
this.autoTagState.eventSource.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
if (data.status === 'loading_model') {
|
||||
document.getElementById('autoTagCurrentFile').textContent = 'Loading model...';
|
||||
} else if (data.status === 'processing') {
|
||||
const percent = Math.round((data.processed / data.total) * 100);
|
||||
document.getElementById('autoTagCurrentFile').textContent = data.current_file || 'Processing...';
|
||||
document.getElementById('autoTagProgressPercent').textContent = `${percent}%`;
|
||||
document.getElementById('autoTagProgressBar').style.width = `${percent}%`;
|
||||
document.getElementById('autoTagProcessed').textContent = data.processed;
|
||||
document.getElementById('autoTagApplied').textContent = data.tags_applied;
|
||||
document.getElementById('autoTagSkipped').textContent = data.skipped;
|
||||
} else if (data.status === 'complete') {
|
||||
this.autoTagState.eventSource.close();
|
||||
this.hideModal("autoTagProgressModal");
|
||||
this.showNotification(`Auto tagging complete! Applied tags to ${data.tags_applied} prompts.`, 'success');
|
||||
this.loadImages(); // Refresh the gallery
|
||||
} else if (data.status === 'error') {
|
||||
this.autoTagState.eventSource.close();
|
||||
this.hideModal("autoTagProgressModal");
|
||||
this.showNotification(`Auto tag error: ${data.message}`, 'error');
|
||||
} else if (data.status === 'cancelled') {
|
||||
this.autoTagState.eventSource.close();
|
||||
this.hideModal("autoTagProgressModal");
|
||||
this.showNotification('Auto tagging cancelled', 'info');
|
||||
}
|
||||
};
|
||||
|
||||
this.autoTagState.eventSource.onerror = () => {
|
||||
this.autoTagState.eventSource.close();
|
||||
this.hideModal("autoTagProgressModal");
|
||||
this.showNotification('Connection lost during auto tagging', 'error');
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Auto tag error:', error);
|
||||
this.hideModal("autoTagProgressModal");
|
||||
this.showNotification('Failed to start auto tagging', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
cancelAutoTag() {
|
||||
this.autoTagState.cancelled = true;
|
||||
if (this.autoTagState.eventSource) {
|
||||
this.autoTagState.eventSource.close();
|
||||
this.autoTagState.eventSource = null;
|
||||
}
|
||||
this.hideModal("autoTagProgressModal");
|
||||
this.showNotification('Auto tagging cancelled', 'info');
|
||||
}
|
||||
|
||||
async startReview() {
|
||||
const modelType = document.querySelector('input[name="autoTagModel"]:checked').value;
|
||||
|
||||
// Check if model is downloaded
|
||||
const response = await fetch('/prompt_manager/autotag/models');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && !data.models[modelType].downloaded) {
|
||||
this.showNotification('Please download the selected model first', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
this.hideModal("autoTagModal");
|
||||
|
||||
// Use current gallery images for review
|
||||
if (!this.images || this.images.length === 0) {
|
||||
this.showNotification('No images found in gallery', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
this.autoTagState.reviewImages = this.images;
|
||||
this.autoTagState.reviewIndex = 0;
|
||||
this.autoTagState.modelType = modelType;
|
||||
this.autoTagState.prompt = document.getElementById('autoTagPrompt').value;
|
||||
|
||||
document.getElementById('reviewTotalCount').textContent = this.images.length;
|
||||
|
||||
this.showModal("autoTagReviewModal");
|
||||
await this.loadNextReviewImage();
|
||||
}
|
||||
|
||||
async loadNextReviewImage() {
|
||||
if (this.autoTagState.reviewIndex >= this.autoTagState.reviewImages.length) {
|
||||
this.hideModal("autoTagReviewModal");
|
||||
this.showNotification('Review complete!', 'success');
|
||||
this.loadImages();
|
||||
return;
|
||||
}
|
||||
|
||||
const image = this.autoTagState.reviewImages[this.autoTagState.reviewIndex];
|
||||
document.getElementById('reviewCurrentIndex').textContent = this.autoTagState.reviewIndex + 1;
|
||||
document.getElementById('reviewImage').src = image.thumbnail_url || image.url;
|
||||
document.getElementById('reviewImageName').textContent = image.filename || 'Unknown';
|
||||
document.getElementById('reviewTagsContainer').innerHTML = '<div class="text-gray-400">Generating tags...</div>';
|
||||
|
||||
try {
|
||||
const response = await fetch('/prompt_manager/autotag/single', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
image_path: image.path,
|
||||
model_type: this.autoTagState.modelType,
|
||||
prompt: this.autoTagState.prompt
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.autoTagState.currentTags = data.tags;
|
||||
this.renderReviewTags();
|
||||
} else {
|
||||
document.getElementById('reviewTagsContainer').innerHTML =
|
||||
`<div class="text-red-400">Error: ${data.error}</div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating tags:', error);
|
||||
document.getElementById('reviewTagsContainer').innerHTML =
|
||||
'<div class="text-red-400">Failed to generate tags</div>';
|
||||
}
|
||||
}
|
||||
|
||||
renderReviewTags() {
|
||||
const container = document.getElementById('reviewTagsContainer');
|
||||
if (this.autoTagState.currentTags.length === 0) {
|
||||
container.innerHTML = '<div class="text-gray-400">No tags generated</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = this.autoTagState.currentTags.map((tag, index) => `
|
||||
<span class="tag-chip">
|
||||
${this.escapeHtml(tag)}
|
||||
<span class="tag-remove" onclick="window.gallery.removeReviewTag(${index})">×</span>
|
||||
</span>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
removeReviewTag(index) {
|
||||
this.autoTagState.currentTags.splice(index, 1);
|
||||
this.renderReviewTags();
|
||||
}
|
||||
|
||||
skipReviewImage() {
|
||||
this.autoTagState.reviewIndex++;
|
||||
this.loadNextReviewImage();
|
||||
}
|
||||
|
||||
async applyReviewTags() {
|
||||
if (this.autoTagState.currentTags.length === 0) {
|
||||
this.skipReviewImage();
|
||||
return;
|
||||
}
|
||||
|
||||
const image = this.autoTagState.reviewImages[this.autoTagState.reviewIndex];
|
||||
|
||||
try {
|
||||
const response = await fetch('/prompt_manager/autotag/apply', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
image_path: image.path,
|
||||
tags: this.autoTagState.currentTags
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.showNotification(`Applied ${this.autoTagState.currentTags.length} tags`, 'success');
|
||||
} else {
|
||||
this.showNotification(data.error || 'Failed to apply tags', 'warning');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error applying tags:', error);
|
||||
this.showNotification('Failed to apply tags', 'error');
|
||||
}
|
||||
|
||||
this.autoTagState.reviewIndex++;
|
||||
this.loadNextReviewImage();
|
||||
}
|
||||
|
||||
cancelReview() {
|
||||
this.autoTagState.reviewImages = [];
|
||||
this.autoTagState.reviewIndex = 0;
|
||||
this.autoTagState.currentTags = [];
|
||||
this.hideModal("autoTagReviewModal");
|
||||
}
|
||||
|
||||
showNotification(message, type = 'info') {
|
||||
// Simple notification - could be enhanced with a toast library
|
||||
const colors = {
|
||||
success: 'bg-green-600',
|
||||
error: 'bg-red-600',
|
||||
warning: 'bg-yellow-600',
|
||||
info: 'bg-blue-600'
|
||||
};
|
||||
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `fixed bottom-4 right-4 ${colors[type]} text-white px-6 py-3 rounded-lg shadow-lg z-50 animate-fade-in`;
|
||||
notification.textContent = message;
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize gallery when page loads
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new GalleryManager();
|
||||
window.gallery = new GalleryManager();
|
||||
});
|
||||
|
||||
// Global helper for modal close buttons
|
||||
function hideModal(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user