Spaces:
Running
Running
#!/usr/bin/env python3 | |
""" | |
Script to convert existing thumbnails and detail images to WebP format using the new thumbnail generation logic. | |
This script should be run after deploying the new thumbnail service to update existing images. | |
""" | |
import os | |
import sys | |
import asyncio | |
import logging | |
from typing import List, Optional, Tuple | |
from sqlalchemy.orm import Session | |
from sqlalchemy import create_engine | |
from sqlalchemy.orm import sessionmaker | |
# Add the current directory to Python path | |
sys.path.append(os.path.dirname(os.path.abspath(__file__))) | |
from app.database import SessionLocal, engine | |
from app.models import Images | |
from app.services.thumbnail_service import ImageProcessingService | |
from app import storage | |
from app.config import settings | |
# Configure logging | |
try: | |
# Try to write to /tmp which should be writable in Docker | |
logging.basicConfig( | |
level=logging.INFO, | |
format='%(asctime)s - %(levelname)s - %(message)s', | |
handlers=[ | |
logging.StreamHandler(sys.stdout), | |
logging.FileHandler('/tmp/thumbnail_conversion.log') | |
] | |
) | |
except PermissionError: | |
# Fallback to console-only logging if file writing fails | |
logging.basicConfig( | |
level=logging.INFO, | |
format='%(asctime)s - %(levelname)s - %(message)s', | |
handlers=[ | |
logging.StreamHandler(sys.stdout) | |
] | |
) | |
logger = logging.getLogger(__name__) | |
class ThumbnailConverter: | |
def __init__(self): | |
self.db = SessionLocal() | |
self.converted_thumbnails = 0 | |
self.converted_details = 0 | |
self.skipped_count = 0 | |
self.error_count = 0 | |
def __del__(self): | |
if self.db: | |
self.db.close() | |
def get_all_images(self) -> List[Images]: | |
"""Fetch all images from the database""" | |
try: | |
images = self.db.query(Images).all() | |
logger.info(f"Found {len(images)} images in database") | |
return images | |
except Exception as e: | |
logger.error(f"Error fetching images from database: {e}") | |
return [] | |
def needs_thumbnail_conversion(self, image: Images) -> bool: | |
"""Check if an image needs thumbnail conversion to WebP""" | |
# Skip if no thumbnail exists | |
if not image.thumbnail_key: | |
return False | |
# Skip if thumbnail is already WebP | |
if image.thumbnail_key.endswith('.webp'): | |
return False | |
# Convert if thumbnail is JPEG | |
if image.thumbnail_key.endswith('.jpg') or image.thumbnail_key.endswith('.jpeg'): | |
return True | |
# Skip other formats for now | |
return False | |
def needs_detail_conversion(self, image: Images) -> bool: | |
"""Check if an image needs detail image conversion to WebP""" | |
# Skip if no detail image exists | |
if not image.detail_key: | |
return False | |
# Skip if detail image is already WebP | |
if image.detail_key.endswith('.webp'): | |
return False | |
# Convert if detail image is JPEG | |
if image.detail_key.endswith('.jpg') or image.detail_key.endswith('.jpeg'): | |
return True | |
# Skip other formats for now | |
return False | |
def fetch_original_image(self, image: Images) -> Optional[bytes]: | |
"""Fetch the original image content from storage""" | |
try: | |
if hasattr(storage, 's3') and settings.STORAGE_PROVIDER != "local": | |
# S3 storage | |
response = storage.s3.get_object( | |
Bucket=settings.S3_BUCKET, | |
Key=image.file_key, | |
) | |
return response["Body"].read() | |
else: | |
# Local storage | |
file_path = os.path.join(settings.STORAGE_DIR, image.file_key) | |
if os.path.exists(file_path): | |
with open(file_path, 'rb') as f: | |
return f.read() | |
else: | |
logger.warning(f"Original image file not found: {file_path}") | |
return None | |
except Exception as e: | |
logger.error(f"Error fetching original image {image.image_id}: {e}") | |
return None | |
def delete_old_file(self, file_key: str, file_type: str) -> bool: | |
"""Delete the old file from storage""" | |
try: | |
if not file_key: | |
return True | |
if hasattr(storage, 's3') and settings.STORAGE_PROVIDER != "local": | |
# S3 storage | |
storage.s3.delete_object( | |
Bucket=settings.S3_BUCKET, | |
Key=file_key, | |
) | |
else: | |
# Local storage | |
file_path = os.path.join(settings.STORAGE_DIR, file_key) | |
if os.path.exists(file_path): | |
os.remove(file_path) | |
logger.info(f"Deleted old {file_type}: {file_key}") | |
return True | |
except Exception as e: | |
logger.error(f"Error deleting old {file_type} {file_key}: {e}") | |
return False | |
def convert_thumbnail(self, image: Images) -> bool: | |
"""Convert thumbnail to WebP format""" | |
try: | |
logger.info(f"Converting thumbnail for image {image.image_id}") | |
# Fetch original image | |
original_content = self.fetch_original_image(image) | |
if not original_content: | |
logger.error(f"Could not fetch original image for {image.image_id}") | |
return False | |
# Generate new WebP thumbnail | |
thumbnail_bytes, thumbnail_filename = ImageProcessingService.create_thumbnail( | |
original_content, | |
image.file_key | |
) | |
if not thumbnail_bytes or not thumbnail_filename: | |
logger.error(f"Failed to generate WebP thumbnail for {image.image_id}") | |
return False | |
# Upload new thumbnail | |
thumbnail_result = ImageProcessingService.upload_image_bytes( | |
thumbnail_bytes, | |
thumbnail_filename, | |
"WEBP" | |
) | |
if not thumbnail_result: | |
logger.error(f"Failed to upload WebP thumbnail for {image.image_id}") | |
return False | |
new_thumbnail_key, new_thumbnail_sha256 = thumbnail_result | |
# Delete old thumbnail | |
if not self.delete_old_file(image.thumbnail_key, "thumbnail"): | |
logger.warning(f"Could not delete old thumbnail for {image.image_id}") | |
# Update database record | |
image.thumbnail_key = new_thumbnail_key | |
image.thumbnail_sha256 = new_thumbnail_sha256 | |
logger.info(f"Successfully converted thumbnail for {image.image_id}: {new_thumbnail_key}") | |
return True | |
except Exception as e: | |
logger.error(f"Error converting thumbnail for {image.image_id}: {e}") | |
return False | |
def convert_detail_image(self, image: Images) -> bool: | |
"""Convert detail image to WebP format""" | |
try: | |
logger.info(f"Converting detail image for image {image.image_id}") | |
# Fetch original image | |
original_content = self.fetch_original_image(image) | |
if not original_content: | |
logger.error(f"Could not fetch original image for {image.image_id}") | |
return False | |
# Generate new WebP detail image | |
detail_bytes, detail_filename = ImageProcessingService.create_detail_image( | |
original_content, | |
image.file_key | |
) | |
if not detail_bytes or not detail_filename: | |
logger.error(f"Failed to generate WebP detail image for {image.image_id}") | |
return False | |
# Upload new detail image | |
detail_result = ImageProcessingService.upload_image_bytes( | |
detail_bytes, | |
detail_filename, | |
"WEBP" | |
) | |
if not detail_result: | |
logger.error(f"Failed to upload WebP detail image for {image.image_id}") | |
return False | |
new_detail_key, new_detail_sha256 = detail_result | |
# Delete old detail image | |
if not self.delete_old_file(image.detail_key, "detail image"): | |
logger.warning(f"Could not delete old detail image for {image.image_id}") | |
# Update database record | |
image.detail_key = new_detail_key | |
image.detail_sha256 = new_detail_sha256 | |
logger.info(f"Successfully converted detail image for {image.image_id}: {new_detail_key}") | |
return True | |
except Exception as e: | |
logger.error(f"Error converting detail image for {image.image_id}: {e}") | |
return False | |
def process_images(self) -> Tuple[int, int, int, int]: | |
"""Process all images and convert thumbnails and detail images to WebP""" | |
images = self.get_all_images() | |
if not images: | |
logger.warning("No images found in database") | |
return 0, 0, 0, 0 | |
logger.info(f"Starting image conversion for {len(images)} images...") | |
for i, image in enumerate(images, 1): | |
try: | |
if i % 10 == 0: | |
logger.info(f"Progress: {i}/{len(images)} images processed") | |
needs_conversion = False | |
# Check and convert thumbnail | |
if self.needs_thumbnail_conversion(image): | |
if self.convert_thumbnail(image): | |
self.converted_thumbnails += 1 | |
needs_conversion = True | |
else: | |
self.error_count += 1 | |
# Check and convert detail image | |
if self.needs_detail_conversion(image): | |
if self.convert_detail_image(image): | |
self.converted_details += 1 | |
needs_conversion = True | |
else: | |
self.error_count += 1 | |
# Commit changes if any conversions were made | |
if needs_conversion: | |
self.db.commit() | |
else: | |
self.skipped_count += 1 | |
except Exception as e: | |
logger.error(f"Error processing image {image.image_id}: {e}") | |
self.db.rollback() | |
self.error_count += 1 | |
return self.converted_thumbnails, self.converted_details, self.skipped_count, self.error_count | |
def main(): | |
"""Main function to run the thumbnail conversion""" | |
logger.info("Starting image conversion script...") | |
try: | |
converter = ThumbnailConverter() | |
converted_thumbnails, converted_details, skipped, errors = converter.process_images() | |
logger.info("=" * 50) | |
logger.info("IMAGE CONVERSION SUMMARY") | |
logger.info("=" * 50) | |
logger.info(f"Thumbnails converted to WebP: {converted_thumbnails}") | |
logger.info(f"Detail images converted to WebP: {converted_details}") | |
logger.info(f"Images skipped (already WebP or no images): {skipped}") | |
logger.info(f"Images with errors: {errors}") | |
logger.info(f"Total conversions: {converted_thumbnails + converted_details}") | |
logger.info("=" * 50) | |
if errors > 0: | |
logger.warning(f"Some images had errors during conversion. Check the log file for details.") | |
return 1 | |
else: | |
logger.info("All image conversions completed successfully!") | |
return 0 | |
except Exception as e: | |
logger.error(f"Fatal error during image conversion: {e}") | |
return 1 | |
if __name__ == "__main__": | |
exit_code = main() | |
sys.exit(exit_code) | |