This commit is contained in:
2026-07-22 22:53:15 -07:00
parent d87fcb85be
commit 48132aa21c
6 changed files with 149 additions and 135 deletions
+30 -59
View File
@@ -1,14 +1,11 @@
import os
import base64
import mimetypes
import logging
import asyncio
import io
import datetime
import time
from typing import Any, Callable, Dict, List, Union
from pathlib import Path
from typing import Any, Dict, List
from PIL import Image, ImageDraw, ImageFont
from tools.utils import format_relative_time
from tools.utils import format_relative_time, ToolError, COMMON_FONTS, get_file_info_list, sort_file_list, get_paginated_list
logger = logging.getLogger("MattCP")
@@ -18,44 +15,32 @@ async def handle(args: Dict[str, Any]):
Grid: 10 columns x 7 rows (70 images total).
Sized for optimal token usage (1120 tokens) on llama.cpp.
"""
dir_path = args.get("path")
dir_path_str = args.get("path")
page = int(args.get("page", 1))
sort_by = args.get("sort_by", "mtime")
if not dir_path:
return {"text": "Error: Missing path argument"}
if not os.path.isdir(dir_path):
return {"text": f"Error: {dir_path} is not a directory."}
if not dir_path_str:
raise ToolError("Missing path argument")
dir_path = Path(dir_path_str)
if not dir_path.is_dir():
raise ToolError(f"{dir_path_str} is not a directory.")
exts = ('.png', '.jpg', '.jpeg', '.webp', '.bmp')
file_info_list = []
for f in os.listdir(dir_path):
if f.lower().endswith(exts):
full_path = os.path.join(dir_path, f)
try:
stats = os.stat(full_path)
file_info_list.append({
"name": f,
"path": full_path,
"mtime": stats.st_mtime,
"size": stats.st_size
})
except Exception as e:
logger.error(f"Could not stat {f}: {e}")
file_info_list = get_file_info_list(dir_path, extensions=exts)
if not file_info_list:
return {"text": "No supported images found in the directory."}
# Sorting
if sort_by == "mtime":
file_info_list.sort(key=lambda x: x["mtime"], reverse=True)
sort_label = "Date (newest first)"
elif sort_by == "size":
file_info_list.sort(key=lambda x: x["size"], reverse=True)
sort_label = "Size (largest first)"
else:
file_info_list.sort(key=lambda x: x["name"].lower())
sort_label = "Name (alphabetical)"
file_info_list = sort_file_list(file_info_list, sort_by)
sort_labels = {
"mtime": "Date (newest first)",
"size": "Size (largest first)",
"name": "Name (alphabetical)"
}
sort_label = sort_labels.get(sort_by, "Name (alphabetical)")
# Fixed grid dimensions for token optimization
COLS = 10
@@ -63,15 +48,12 @@ async def handle(args: Dict[str, Any]):
PAGE_SIZE = COLS * ROWS
total_files = len(file_info_list)
start_idx = (page - 1) * PAGE_SIZE
end_idx = start_idx + PAGE_SIZE
paged_files = file_info_list[start_idx:end_idx]
paged_files = get_paginated_list(file_info_list, page, PAGE_SIZE)
if not paged_files:
return {"text": f"No images found on page {page}."}
thumb_size = 192 # 192 / 48 = 4 patches per side
canvas_w = COLS * thumb_size
canvas_h = ROWS * thumb_size
@@ -80,21 +62,16 @@ async def handle(args: Dict[str, Any]):
# Font loading
font = None
font_paths = [
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
"/usr/share/fonts/truetype/freefont/FreeSansBold.ttf",
]
for path in font_paths:
if os.path.exists(path):
try:
font = ImageFont.truetype(path, 24)
break
except Exception:
continue
for font_candidate in COMMON_FONTS:
try:
font = ImageFont.truetype(font_candidate, 24)
break
except Exception:
continue
if font is None:
font = ImageFont.load_default()
start_idx = (page - 1) * PAGE_SIZE
for i, info in enumerate(paged_files):
full_path = info["path"]
row = i // COLS
@@ -102,7 +79,6 @@ async def handle(args: Dict[str, Any]):
x = col * thumb_size
y = row * thumb_size
global_index = start_idx + i + 1
try:
@@ -112,17 +88,13 @@ async def handle(args: Dict[str, Any]):
off_y = (thumb_size - img.height) // 2
canvas.paste(img, (x + off_x, y + off_y))
# Index number in lower-left corner
text = str(global_index)
if font:
bbox = draw.textbbox((0, 0), text, font=font)
text_w = bbox[2] - bbox[0]
text_h = bbox[3] - bbox[1]
text_w, text_h = bbox[2] - bbox[0], bbox[3] - bbox[1]
else:
text_w = len(text) * 8
text_h = 15
text_w, text_h = len(text) * 8, 15
# Semi-opaque background rectangle for the number
draw.rectangle([x, y + thumb_size - text_h - 5, x + text_w + 5, y + thumb_size - 2], fill=(0, 0, 0, 180))
draw.text((x + 2, y + thumb_size - text_h - 7), text, fill=(255, 255, 255), font=font)
@@ -136,7 +108,6 @@ async def handle(args: Dict[str, Any]):
total_pages = (total_files + PAGE_SIZE - 1) // PAGE_SIZE
# Range shown: first and last of the CURRENT PAGE
first_on_page = paged_files[0]
last_on_page = paged_files[-1]
@@ -151,7 +122,7 @@ async def handle(args: Dict[str, Any]):
last_val = last_on_page['name']
header_text = (
f"Directory: {dir_path}\n"
f"Directory: {dir_path_str}\n"
f"Page {page} of {total_pages} ({total_files} images total). Sorted by: {sort_label}\n"
f"Range shown: {first_val} ... {last_val}"
)