Fix: Complete image upload and display system with error handling
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 230 KiB |
@@ -44,7 +44,27 @@ app.add_middleware(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Mount static files for serving images
|
# Mount static files for serving images
|
||||||
app.mount("/static", StaticFiles(directory="data"), name="static")
|
app.mount("/static", StaticFiles(directory="../../data"), name="static")
|
||||||
|
|
||||||
|
# Create uploads directory for temporary image storage
|
||||||
|
uploads_dir = "uploads"
|
||||||
|
os.makedirs(uploads_dir, exist_ok=True)
|
||||||
|
app.mount("/uploads", StaticFiles(directory=uploads_dir), name="uploads")
|
||||||
|
|
||||||
|
def cleanup_old_uploads():
|
||||||
|
"""Clean up uploaded files older than 1 hour"""
|
||||||
|
try:
|
||||||
|
import time
|
||||||
|
current_time = time.time()
|
||||||
|
for filename in os.listdir(uploads_dir):
|
||||||
|
file_path = os.path.join(uploads_dir, filename)
|
||||||
|
if os.path.isfile(file_path):
|
||||||
|
# Remove files older than 1 hour (3600 seconds)
|
||||||
|
if current_time - os.path.getctime(file_path) > 3600:
|
||||||
|
os.remove(file_path)
|
||||||
|
print(f"Cleaned up old upload: {filename}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error during cleanup: {e}")
|
||||||
|
|
||||||
# Global components (initialized on startup)
|
# Global components (initialized on startup)
|
||||||
image_processor = None
|
image_processor = None
|
||||||
@@ -126,6 +146,7 @@ async def root():
|
|||||||
.loading { display: none; text-align: center; margin: 20px 0; }
|
.loading { display: none; text-align: center; margin: 20px 0; }
|
||||||
.status { padding: 10px; border-radius: 5px; margin: 10px 0; }
|
.status { padding: 10px; border-radius: 5px; margin: 10px 0; }
|
||||||
.status.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
|
.status.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
|
||||||
|
.status.warning { background: #fff3cd; color: #856404; border: 1px solid #ffeaa7; }
|
||||||
.status.error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
|
.status.error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
|
||||||
.demo-section { margin: 30px 0; padding: 20px; background: #e8f5e8; border-radius: 8px; }
|
.demo-section { margin: 30px 0; padding: 20px; background: #e8f5e8; border-radius: 8px; }
|
||||||
.api-docs { margin: 20px 0; }
|
.api-docs { margin: 20px 0; }
|
||||||
@@ -230,16 +251,34 @@ async def root():
|
|||||||
|
|
||||||
let html = `
|
let html = `
|
||||||
<h3>📊 Processing Results</h3>
|
<h3>📊 Processing Results</h3>
|
||||||
<div class="status success">
|
|
||||||
✅ Processed ${data.successful}/${data.total_images} images successfully<br>
|
|
||||||
⏱️ Total time: ${data.total_processing_time.toFixed(1)}s<br>
|
|
||||||
🎯 Average quality: ${data.average_quality.toFixed(1)}/100
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
if (data.successful === 0 && data.failed > 0) {
|
||||||
|
html += `
|
||||||
|
<div class="status error">
|
||||||
|
❌ Failed to process ${data.failed} image(s)<br>
|
||||||
|
💡 <strong>Tips:</strong><br>
|
||||||
|
• Make sure you're uploading valid image files (JPG, PNG, GIF, etc.)<br>
|
||||||
|
• Try converting your image to JPG format<br>
|
||||||
|
• Check that the file isn't corrupted<br>
|
||||||
|
• Supported formats: JPEG, PNG, GIF, BMP, TIFF
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
html += `
|
||||||
|
<div class="status ${data.failed > 0 ? 'warning' : 'success'}">
|
||||||
|
✅ Processed ${data.successful}/${data.total_images} images successfully<br>
|
||||||
|
${data.failed > 0 ? `⚠️ ${data.failed} image(s) failed to process<br>` : ''}
|
||||||
|
⏱️ Total time: ${(data.total_processing_time || 0).toFixed(1)}s<br>
|
||||||
|
🎯 Average quality: ${(data.average_quality || 0).toFixed(1)}/100
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
data.results.forEach((result, index) => {
|
data.results.forEach((result, index) => {
|
||||||
const qualityClass = result.quality_score >= 70 ? 'quality-high' :
|
const qualityScore = result.quality_score || 0;
|
||||||
result.quality_score >= 50 ? 'quality-medium' : 'quality-low';
|
const qualityClass = qualityScore >= 70 ? 'quality-high' :
|
||||||
|
qualityScore >= 50 ? 'quality-medium' : 'quality-low';
|
||||||
|
|
||||||
// Create image URL for sample images or uploaded images
|
// Create image URL for sample images or uploaded images
|
||||||
const imageUrl = result.image_url || `/static/working_images/${result.filename}`;
|
const imageUrl = result.image_url || `/static/working_images/${result.filename}`;
|
||||||
@@ -248,9 +287,10 @@ async def root():
|
|||||||
<div class="result-card">
|
<div class="result-card">
|
||||||
<div class="image-preview">
|
<div class="image-preview">
|
||||||
<img src="${imageUrl}" alt="${result.filename}"
|
<img src="${imageUrl}" alt="${result.filename}"
|
||||||
onerror="this.style.display='none'; this.nextElementSibling.style.display='block';">
|
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';"
|
||||||
<div style="display:none; width:200px; height:150px; background:#f0f0f0;
|
onload="this.nextElementSibling.style.display='none';">
|
||||||
border-radius:8px; display:flex; align-items:center; justify-content:center;
|
<div class="image-placeholder" style="display:none; width:200px; height:150px; background:#f0f0f0;
|
||||||
|
border-radius:8px; align-items:center; justify-content:center;
|
||||||
color:#666; font-size:14px;">📸 Image not available</div>
|
color:#666; font-size:14px;">📸 Image not available</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="result-content">
|
<div class="result-content">
|
||||||
@@ -261,9 +301,9 @@ async def root():
|
|||||||
${result.keywords.map(k => `<span class="keyword">${k}</span>`).join('')}
|
${result.keywords.map(k => `<span class="keyword">${k}</span>`).join('')}
|
||||||
</div>
|
</div>
|
||||||
<p><strong>Quality Score:</strong>
|
<p><strong>Quality Score:</strong>
|
||||||
<span class="quality-score ${qualityClass}">${result.quality_score}/100</span>
|
<span class="quality-score ${qualityClass}">${qualityScore}/100</span>
|
||||||
</p>
|
</p>
|
||||||
<p><strong>Processing Time:</strong> ${result.processing_time.toFixed(1)}s</p>
|
<p><strong>Processing Time:</strong> ${(result.processing_time || 0).toFixed(1)}s</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -309,23 +349,40 @@ async def analyze_single_image(file: UploadFile = File(...)):
|
|||||||
try:
|
try:
|
||||||
# Read and validate image
|
# Read and validate image
|
||||||
contents = await file.read()
|
contents = await file.read()
|
||||||
image = Image.open(io.BytesIO(contents))
|
|
||||||
|
# Validate file is an image
|
||||||
# Save temporarily for processing
|
if not file.content_type or not file.content_type.startswith('image/'):
|
||||||
temp_path = f"temp_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{file.filename}"
|
raise ValueError(f"File {file.filename} is not a valid image")
|
||||||
image.save(temp_path)
|
|
||||||
|
# Create BytesIO object and open image
|
||||||
|
image_bytes = io.BytesIO(contents)
|
||||||
|
image = Image.open(image_bytes)
|
||||||
|
|
||||||
|
# Convert to RGB if necessary (handles RGBA, P mode, etc.)
|
||||||
|
if image.mode not in ('RGB', 'L'):
|
||||||
|
image = image.convert('RGB')
|
||||||
|
|
||||||
|
# Save temporarily for processing and display
|
||||||
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S_%f')
|
||||||
|
safe_filename = f"{timestamp}_{file.filename.replace(' ', '_')}"
|
||||||
|
temp_path = f"temp_{safe_filename}"
|
||||||
|
upload_path = f"uploads/{safe_filename}"
|
||||||
|
|
||||||
|
# Save both temp file for processing and upload file for display
|
||||||
|
image.save(temp_path, format='JPEG')
|
||||||
|
image.save(upload_path, format='JPEG')
|
||||||
|
|
||||||
start_time = datetime.now()
|
start_time = datetime.now()
|
||||||
|
|
||||||
# Generate keywords
|
# Generate keywords
|
||||||
ai_results = keyword_generator.generate_keywords(temp_path)
|
ai_results = keyword_generator.generate_keywords(temp_path)
|
||||||
|
|
||||||
# Validate quality
|
# Validate quality
|
||||||
quality_result = validator.validate_keywords(ai_results['keywords'])
|
quality_result = validator.validate_keywords(ai_results['keywords'])
|
||||||
|
|
||||||
processing_time = (datetime.now() - start_time).total_seconds()
|
processing_time = (datetime.now() - start_time).total_seconds()
|
||||||
|
|
||||||
# Clean up
|
# Clean up temp file (keep upload file for display)
|
||||||
os.remove(temp_path)
|
os.remove(temp_path)
|
||||||
|
|
||||||
return KeywordResponse(
|
return KeywordResponse(
|
||||||
@@ -335,7 +392,7 @@ async def analyze_single_image(file: UploadFile = File(...)):
|
|||||||
quality_score=quality_result['score'],
|
quality_score=quality_result['score'],
|
||||||
processing_time=processing_time,
|
processing_time=processing_time,
|
||||||
caption=ai_results['caption'],
|
caption=ai_results['caption'],
|
||||||
image_url=None # For uploaded files, we don't serve them back
|
image_url=f"/uploads/{safe_filename}"
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -346,7 +403,10 @@ async def analyze_batch_images(files: List[UploadFile] = File(...)):
|
|||||||
"""Analyze multiple agricultural images"""
|
"""Analyze multiple agricultural images"""
|
||||||
if not keyword_generator:
|
if not keyword_generator:
|
||||||
raise HTTPException(status_code=500, detail="AI system not initialized")
|
raise HTTPException(status_code=500, detail="AI system not initialized")
|
||||||
|
|
||||||
|
# Clean up old uploads periodically
|
||||||
|
cleanup_old_uploads()
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
failed = 0
|
failed = 0
|
||||||
start_time = datetime.now()
|
start_time = datetime.now()
|
||||||
@@ -355,16 +415,34 @@ async def analyze_batch_images(files: List[UploadFile] = File(...)):
|
|||||||
try:
|
try:
|
||||||
# Process each file
|
# Process each file
|
||||||
contents = await file.read()
|
contents = await file.read()
|
||||||
image = Image.open(io.BytesIO(contents))
|
|
||||||
|
# Validate file is an image
|
||||||
temp_path = f"temp_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{file.filename}"
|
if not file.content_type or not file.content_type.startswith('image/'):
|
||||||
image.save(temp_path)
|
raise ValueError(f"File {file.filename} is not a valid image")
|
||||||
|
|
||||||
|
# Create BytesIO object and open image
|
||||||
|
image_bytes = io.BytesIO(contents)
|
||||||
|
image = Image.open(image_bytes)
|
||||||
|
|
||||||
|
# Convert to RGB if necessary (handles RGBA, P mode, etc.)
|
||||||
|
if image.mode not in ('RGB', 'L'):
|
||||||
|
image = image.convert('RGB')
|
||||||
|
|
||||||
|
# Save temporarily for processing and display
|
||||||
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S_%f')
|
||||||
|
safe_filename = f"{timestamp}_{file.filename.replace(' ', '_')}"
|
||||||
|
temp_path = f"temp_{safe_filename}"
|
||||||
|
upload_path = f"uploads/{safe_filename}"
|
||||||
|
|
||||||
|
# Save both temp file for processing and upload file for display
|
||||||
|
image.save(temp_path, format='JPEG')
|
||||||
|
image.save(upload_path, format='JPEG')
|
||||||
|
|
||||||
file_start = datetime.now()
|
file_start = datetime.now()
|
||||||
ai_results = keyword_generator.generate_keywords(temp_path)
|
ai_results = keyword_generator.generate_keywords(temp_path)
|
||||||
quality_result = validator.validate_keywords(ai_results['keywords'])
|
quality_result = validator.validate_keywords(ai_results['keywords'])
|
||||||
file_time = (datetime.now() - file_start).total_seconds()
|
file_time = (datetime.now() - file_start).total_seconds()
|
||||||
|
|
||||||
results.append(KeywordResponse(
|
results.append(KeywordResponse(
|
||||||
filename=file.filename,
|
filename=file.filename,
|
||||||
keywords=ai_results['keywords'],
|
keywords=ai_results['keywords'],
|
||||||
@@ -372,25 +450,32 @@ async def analyze_batch_images(files: List[UploadFile] = File(...)):
|
|||||||
quality_score=quality_result['score'],
|
quality_score=quality_result['score'],
|
||||||
processing_time=file_time,
|
processing_time=file_time,
|
||||||
caption=ai_results['caption'],
|
caption=ai_results['caption'],
|
||||||
image_url=None # For uploaded files, we don't serve them back
|
image_url=f"/uploads/{safe_filename}"
|
||||||
))
|
))
|
||||||
|
|
||||||
|
# Clean up temp file (keep upload file for display)
|
||||||
os.remove(temp_path)
|
os.remove(temp_path)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
failed += 1
|
failed += 1
|
||||||
print(f"Error processing {file.filename}: {e}")
|
error_msg = f"Error processing {file.filename}: {str(e)}"
|
||||||
|
print(error_msg)
|
||||||
|
# Add error details to help debugging
|
||||||
|
if "cannot identify image file" in str(e):
|
||||||
|
print(f" - File type: {file.content_type}")
|
||||||
|
print(f" - File size: {len(contents) if 'contents' in locals() else 'unknown'} bytes")
|
||||||
|
# You could also add failed files to results with error info if needed
|
||||||
|
|
||||||
total_time = (datetime.now() - start_time).total_seconds()
|
total_time = (datetime.now() - start_time).total_seconds()
|
||||||
avg_quality = sum(r.quality_score for r in results) / len(results) if results else 0
|
avg_quality = sum(r.quality_score for r in results) / len(results) if results else 0.0
|
||||||
|
|
||||||
return BatchResponse(
|
return BatchResponse(
|
||||||
total_images=len(files),
|
total_images=len(files),
|
||||||
successful=len(results),
|
successful=len(results),
|
||||||
failed=failed,
|
failed=failed,
|
||||||
results=results,
|
results=results,
|
||||||
average_quality=avg_quality,
|
average_quality=float(avg_quality),
|
||||||
total_processing_time=total_time
|
total_processing_time=float(total_time)
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.get("/demo", response_model=BatchResponse)
|
@app.get("/demo", response_model=BatchResponse)
|
||||||
@@ -400,7 +485,7 @@ async def run_demo():
|
|||||||
raise HTTPException(status_code=500, detail="AI system not initialized")
|
raise HTTPException(status_code=500, detail="AI system not initialized")
|
||||||
|
|
||||||
# Use existing sample images
|
# Use existing sample images
|
||||||
sample_dir = "data/working_images"
|
sample_dir = "../../data/working_images"
|
||||||
if not os.path.exists(sample_dir):
|
if not os.path.exists(sample_dir):
|
||||||
raise HTTPException(status_code=404, detail="Sample images not found")
|
raise HTTPException(status_code=404, detail="Sample images not found")
|
||||||
|
|
||||||
@@ -419,7 +504,7 @@ async def run_demo():
|
|||||||
file_time = (datetime.now() - file_start).total_seconds()
|
file_time = (datetime.now() - file_start).total_seconds()
|
||||||
|
|
||||||
# Create image URL for serving
|
# Create image URL for serving
|
||||||
relative_path = os.path.relpath(img_path, "data")
|
relative_path = os.path.relpath(img_path, "../../data")
|
||||||
image_url = f"/static/{relative_path}"
|
image_url = f"/static/{relative_path}"
|
||||||
|
|
||||||
results.append(KeywordResponse(
|
results.append(KeywordResponse(
|
||||||
@@ -436,15 +521,15 @@ async def run_demo():
|
|||||||
print(f"Error processing {img_path}: {e}")
|
print(f"Error processing {img_path}: {e}")
|
||||||
|
|
||||||
total_time = (datetime.now() - start_time).total_seconds()
|
total_time = (datetime.now() - start_time).total_seconds()
|
||||||
avg_quality = sum(r.quality_score for r in results) / len(results) if results else 0
|
avg_quality = sum(r.quality_score for r in results) / len(results) if results else 0.0
|
||||||
|
|
||||||
return BatchResponse(
|
return BatchResponse(
|
||||||
total_images=len(image_files),
|
total_images=len(image_files),
|
||||||
successful=len(results),
|
successful=len(results),
|
||||||
failed=len(image_files) - len(results),
|
failed=len(image_files) - len(results),
|
||||||
results=results,
|
results=results,
|
||||||
average_quality=avg_quality,
|
average_quality=float(avg_quality),
|
||||||
total_processing_time=total_time
|
total_processing_time=float(total_time)
|
||||||
)
|
)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 53 KiB |