#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Timestamp: "2025-10-18 09:55:56 (ywatanabe)"
# File: /home/ywatanabe/proj/scitex-code/src/scitex/capture/gif.py
# ----------------------------------------
from __future__ import annotations
import os
__FILE__ = "./src/scitex/capture/gif.py"
__DIR__ = os.path.dirname(__FILE__)
# ----------------------------------------
"""
GIF creation functionality for CAM.
Create animated GIFs from screenshot sequences for visual summaries.
"""
import glob
import re
from datetime import datetime
from pathlib import Path
from typing import List, Optional
from ._paths import get_gifs_dir, get_screenshots_dir
class GifCreator:
"""
Creates animated GIFs from screenshot sequences.
Useful for creating visual summaries of monitoring sessions or workflows.
"""
def __init__(self):
"""Initialize GIF creator."""
pass
def create_gif_from_session(
self,
session_id: str,
output_path: Optional[str] = None,
screenshot_dir: Optional[str] = None,
duration: float = 0.5,
optimize: bool = True,
max_frames: Optional[int] = None,
) -> Optional[str]:
"""
Create a GIF from a monitoring session's screenshots.
Args:
session_id: Session ID from monitoring (e.g., "20250823_104523")
output_path: Output GIF path (auto-generated if None)
screenshot_dir: Directory containing screenshots
duration: Duration per frame in seconds (default: 0.5)
optimize: Optimize GIF for smaller file size (default: True)
max_frames: Maximum number of frames to include (None = all)
Returns:
Path to created GIF file, or None if failed
"""
try:
if screenshot_dir is None:
screenshot_dir = get_screenshots_dir()
else:
screenshot_dir = Path(screenshot_dir).expanduser()
# Find all screenshots for this session
pattern = f"{session_id}_*.jpg"
jpg_files = list(screenshot_dir.glob(pattern))
# Also try PNG if no JPG files found
if not jpg_files:
pattern = f"{session_id}_*.png"
jpg_files = list(screenshot_dir.glob(pattern))
if not jpg_files:
print(f"No screenshots found for session {session_id}")
return None
# Sort by filename (which includes timestamp)
jpg_files.sort()
# Limit frames if specified
if max_frames and len(jpg_files) > max_frames:
# Take evenly spaced frames
step = len(jpg_files) // max_frames
jpg_files = jpg_files[::step][:max_frames]
if output_path is None:
output_path = get_gifs_dir() / f"{session_id}_summary.gif"
else:
output_path = Path(output_path)
return self.create_gif_from_files(
image_paths=[str(f) for f in jpg_files],
output_path=str(output_path),
duration=duration,
optimize=optimize,
)
except Exception as e:
print(f"Error creating GIF from session: {e}")
return None
def create_gif_from_files(
self,
image_paths: List[str],
output_path: str,
duration: float = 0.5,
optimize: bool = True,
loop: int = 0,
) -> Optional[str]:
"""
Create a GIF from a list of image files.
Args:
image_paths: List of image file paths
output_path: Output GIF path
duration: Duration per frame in seconds (default: 0.5)
optimize: Optimize GIF for smaller file size (default: True)
loop: Number of loops (0 = infinite, default: 0)
Returns:
Path to created GIF file, or None if failed
"""
try:
from PIL import Image
if not image_paths:
print("No image paths provided")
return None
# Load all images
images = []
for path in image_paths:
if not os.path.exists(path):
print(f"Image not found: {path}")
continue
try:
img = Image.open(path)
# Convert to RGB if necessary (for consistency)
if img.mode != "RGB":
img = img.convert("RGB")
images.append(img)
except Exception as e:
print(f"Error loading image {path}: {e}")
continue
if not images:
print("No valid images found")
return None
# Ensure all images have the same size (resize to first image size)
target_size = images[0].size
for i in range(1, len(images)):
if images[i].size != target_size:
images[i] = images[i].resize(target_size, Image.Resampling.LANCZOS)
# Create output directory if it doesn't exist
output_path = Path(output_path)
output_path.parent.mkdir(parents=True, exist_ok=True)
# Save as GIF
duration_ms = int(duration * 1000) # Convert to milliseconds
images[0].save(
str(output_path),
format="GIF",
save_all=True,
append_images=images[1:],
duration=duration_ms,
loop=loop,
optimize=optimize,
)
if output_path.exists():
file_size = output_path.stat().st_size / 1024 # KB
print(
f"📹 GIF created: {output_path} ({len(images)} frames, {file_size:.1f}KB)"
)
return str(output_path)
else:
return None
except ImportError:
print(
"PIL (Pillow) is required for GIF creation. Install with: pip install Pillow"
)
return None
except Exception as e:
print(f"Error creating GIF: {e}")
return None
def create_gif_from_pattern(
self,
pattern: str,
output_path: Optional[str] = None,
duration: float = 0.5,
optimize: bool = True,
max_frames: Optional[int] = None,
) -> Optional[str]:
"""
Create a GIF from files matching a glob pattern.
Args:
pattern: Glob pattern for image files (e.g., "/path/screenshots/*.jpg")
output_path: Output GIF path (auto-generated if None)
duration: Duration per frame in seconds (default: 0.5)
optimize: Optimize GIF for smaller file size (default: True)
max_frames: Maximum number of frames to include (None = all)
Returns:
Path to created GIF file, or None if failed
"""
try:
# Find matching files
files = glob.glob(pattern)
files.sort() # Sort alphabetically
if not files:
print(f"No files found matching pattern: {pattern}")
return None
# Limit frames if specified
if max_frames and len(files) > max_frames:
step = len(files) // max_frames
files = files[::step][:max_frames]
if output_path is None:
# Generate output path under the canonical gifs runtime dir
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_path = get_gifs_dir() / f"gif_summary_{timestamp}.gif"
return self.create_gif_from_files(
image_paths=files,
output_path=str(output_path),
duration=duration,
optimize=optimize,
)
except Exception as e:
print(f"Error creating GIF from pattern: {e}")
return None
def get_recent_sessions(
self, screenshot_dir: Optional[str] = None
) -> List[str]:
"""
Get list of recent monitoring session IDs.
Args:
screenshot_dir: Directory containing screenshots. Defaults
to ``$SCITEX_DIR/capture/runtime/screenshots/``.
Returns:
List of session IDs sorted by recency (newest first)
"""
try:
if screenshot_dir is None:
screenshot_dir = get_screenshots_dir()
else:
screenshot_dir = Path(screenshot_dir).expanduser()
if not screenshot_dir.exists():
return []
# Find all monitoring session files (format: SESSIONID_NNNN_timestamp.ext)
session_pattern = re.compile(r"^(\d{8}_\d{6})_\d{4}_.*\.(jpg|png)$")
sessions = set()
for file in screenshot_dir.iterdir():
if file.is_file():
match = session_pattern.match(file.name)
if match:
sessions.add(match.group(1))
# Sort by session ID (which includes timestamp)
return sorted(sessions, reverse=True)
except Exception as e:
print(f"Error getting recent sessions: {e}")
return []
def create_gif_from_recent_session(
self,
screenshot_dir: Optional[str] = None,
duration: float = 0.5,
optimize: bool = True,
max_frames: Optional[int] = None,
) -> Optional[str]:
"""
Create a GIF from the most recent monitoring session.
Args:
screenshot_dir: Directory containing screenshots
duration: Duration per frame in seconds (default: 0.5)
optimize: Optimize GIF for smaller file size (default: True)
max_frames: Maximum number of frames to include (None = all)
Returns:
Path to created GIF file, or None if failed
"""
sessions = self.get_recent_sessions(screenshot_dir)
if not sessions:
print("No monitoring sessions found")
return None
latest_session = sessions[0]
print(f"Creating GIF from latest session: {latest_session}")
return self.create_gif_from_session(
session_id=latest_session,
screenshot_dir=screenshot_dir,
duration=duration,
optimize=optimize,
max_frames=max_frames,
)
# Convenience functions for easy usage
def create_gif_from_session(session_id: str, **kwargs) -> Optional[str]:
"""Create GIF from monitoring session screenshots."""
creator = GifCreator()
return creator.create_gif_from_session(session_id, **kwargs)
def create_gif_from_files(
image_paths: List[str], output_path: str, **kwargs
) -> Optional[str]:
"""Create GIF from list of image files."""
creator = GifCreator()
return creator.create_gif_from_files(image_paths, output_path, **kwargs)
def create_gif_from_pattern(pattern: str, **kwargs) -> Optional[str]:
"""Create GIF from files matching glob pattern."""
creator = GifCreator()
return creator.create_gif_from_pattern(pattern, **kwargs)
def create_gif_from_latest_session(**kwargs) -> Optional[str]:
"""Create GIF from the most recent monitoring session."""
creator = GifCreator()
return creator.create_gif_from_recent_session(**kwargs)
# EOF