Enhance image processing and progress tracking in ImageQueueModal; update docker-compose for read-write access

This commit is contained in:
IGNY8 VPS (Salman)
2025-11-12 08:39:03 +00:00
parent e1a82c3615
commit 9f704313fb
4 changed files with 217 additions and 70 deletions

View File

@@ -1041,6 +1041,13 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
'phase': meta.get('phase', 'processing') if isinstance(meta, dict) else 'processing',
'current_item': meta.get('current_item') if isinstance(meta, dict) else None,
'completed': meta.get('completed', 0) if isinstance(meta, dict) else 0,
# Image generation progress fields
'current_image': meta.get('current_image') if isinstance(meta, dict) else None,
'current_image_id': meta.get('current_image_id') if isinstance(meta, dict) else None,
'current_image_progress': meta.get('current_image_progress') if isinstance(meta, dict) else None,
'total_images': meta.get('total_images') if isinstance(meta, dict) else None,
'failed': meta.get('failed', 0) if isinstance(meta, dict) else 0,
'results': meta.get('results', []) if isinstance(meta, dict) else [],
}
# Include step logs if available
if isinstance(meta, dict):

View File

@@ -96,7 +96,7 @@ services:
DEBUG: "False"
volumes:
- /data/app/igny8/backend:/app:rw
- /data/app/igny8:/data/app/igny8:ro
- /data/app/igny8:/data/app/igny8:rw
- /data/app/logs:/app/logs:rw
# Note: postgres and redis are external services from infra stack
# Ensure they're running before starting this stack

View File

@@ -16,6 +16,7 @@ export interface ImageQueueItem {
type: 'featured' | 'in_article';
position?: number;
contentTitle: string;
prompt?: string; // Image prompt text
status: 'pending' | 'processing' | 'completed' | 'failed';
progress: number;
imageUrl: string | null;
@@ -67,55 +68,147 @@ export default function ImageQueueModal({
}
}, [localQueue, onUpdateQueue]);
// Smooth progress animation (like reference plugin)
// Time-based progress: 1% → 50% in 5s, then 2% every 200ms until 80%, then event-based to 100%
useEffect(() => {
// Start smooth progress for items that are processing
localQueue.forEach((item) => {
if (item.status === 'processing' && item.progress < 95) {
// Only start animation if not already running
if (!progressIntervalsRef.current[item.index]) {
// Start from current progress or 0
let currentProgress = smoothProgress[item.index] || item.progress || 0;
let phase = currentProgress < 50 ? 1 : currentProgress < 75 ? 2 : 3;
let phaseStartTime = Date.now();
const interval = setInterval(() => {
const elapsed = Date.now() - phaseStartTime;
if (phase === 1 && currentProgress < 50) {
// Phase 1: 0% to 50% in 7 seconds (7.14% per second)
currentProgress += 0.714;
if (currentProgress >= 50 || elapsed >= 7000) {
currentProgress = 50;
phase = 2;
phaseStartTime = Date.now();
}
} else if (phase === 2 && currentProgress < 75) {
// Phase 2: 50% to 75% in 5 seconds (5% per second)
currentProgress += 0.5;
if (currentProgress >= 75 || elapsed >= 5000) {
currentProgress = 75;
phase = 3;
phaseStartTime = Date.now();
}
} else if (phase === 3 && currentProgress < 95) {
// Phase 3: 75% to 95% - 5% every second
if (elapsed >= 1000) {
currentProgress = Math.min(95, currentProgress + 5);
phaseStartTime = Date.now();
}
// Helper function for Phase 2: 2% every 200ms until 80%
const startPhase2Animation = (index: number, startProgress: number) => {
if (progressIntervalsRef.current[index]) {
clearInterval(progressIntervalsRef.current[index]);
delete progressIntervalsRef.current[index];
}
let currentPercent = Math.max(startProgress, 50);
const interval = setInterval(() => {
setSmoothProgress(prev => {
const current = prev[index] ?? currentPercent;
// Only increment if still below 80% and status is processing or pending
const item = localQueue.find(item => item.index === index);
if (current < 80 && (item?.status === 'processing' || item?.status === 'pending')) {
const newPercentage = Math.min(current + 2, 80); // 2% every 200ms
currentPercent = newPercentage;
// Stop if we've reached 80%
if (newPercentage >= 80) {
clearInterval(interval);
delete progressIntervalsRef.current[index];
}
setSmoothProgress(prev => ({
return {
...prev,
[item.index]: Math.round(currentProgress)
}));
}, 100);
[index]: newPercentage
};
} else {
// Stop if status changed or reached 80%
clearInterval(interval);
delete progressIntervalsRef.current[index];
return prev;
}
});
}, 200); // 2% every 200ms
progressIntervalsRef.current[index] = interval;
};
localQueue.forEach((item) => {
// Apply progress logic to all items that are processing OR pending (waiting in queue)
// This ensures all images start progress animation, not just the currently processing one
if (item.status === 'processing' || item.status === 'pending') {
// Initialize smoothProgress if not set (start at 1% for new items)
const currentProgress = smoothProgress[item.index] ?? (item.progress > 0 ? item.progress : 1);
// Phase 1: 1% → 50% at 100ms per 1% (regardless of events)
// Only start if we're below 50% and no interval is already running
if (currentProgress < 50 && !progressIntervalsRef.current[item.index]) {
let currentPercent = Math.max(currentProgress, 1); // Start from 1% or current if higher
const interval = setInterval(() => {
setSmoothProgress(prev => {
const current = prev[item.index] ?? currentPercent;
// Only increment if still below 50% and status is processing or pending
if (current < 50 && (item.status === 'processing' || item.status === 'pending')) {
const newPercentage = Math.min(current + 1, 50); // 1% every 100ms
currentPercent = newPercentage;
// Stop if we've reached 50%
if (newPercentage >= 50) {
clearInterval(interval);
delete progressIntervalsRef.current[item.index];
// Start Phase 2: 2% every 200ms until 80%
if (newPercentage < 80) {
startPhase2Animation(item.index, newPercentage);
}
}
return {
...prev,
[item.index]: newPercentage
};
} else {
// Stop if status changed or reached 50%
clearInterval(interval);
delete progressIntervalsRef.current[item.index];
return prev;
}
});
}, 200); // 1% every 100ms
progressIntervalsRef.current[item.index] = interval;
}
} else {
// Stop animation if item is no longer processing
// Phase 2: 2% every 200ms until 80%
else if (currentProgress >= 50 && currentProgress < 80 && !progressIntervalsRef.current[item.index]) {
startPhase2Animation(item.index, currentProgress);
}
// Phase 3: Event-based smooth completion to 100% (after image is downloaded and shown)
// Check if image is ready (has imageUrl) even if still processing
else if (item.imageUrl && currentProgress < 100 && !progressIntervalsRef.current[item.index]) {
// Stop any existing animation
if (progressIntervalsRef.current[item.index]) {
clearInterval(progressIntervalsRef.current[item.index]);
delete progressIntervalsRef.current[item.index];
}
// Smoothly complete to 100%
if (!progressIntervalsRef.current[item.index]) {
let animatingProgress = Math.max(currentProgress, 80); // Start from 80% or current if higher
const startProgress = animatingProgress;
const endProgress = 100;
const duration = 800; // 500ms
const startTime = Date.now();
const interval = setInterval(() => {
const elapsed = Date.now() - startTime;
const progress = Math.min(1, elapsed / duration);
// Ease-out quadratic
const eased = 1 - Math.pow(1 - progress, 2);
animatingProgress = startProgress + ((endProgress - startProgress) * eased);
if (animatingProgress >= 100 || elapsed >= duration) {
animatingProgress = 100;
clearInterval(interval);
delete progressIntervalsRef.current[item.index];
}
setSmoothProgress(prev => ({
...prev,
[item.index]: Math.round(animatingProgress)
}));
}, 16);
progressIntervalsRef.current[item.index] = interval;
}
}
}
});
// Handle non-processing items
localQueue.forEach((item) => {
if (item.status !== 'processing') {
// Stop animation for completed/failed items
if (progressIntervalsRef.current[item.index]) {
clearInterval(progressIntervalsRef.current[item.index]);
delete progressIntervalsRef.current[item.index];
@@ -130,7 +223,7 @@ export default function ImageQueueModal({
}
}
});
// Cleanup on unmount
return () => {
Object.values(progressIntervalsRef.current).forEach(interval => clearInterval(interval));
@@ -256,6 +349,28 @@ export default function ImageQueueModal({
return () => clearInterval(pollInterval);
}, [isOpen, taskId]);
// Helper function to convert image_path to frontend-accessible URL
const getImageUrlFromPath = (imagePath: string | null | undefined): string | null => {
if (!imagePath) return null;
// If path contains 'ai-images', extract filename and convert to web URL
if (imagePath.includes('ai-images')) {
const filename = imagePath.split('ai-images/')[1] || imagePath.split('ai-images\\')[1];
if (filename) {
return `/images/ai-images/${filename}`;
}
}
// If path is already a web path, return as-is
if (imagePath.startsWith('/images/')) {
return imagePath;
}
// Otherwise, try to extract filename and use ai-images path
const filename = imagePath.split('/').pop() || imagePath.split('\\').pop();
return filename ? `/images/ai-images/${filename}` : null;
};
const updateQueueFromTaskMeta = (meta: any) => {
const { current_image, total_images, completed, failed, results, current_image_progress, current_image_id } = meta;
@@ -272,20 +387,25 @@ export default function ImageQueueModal({
}
}
// Only set imageUrl if image is completed AND image_path exists (image is saved)
const imageUrl = (result.status === 'completed' && result.image_path)
? getImageUrlFromPath(result.image_path)
: null;
// Use backend progress as target (0%, 50%, 75%, 90%, 100%)
// This works for both featured and in-article images
const backendProgress = (current_image_id === item.imageId && current_image_progress !== undefined)
? current_image_progress
: (result.status === 'completed' ? 100 :
result.status === 'failed' ? 0 :
item.progress);
return {
...item,
status: result.status === 'completed' ? 'completed' :
result.status === 'failed' ? 'failed' : 'processing',
progress: result.status === 'completed' ? 100 :
result.status === 'failed' ? 0 :
// Use current_image_progress if this is the current image being processed
// Otherwise use smooth progress animation
(current_image_id === item.imageId && current_image_progress !== undefined) ? current_image_progress :
index + 1 < current_image ? 100 :
index + 1 === current_image ? (smoothProgress[item.index] || 0) : 0,
imageUrl: result.image_path
? `/api/v1/writer/images/${item.imageId}/file/`
: (result.image_url || item.imageUrl),
progress: backendProgress, // Use backend progress as target for smooth animation
imageUrl: imageUrl || item.imageUrl, // Only update if we have a new URL, otherwise keep existing
error: result.error || null
};
}
@@ -299,11 +419,11 @@ export default function ImageQueueModal({
}
return { ...item, status: 'completed', progress: 100 };
} else if (index + 1 === current_image || current_image_id === item.imageId) {
// Currently processing - use current_image_progress if available, otherwise smooth progress
const progress = (current_image_progress !== undefined && current_image_id === item.imageId)
// Currently processing - use backend progress if available (works for featured and in-article)
const backendProgress = (current_image_progress !== undefined && current_image_id === item.imageId)
? current_image_progress
: (smoothProgress[item.index] || 0);
return { ...item, status: 'processing', progress };
: (smoothProgress[item.index] ?? 0);
return { ...item, status: 'processing', progress: backendProgress };
}
return item;
@@ -325,13 +445,16 @@ export default function ImageQueueModal({
delete progressIntervalsRef.current[item.index];
}
// Only set imageUrl if image is completed AND image_path exists (image is saved)
const imageUrl = (taskResult.status === 'completed' && taskResult.image_path)
? getImageUrlFromPath(taskResult.image_path)
: null;
return {
...item,
status: taskResult.status === 'completed' ? 'completed' : 'failed',
progress: taskResult.status === 'completed' ? 100 : 0,
imageUrl: taskResult.image_path
? `/api/v1/writer/images/${item.imageId}/file/`
: (taskResult.image_url || item.imageUrl),
imageUrl: imageUrl || item.imageUrl, // Only update if we have a new URL, otherwise keep existing
error: taskResult.error || null
};
}
@@ -451,7 +574,13 @@ export default function ImageQueueModal({
{item.label}
</span>
<span className="flex-1 text-xs text-gray-500 dark:text-gray-400 truncate">
{item.contentTitle}
{item.prompt
? (() => {
const words = item.prompt.split(' ');
const firstTenWords = words.slice(0, 10).join(' ');
return words.length > 10 ? `${firstTenWords}...` : item.prompt;
})()
: item.contentTitle}
</span>
<span className="text-xs font-semibold text-gray-600 dark:text-gray-300 whitespace-nowrap flex items-center gap-1">
{getStatusIcon(item.status)}
@@ -459,15 +588,15 @@ export default function ImageQueueModal({
</span>
</div>
{/* Progress Bar */}
{/* Progress Bar - Use smooth progress for visual display */}
<div className="relative h-5 bg-gray-200 dark:bg-gray-600 rounded-full overflow-hidden">
<div
className={`h-full ${getProgressColor(item.status)} transition-all duration-300 ease-out`}
style={{ width: `${item.progress}%` }}
className={`h-full ${getProgressColor(item.status)} transition-all duration-150 ease-out`}
style={{ width: `${smoothProgress[item.index] ?? item.progress ?? 0}%` }}
/>
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-xs font-bold text-gray-700 dark:text-gray-200">
{item.progress}%
{smoothProgress[item.index] ?? item.progress ?? 0}%
</span>
</div>
</div>
@@ -480,13 +609,22 @@ export default function ImageQueueModal({
)}
</div>
{/* Right: Thumbnail */}
{/* Right: Thumbnail - Only show if image is completed and saved */}
<div className="w-20 h-20 bg-gray-100 dark:bg-gray-700 rounded-lg overflow-hidden flex-shrink-0 flex items-center justify-center">
{item.imageUrl ? (
{item.status === 'completed' && item.imageUrl ? (
<img
src={item.imageUrl}
alt={item.label}
className="w-full h-full object-cover"
onError={(e) => {
// Hide image if it fails to load (not yet available)
const target = e.target as HTMLImageElement;
target.style.display = 'none';
const parent = target.parentElement;
if (parent) {
parent.innerHTML = '<span class="text-xs text-gray-400 dark:text-gray-500">No image</span>';
}
}}
/>
) : (
<span className="text-xs text-gray-400 dark:text-gray-500">

View File

@@ -229,6 +229,7 @@ export default function Images() {
label: 'Featured Image',
type: 'featured',
contentTitle: contentImages.content_title || `Content #${contentId}`,
prompt: contentImages.featured_image.prompt,
status: 'pending',
progress: 0,
imageUrl: null,
@@ -250,6 +251,7 @@ export default function Images() {
type: 'in_article',
position: img.position || idx + 1,
contentTitle: contentImages.content_title || `Content #${contentId}`,
prompt: img.prompt,
status: 'pending',
progress: 0,
imageUrl: null,