docs: Add 24 files
All checks were successful
Test and Publish Templates / test-and-publish (push) Successful in 48s

This commit is contained in:
Your Name
2025-09-22 23:57:38 +12:00
parent e27df71cd2
commit 2222a77070
12 changed files with 826 additions and 0 deletions

View File

@@ -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"]

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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 <video_file_or_directory>")
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())

23
squashthumbnailmaker/install.sh Executable file
View File

@@ -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."

14
squashthumbnailmaker/logs.sh Executable file
View File

@@ -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

View File

@@ -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 <video_file_or_directory>"
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

15
squashthumbnailmaker/start.sh Executable file
View File

@@ -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"

15
squashthumbnailmaker/status.sh Executable file
View File

@@ -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

8
squashthumbnailmaker/stop.sh Executable file
View File

@@ -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"

View File

@@ -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"