diff --git a/squashthumbnailmaker/Dockerfile b/squashthumbnailmaker/Dockerfile new file mode 100644 index 0000000..430f457 --- /dev/null +++ b/squashthumbnailmaker/Dockerfile @@ -0,0 +1,31 @@ +FROM python:3.11-slim + +# Install ffmpeg and other dependencies +RUN apt-get update && apt-get install -y \ + ffmpeg \ + libgl1 \ + libglib2.0-0 \ + libsm6 \ + libxext6 \ + libxrender-dev \ + libgomp1 \ + wget \ + && rm -rf /var/lib/apt/lists/* + +# Create app directory +WORKDIR /app + +# Install Python dependencies +RUN pip install --no-cache-dir \ + ultralytics \ + opencv-python-headless \ + numpy + +# Copy the thumbnail generation script +COPY generate_thumbnail.py /app/ + +# Make script executable +RUN chmod +x /app/generate_thumbnail.py + +# Set the entrypoint +ENTRYPOINT ["python", "/app/generate_thumbnail.py"] \ No newline at end of file diff --git a/squashthumbnailmaker/README.txt b/squashthumbnailmaker/README.txt new file mode 100644 index 0000000..fde4c84 --- /dev/null +++ b/squashthumbnailmaker/README.txt @@ -0,0 +1,75 @@ +Squash Thumbnail Maker +====================== + +A Docker-based utility for generating high-quality thumbnails from MP4 videos using intelligent frame detection. + +Features: +--------- +- Smart thumbnail generation using YOLOv8 pose detection to find frames with both players visible +- Fallback to simple thumbnail extraction if smart detection fails +- Process single videos or entire directories +- Configurable thumbnail quality and size +- Low priority execution to avoid system impact + +Usage: +------ +After installation, use the Docker container to process videos: + +# Process a single video file +docker run --rm -v /path/to/videos:/data squashthumbnailmaker_image:latest /data/video.mp4 + +# Process all videos in a directory +docker run --rm -v /path/to/videos:/data squashthumbnailmaker_image:latest /data + +# With custom settings +docker run --rm \ + -e ENABLE_SMART_DETECTION=false \ + -e THUMBNAIL_WIDTH=1280 \ + -e THUMBNAIL_QUALITY=90 \ + -v /path/to/videos:/data \ + squashthumbnailmaker_image:latest /data + +Configuration Options: +---------------------- +The following environment variables can be set in config/service.env or passed with -e flag: + +- ENABLE_SMART_DETECTION: Use YOLO for intelligent thumbnail generation (default: true) +- FALLBACK_TIME: Seconds into video for fallback thumbnail (default: 10) +- THUMBNAIL_WIDTH: Width of generated thumbnails in pixels (default: 640) +- THUMBNAIL_QUALITY: JPEG quality 1-100 (default: 95) +- MAX_SAMPLE_TIME: Maximum seconds to sample in video (default: 180) +- SAMPLE_INTERVAL: Interval between samples in seconds (default: 1) +- NICE_LEVEL: Process priority 0-19, 19 is lowest (default: 19) + +How It Works: +------------- +1. Smart Detection Mode (if enabled): + - Samples frames throughout the video + - Uses YOLOv8 pose model to detect players + - Scores frames based on: + * Number of players visible (ideally 2) + * Face visibility (crucial for good thumbnails) + * Player size and positioning + * Players being centered in frame + - Fine-tunes around the best frame for optimal result + +2. Simple Mode (fallback): + - Extracts frame at specified time (default 10 seconds) + - Falls back to 1 second if initial extraction fails + +Output: +------- +Thumbnails are saved as .jpg files in the same directory as the source video with the same base filename. +Example: video.mp4 → video.jpg + +Requirements: +------------- +- Docker +- Videos should be in MP4, MOV, AVI, or MKV format +- Sufficient disk space for Docker image (~2GB with YOLO model) + +Notes: +------ +- The first run will download the YOLOv8 model (~6MB) if smart detection is enabled +- Processing time depends on video length and smart detection settings +- Thumbnails are only generated if they don't already exist (skip existing) \ No newline at end of file diff --git a/squashthumbnailmaker/config/.template_info.env b/squashthumbnailmaker/config/.template_info.env new file mode 100644 index 0000000..b34fe7f --- /dev/null +++ b/squashthumbnailmaker/config/.template_info.env @@ -0,0 +1,14 @@ +# Template identifier - MUST match the directory name +TEMPLATE=squashthumbnailmaker + +# Requirements +REQUIRES_HOST_ROOT=false # Whether root access on host is needed +REQUIRES_DOCKER=true # Whether Docker is required +REQUIRES_DOCKER_ROOT=false # Whether Docker root privileges are needed + +# Docker image settings (will be built locally) +IMAGE_REGISTRY="local" +IMAGE_REPO="squashthumbnailmaker" +IMAGE_TAG="latest" + +# This is a utility container, no persistent volumes needed \ No newline at end of file diff --git a/squashthumbnailmaker/config/service.env b/squashthumbnailmaker/config/service.env new file mode 100644 index 0000000..71f1faa --- /dev/null +++ b/squashthumbnailmaker/config/service.env @@ -0,0 +1,21 @@ +# Squash Thumbnail Maker Configuration +CONTAINER_NAME=squashthumbnailmaker + +# Server settings (REQUIRED by dropshell) +SSH_USER="root" + +# Docker image settings +IMAGE_REGISTRY="docker.io" +IMAGE_REPO="python" +IMAGE_TAG="3.11-slim" + +# Processing settings +ENABLE_SMART_DETECTION=true # Use YOLO for intelligent thumbnail generation +FALLBACK_TIME=10 # Seconds into video for fallback thumbnail +THUMBNAIL_WIDTH=640 # Width of generated thumbnails +THUMBNAIL_QUALITY=95 # JPEG quality (1-100) +MAX_SAMPLE_TIME=180 # Maximum seconds to sample in video +SAMPLE_INTERVAL=1 # Interval between samples in seconds + +# Process priority +NICE_LEVEL=19 # Process priority (0-19, 19 is lowest) \ No newline at end of file diff --git a/squashthumbnailmaker/generate_thumbnail.py b/squashthumbnailmaker/generate_thumbnail.py new file mode 100755 index 0000000..01e100d --- /dev/null +++ b/squashthumbnailmaker/generate_thumbnail.py @@ -0,0 +1,529 @@ +#!/usr/bin/env python3 +""" +Squash Thumbnail Maker - Generates thumbnails for MP4 videos +Uses smart detection to find best frame with players visible +Independent standalone version based on squashkiwi approach +""" + +import os +import sys +import json +import logging +import subprocess +import asyncio +from pathlib import Path +from typing import Optional, Tuple +from datetime import datetime + +# Set process to low priority if configured +try: + nice_level = int(os.environ.get('NICE_LEVEL', '19')) + os.nice(nice_level) +except: + pass + +logging.basicConfig( + level=logging.INFO, + format='%(levelname)s - %(message)s' +) +logger = logging.getLogger('thumbnailmaker') + +class ThumbnailGenerator: + """Generates thumbnails for videos using sophisticated AI detection""" + + def __init__(self): + self.has_yolo = self.check_yolo_available() + self.enable_smart = os.environ.get('ENABLE_SMART_DETECTION', 'true').lower() == 'true' + self.fallback_time = int(os.environ.get('FALLBACK_TIME', '10')) + self.thumbnail_width = int(os.environ.get('THUMBNAIL_WIDTH', '640')) + self.thumbnail_quality = int(os.environ.get('THUMBNAIL_QUALITY', '95')) + self.max_sample_time = int(os.environ.get('MAX_SAMPLE_TIME', '180')) + self.sample_interval = int(os.environ.get('SAMPLE_INTERVAL', '1')) + + def check_yolo_available(self) -> bool: + """Check if YOLOv8 is available for smart detection""" + try: + from ultralytics import YOLO + return True + except ImportError: + logger.debug("YOLOv8 not available, will use simple thumbnail generation") + return False + + async def process_video_file(self, video_file: Path) -> bool: + """Process a single video file""" + if not video_file.exists(): + logger.error(f"Video file not found: {video_file}") + return False + + if video_file.suffix.lower() not in ['.mp4', '.mov', '.avi', '.mkv']: + logger.warning(f"Skipping non-video file: {video_file}") + return False + + return await self.generate_thumbnail(video_file) + + async def process_directory(self, directory: Path) -> int: + """Process all video files in a directory""" + if not directory.exists() or not directory.is_dir(): + logger.error(f"Directory not found: {directory}") + return 0 + + video_extensions = ['.mp4', '.mov', '.avi', '.mkv'] + video_files = [] + + for ext in video_extensions: + video_files.extend(directory.glob(f'*{ext}')) + video_files.extend(directory.glob(f'*{ext.upper()}')) + + if not video_files: + logger.warning(f"No video files found in {directory}") + return 0 + + logger.info(f"Found {len(video_files)} video files to process") + + success_count = 0 + for video_file in video_files: + if await self.process_video_file(video_file): + success_count += 1 + + return success_count + + async def generate_thumbnail(self, video_file: Path) -> bool: + """Main method to generate thumbnail - tries smart detection first, falls back to simple""" + thumbnail_file = video_file.with_suffix('.jpg') + + # Skip if thumbnail already exists + if thumbnail_file.exists(): + logger.info(f"Thumbnail already exists: {thumbnail_file.name}") + return True + + try: + # Try smart thumbnail generation first if enabled + if self.enable_smart and self.has_yolo: + try: + success = await self.generate_smart_thumbnail(video_file) + if success: + return True + except ImportError as e: + logger.debug(f"YOLOv8 not available, using simple thumbnail: {e}") + except Exception as e: + logger.warning(f"Smart thumbnail failed, using fallback: {e}") + + # Fallback to simple thumbnail at configured time + success = await self.generate_simple_thumbnail(video_file, seek_time=self.fallback_time) + if success: + return True + + # Last resort - try to get frame at 1 second + success = await self.generate_simple_thumbnail(video_file, seek_time=1) + if not success: + logger.error(f"All thumbnail generation methods failed for {video_file.name}") + return False + + except Exception as e: + logger.error(f"Critical error generating thumbnail: {e}") + return False + + async def generate_simple_thumbnail(self, video_file: Path, seek_time: int = 10) -> bool: + """Generate a simple thumbnail at specified time""" + try: + thumbnail_file = video_file.with_suffix('.jpg') + logger.debug(f"Attempting simple thumbnail generation for {video_file.name} at {seek_time}s") + + if not video_file.exists(): + logger.error(f"Video file does not exist: {video_file}") + return False + + video_size = video_file.stat().st_size + logger.debug(f"Video file size: {video_size / (1024*1024):.1f} MB") + + cmd = [ + 'ffmpeg', + '-i', str(video_file), + '-ss', str(seek_time), + '-vframes', '1', + '-vf', f'scale={self.thumbnail_width}:-1', + '-q:v', str(100 - self.thumbnail_quality + 1), # Convert quality to ffmpeg scale + '-y', + str(thumbnail_file) + ] + + logger.debug(f"Running FFmpeg command: {' '.join(cmd[:6])}") + + process = await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + + stdout, stderr = await process.communicate() + + if process.returncode == 0: + if thumbnail_file.exists(): + thumb_size = thumbnail_file.stat().st_size + logger.info(f"Generated simple thumbnail: {thumbnail_file.name} at {seek_time}s ({thumb_size / 1024:.1f} KB)") + return True + else: + logger.error(f"FFmpeg succeeded but thumbnail file not created: {thumbnail_file}") + return False + else: + logger.error(f"FFmpeg failed with code {process.returncode} for {video_file.name}") + if stderr: + stderr_text = stderr.decode('utf-8', errors='ignore')[-500:] + logger.error(f"FFmpeg stderr: {stderr_text}") + return False + + except Exception as e: + logger.error(f"Exception generating simple thumbnail for {video_file.name}: {e}") + return False + + async def generate_smart_thumbnail(self, video_file: Path) -> bool: + """Generate thumbnail using YOLOv8 pose model to find best frame with both players""" + try: + logger.info(f"Starting smart thumbnail generation for {video_file.name}") + + # Import here to avoid loading if not needed + from ultralytics import YOLO + import cv2 + import numpy as np + + thumbnail_file = video_file.with_suffix('.jpg') + + # Load YOLO pose model for better player detection + logger.debug("Loading YOLOv8 pose model...") + model = YOLO('yolov8n-pose.pt') + logger.debug("YOLOv8 model loaded successfully") + + # Sample frames from video + sample_times = list(range(1, min(self.max_sample_time + 1, 181), self.sample_interval)) + logger.debug(f"Will sample {len(sample_times)} frames from video") + + # Get video dimensions first + probe_cmd = [ + 'ffprobe', '-v', 'error', + '-select_streams', 'v:0', + '-show_entries', 'stream=width,height', + '-of', 'csv=s=x:p=0', + str(video_file) + ] + + probe_process = await asyncio.create_subprocess_exec( + *probe_cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.DEVNULL + ) + + dimensions, _ = await probe_process.communicate() + + if not dimensions: + logger.error("Failed to get video dimensions") + return False + + width, height = map(int, dimensions.decode().strip().split('x')) + logger.debug(f"Video dimensions: {width}x{height}") + + best_frame = None + best_score = 0 + best_time = self.fallback_time + frames_processed = 0 + + # First pass: sample frames + for seek_time in sample_times: + logger.debug(f"Extracting frame at {seek_time}s...") + + cmd = [ + 'ffmpeg', + '-ss', str(seek_time), + '-i', str(video_file), + '-vframes', '1', + '-f', 'image2pipe', + '-pix_fmt', 'rgb24', + '-vcodec', 'rawvideo', + '-' + ] + + process = await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.DEVNULL + ) + + stdout, _ = await process.communicate() + + if process.returncode != 0 or not stdout: + logger.debug(f"Failed to extract frame at {seek_time}s") + continue + + # Convert raw video to numpy array + frame = np.frombuffer(stdout, dtype=np.uint8) + frame = frame.reshape((height, width, 3)) + frames_processed += 1 + + # Run YOLO detection + logger.debug(f"Running YOLO detection on frame at {seek_time}s...") + results = model(frame, verbose=False, conf=0.3) + + # Calculate score for this frame + score = self.calculate_frame_score(results, width, height) + logger.debug(f"Frame at {seek_time}s scored: {score}") + + if score > best_score: + best_score = score + best_frame = frame + best_time = seek_time + logger.info(f"New best frame found at {seek_time}s with score {score}") + + # If we find an excellent frame, stop searching + if score >= 180: + logger.info(f"Excellent frame found at {seek_time}s! Stopping search.") + break + + # Second pass: fine-grained search around the best frame + if best_frame is not None and best_score > 50: + logger.info(f"Found initial best frame at {best_time}s with score {best_score}") + logger.info(f"Performing fine-tuning search ±0.9s around best frame...") + + fine_sample_times = [] + for offset in range(-9, 10): + if offset == 0: + continue + fine_time = best_time + (offset * 0.1) + if fine_time > 0: + fine_sample_times.append(fine_time) + + logger.info(f"Fine-tuning: checking {len(fine_sample_times)} frames around {best_time}s") + + for seek_time in fine_sample_times: + logger.debug(f"Fine-tuning: checking frame at {seek_time:.1f}s...") + + cmd = [ + 'ffmpeg', + '-ss', str(seek_time), + '-i', str(video_file), + '-vframes', '1', + '-f', 'image2pipe', + '-pix_fmt', 'rgb24', + '-vcodec', 'rawvideo', + '-' + ] + + process = await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.DEVNULL + ) + + stdout, _ = await process.communicate() + + if process.returncode != 0 or not stdout: + logger.debug(f"Failed to extract frame at {seek_time:.1f}s") + continue + + frame = np.frombuffer(stdout, dtype=np.uint8) + frame = frame.reshape((height, width, 3)) + frames_processed += 1 + + results = model(frame, verbose=False, conf=0.3) + score = self.calculate_frame_score(results, width, height) + logger.debug(f"Fine-tuning: frame at {seek_time:.1f}s scored: {score}") + + if score > best_score: + logger.info(f"Fine-tuning: found better frame at {seek_time:.1f}s with score {score}") + best_score = score + best_frame = frame + best_time = seek_time + + if score >= 180: + logger.info(f"Fine-tuning: excellent frame found at {seek_time:.1f}s!") + break + + logger.info(f"Fine-tuning complete. Final best frame at {best_time}s with score {best_score}") + + # Save the best frame if we found one + logger.info(f"Processed {frames_processed} frames total") + if best_frame is not None and best_score > 0: + logger.info(f"Saving final best frame (score {best_score}) from {best_time}s as thumbnail") + + # Convert RGB to BGR for OpenCV + best_frame_bgr = cv2.cvtColor(best_frame, cv2.COLOR_RGB2BGR) + + # Resize to configured width + height, width = best_frame_bgr.shape[:2] + new_width = self.thumbnail_width + new_height = int(height * (new_width / width)) + resized = cv2.resize(best_frame_bgr, (new_width, new_height), interpolation=cv2.INTER_AREA) + logger.debug(f"Resized thumbnail to {new_width}x{new_height}") + + # Save as JPEG with configured quality + cv2.imwrite(str(thumbnail_file), resized, [cv2.IMWRITE_JPEG_QUALITY, self.thumbnail_quality]) + + if thumbnail_file.exists(): + thumb_size = thumbnail_file.stat().st_size + logger.info(f"Smart thumbnail saved: {thumbnail_file.name} ({thumb_size / 1024:.1f} KB) from {best_time}s with score {best_score}") + return True + else: + logger.error(f"Failed to save smart thumbnail to {thumbnail_file}") + return False + else: + logger.warning(f"No suitable frame found for smart thumbnail (best score was {best_score})") + return False + + except ImportError as e: + logger.debug(f"YOLOv8 not installed: {e}") + return False + except Exception as e: + logger.error(f"Smart thumbnail generation error: {e}", exc_info=True) + return False + + def calculate_frame_score(self, results, frame_width, frame_height) -> int: + """Calculate score for a frame based on player detection and pose""" + import numpy as np + + score = 0 + + if not results or len(results) == 0: + logger.debug("No detection results") + return 0 + + result = results[0] + + # Get person detections + if result.boxes is None: + logger.debug("No boxes detected") + return 0 + + persons = [] + for box in result.boxes: + if box.cls == 0: # Class 0 is person in COCO + persons.append(box) + + logger.debug(f"Detected {len(persons)} person(s) in frame") + + # Score based on number of people + if len(persons) == 2: + score = 30 # Base score for having both players + logger.debug("Both players detected: +30 points") + elif len(persons) == 1: + score = 5 # Low score for single player + logger.debug("Single player detected: +5 points") + else: + logger.debug(f"No valid player count ({len(persons)} detected)") + return 0 + + # Check poses if available for face visibility + if hasattr(result, 'keypoints') and result.keypoints is not None: + keypoints = result.keypoints.xy.cpu().numpy() if hasattr(result.keypoints.xy, 'cpu') else result.keypoints.xy + + faces_visible = 0 + for person_kpts in keypoints[:2]: # Check first 2 people + if len(person_kpts) > 0: + # Check if face keypoints are visible (indices 0-4 are face in COCO pose) + face_points = person_kpts[:5] + face_visible = np.sum(face_points[:, 0] > 0) >= 3 # At least 3 face points visible + + if face_visible: + faces_visible += 1 + + # Bonus points for faces visible + if faces_visible == 2: + score += 100 # Both faces clearly visible + logger.debug("Both faces visible: +100 points") + elif faces_visible == 1: + score += 20 # One face visible + logger.debug("One face visible: +20 points") + + # Additional scoring based on player size and positioning + if len(persons) == 2: + center_players = 0 + total_player_area = 0 + min_player_area = float('inf') + + for box in persons[:2]: + x1, y1, x2, y2 = box.xyxy[0].tolist() + center_x = (x1 + x2) / 2 + + # Calculate player bounding box area as percentage of frame + box_width = x2 - x1 + box_height = y2 - y1 + box_area = (box_width * box_height) / (frame_width * frame_height) + total_player_area += box_area + min_player_area = min(min_player_area, box_area) + + # Check if player is in central 60% of frame + if 0.2 * frame_width < center_x < 0.8 * frame_width: + center_players += 1 + + # Score based on player size + avg_player_area = total_player_area / 2 + + if avg_player_area > 0.08: + score += 40 + logger.debug(f"Players are large ({avg_player_area*100:.1f}% avg): +40 points") + elif avg_player_area > 0.05: + score += 25 + logger.debug(f"Players are good size ({avg_player_area*100:.1f}% avg): +25 points") + elif avg_player_area > 0.03: + score += 10 + logger.debug(f"Players are small ({avg_player_area*100:.1f}% avg): +10 points") + + # Both players should be similar size + if min_player_area > 0: + size_ratio = min_player_area / (total_player_area - min_player_area) + if size_ratio > 0.5: + score += 10 + logger.debug(f"Players are similar size (ratio {size_ratio:.2f}): +10 points") + + # Bonus for both players being centered + if center_players == 2: + score += 15 + logger.debug("Both players centered: +15 points") + elif center_players == 1: + score += 5 + logger.debug("One player centered: +5 points") + + logger.debug(f"Final frame score: {score}") + return score + + +async def main(): + """Main entry point""" + if len(sys.argv) != 2: + logger.error("Usage: generate_thumbnail.py ") + sys.exit(1) + + input_path = Path(sys.argv[1]) + + # Report nice level + try: + nice_level = os.nice(0) # Get current nice level + logger.info(f"Running at nice level: {nice_level} (low priority)") + except: + pass + + generator = ThumbnailGenerator() + + try: + if input_path.is_file(): + # Process single file + if await generator.process_video_file(input_path): + logger.info(f"Thumbnail generation successful for {input_path.name}") + sys.exit(0) + else: + logger.error(f"Thumbnail generation failed for {input_path.name}") + sys.exit(1) + elif input_path.is_dir(): + # Process directory + success_count = await generator.process_directory(input_path) + total_count = len(list(input_path.glob('*.mp4')) + list(input_path.glob('*.MP4'))) + logger.info(f"Processed {success_count}/{total_count} videos successfully") + sys.exit(0 if success_count > 0 else 1) + else: + logger.error(f"Invalid input: {input_path} is neither a file nor a directory") + sys.exit(1) + + except Exception as e: + logger.error(f"Fatal error: {e}") + sys.exit(1) + + +if __name__ == '__main__': + asyncio.run(main()) \ No newline at end of file diff --git a/squashthumbnailmaker/install.sh b/squashthumbnailmaker/install.sh new file mode 100755 index 0000000..a6ac7f0 --- /dev/null +++ b/squashthumbnailmaker/install.sh @@ -0,0 +1,23 @@ +#!/bin/bash +source "${AGENT_PATH}/common.sh" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Check required environment variables +_check_required_env_vars "CONTAINER_NAME" "IMAGE_REGISTRY" "IMAGE_REPO" "IMAGE_TAG" + +# Check Docker is available +_check_docker_installed || _die "Docker test failed" + +echo "Building squashthumbnailmaker Docker image..." + +# Build the Docker image locally +cd "$SCRIPT_DIR" +docker build -t "${CONTAINER_NAME}_image:latest" . || _die "Failed to build Docker image" + +echo "Installation of ${CONTAINER_NAME} complete" +echo "" +echo "Usage:" +echo " Process single video: docker run --rm -v /path/to/videos:/data ${CONTAINER_NAME}_image:latest /data/video.mp4" +echo " Process entire directory: docker run --rm -v /path/to/videos:/data ${CONTAINER_NAME}_image:latest /data" +echo "" +echo "The thumbnail will be created as video.jpg in the same directory as the video file." \ No newline at end of file diff --git a/squashthumbnailmaker/logs.sh b/squashthumbnailmaker/logs.sh new file mode 100755 index 0000000..f23e878 --- /dev/null +++ b/squashthumbnailmaker/logs.sh @@ -0,0 +1,14 @@ +#!/bin/bash +source "${AGENT_PATH}/common.sh" +_check_required_env_vars "CONTAINER_NAME" + +# Show logs from any containers running with this image +containers=$(docker ps -a --filter "ancestor=${CONTAINER_NAME}_image:latest" --format "{{.ID}}") +if [ -n "$containers" ]; then + for container in $containers; do + echo "=== Logs from container $container ===" + docker logs "$container" "$@" + done +else + echo "No containers found for ${CONTAINER_NAME}" +fi \ No newline at end of file diff --git a/squashthumbnailmaker/process_videos.sh b/squashthumbnailmaker/process_videos.sh new file mode 100755 index 0000000..bd2f5fe --- /dev/null +++ b/squashthumbnailmaker/process_videos.sh @@ -0,0 +1,70 @@ +#!/bin/bash +# Helper script for easier local usage of the thumbnail maker + +# Check if Docker image exists +if ! docker images --format "{{.Repository}}:{{.Tag}}" | grep -q "^squashthumbnailmaker_image:latest$"; then + echo "Docker image not found. Please run install.sh first." + exit 1 +fi + +# Check arguments +if [ $# -eq 0 ]; then + echo "Usage: $0 " + echo "" + echo "Examples:" + echo " $0 /path/to/video.mp4 # Process single video" + echo " $0 /path/to/videos/ # Process all videos in directory" + echo "" + echo "Options (set as environment variables):" + echo " ENABLE_SMART_DETECTION=false # Disable YOLO detection" + echo " THUMBNAIL_WIDTH=1280 # Change thumbnail width" + echo " THUMBNAIL_QUALITY=90 # Change JPEG quality" + exit 1 +fi + +INPUT_PATH="$1" + +# Get absolute path +INPUT_PATH=$(realpath "$INPUT_PATH") + +if [ ! -e "$INPUT_PATH" ]; then + echo "Error: $INPUT_PATH does not exist" + exit 1 +fi + +# Determine if input is file or directory and get the mount path +if [ -f "$INPUT_PATH" ]; then + # For a file, mount its directory and reference the file + MOUNT_DIR=$(dirname "$INPUT_PATH") + FILENAME=$(basename "$INPUT_PATH") + CONTAINER_PATH="/data/$FILENAME" +elif [ -d "$INPUT_PATH" ]; then + # For a directory, mount it directly + MOUNT_DIR="$INPUT_PATH" + CONTAINER_PATH="/data" +else + echo "Error: $INPUT_PATH is neither a file nor a directory" + exit 1 +fi + +# Build Docker run command +DOCKER_CMD="docker run --rm" + +# Add environment variables if set +[ -n "$ENABLE_SMART_DETECTION" ] && DOCKER_CMD="$DOCKER_CMD -e ENABLE_SMART_DETECTION=$ENABLE_SMART_DETECTION" +[ -n "$FALLBACK_TIME" ] && DOCKER_CMD="$DOCKER_CMD -e FALLBACK_TIME=$FALLBACK_TIME" +[ -n "$THUMBNAIL_WIDTH" ] && DOCKER_CMD="$DOCKER_CMD -e THUMBNAIL_WIDTH=$THUMBNAIL_WIDTH" +[ -n "$THUMBNAIL_QUALITY" ] && DOCKER_CMD="$DOCKER_CMD -e THUMBNAIL_QUALITY=$THUMBNAIL_QUALITY" +[ -n "$MAX_SAMPLE_TIME" ] && DOCKER_CMD="$DOCKER_CMD -e MAX_SAMPLE_TIME=$MAX_SAMPLE_TIME" +[ -n "$SAMPLE_INTERVAL" ] && DOCKER_CMD="$DOCKER_CMD -e SAMPLE_INTERVAL=$SAMPLE_INTERVAL" +[ -n "$NICE_LEVEL" ] && DOCKER_CMD="$DOCKER_CMD -e NICE_LEVEL=$NICE_LEVEL" + +# Add volume mount and run +DOCKER_CMD="$DOCKER_CMD -v \"$MOUNT_DIR:/data\" squashthumbnailmaker_image:latest \"$CONTAINER_PATH\"" + +echo "Processing: $INPUT_PATH" +echo "Running: $DOCKER_CMD" +echo "" + +# Execute the command +eval $DOCKER_CMD \ No newline at end of file diff --git a/squashthumbnailmaker/start.sh b/squashthumbnailmaker/start.sh new file mode 100755 index 0000000..2ebd6cf --- /dev/null +++ b/squashthumbnailmaker/start.sh @@ -0,0 +1,15 @@ +#!/bin/bash +source "${AGENT_PATH}/common.sh" +_check_required_env_vars "CONTAINER_NAME" + +echo "This is a utility container that runs on-demand." +echo "To process videos, use:" +echo "" +echo " Single video: docker run --rm -v /path/to/videos:/data ${CONTAINER_NAME}_image:latest /data/video.mp4" +echo " All videos in dir: docker run --rm -v /path/to/videos:/data ${CONTAINER_NAME}_image:latest /data" +echo "" +echo "Environment variables can be passed with -e flag:" +echo " -e ENABLE_SMART_DETECTION=false # Disable YOLO detection" +echo " -e FALLBACK_TIME=5 # Change fallback thumbnail time" +echo " -e THUMBNAIL_WIDTH=1280 # Change thumbnail width" +echo " -e THUMBNAIL_QUALITY=90 # Change JPEG quality" \ No newline at end of file diff --git a/squashthumbnailmaker/status.sh b/squashthumbnailmaker/status.sh new file mode 100755 index 0000000..42833ec --- /dev/null +++ b/squashthumbnailmaker/status.sh @@ -0,0 +1,15 @@ +#!/bin/bash +source "${AGENT_PATH}/common.sh" +_check_required_env_vars "CONTAINER_NAME" + +# Check if the image exists +if docker images --format "{{.Repository}}:{{.Tag}}" | grep -q "^${CONTAINER_NAME}_image:latest$"; then + # Check if any containers are currently running with this image + if docker ps --filter "ancestor=${CONTAINER_NAME}_image:latest" --format "{{.ID}}" | grep -q .; then + echo "Running" + else + echo "Stopped" + fi +else + echo "Unknown" +fi \ No newline at end of file diff --git a/squashthumbnailmaker/stop.sh b/squashthumbnailmaker/stop.sh new file mode 100755 index 0000000..96decb5 --- /dev/null +++ b/squashthumbnailmaker/stop.sh @@ -0,0 +1,8 @@ +#!/bin/bash +source "${AGENT_PATH}/common.sh" +_check_required_env_vars "CONTAINER_NAME" + +# Stop any running containers using this image +docker ps --filter "ancestor=${CONTAINER_NAME}_image:latest" --format "{{.ID}}" | xargs -r docker stop 2>/dev/null || true + +echo "Stopped any running ${CONTAINER_NAME} containers" \ No newline at end of file diff --git a/squashthumbnailmaker/uninstall.sh b/squashthumbnailmaker/uninstall.sh new file mode 100755 index 0000000..893eec7 --- /dev/null +++ b/squashthumbnailmaker/uninstall.sh @@ -0,0 +1,11 @@ +#!/bin/bash +source "${AGENT_PATH}/common.sh" +_check_required_env_vars "CONTAINER_NAME" + +# Remove any running containers using this image +docker ps -a --filter "ancestor=${CONTAINER_NAME}_image:latest" --format "{{.ID}}" | xargs -r docker rm -f 2>/dev/null || true + +# Remove the Docker image +docker rmi "${CONTAINER_NAME}_image:latest" 2>/dev/null || true + +echo "Uninstallation of ${CONTAINER_NAME} complete" \ No newline at end of file