README, config file
This commit is contained in:
parent
504794f9a8
commit
05e70a7fc4
63
README.md
Normal file
63
README.md
Normal file
@ -0,0 +1,63 @@
|
||||
# MattCP Image Server
|
||||
|
||||
A Model Context Protocol (MCP) server designed to provide LLMs with efficient, token-optimized visual access to image directories. Instead of dumping full-resolution images (which waste tokens and cause context overflow), MattCP provides a hierarchical workflow: **List $\rightarrow$ Scan $\rightarrow$ Preview $\rightarrow$ Inspect**.
|
||||
|
||||
## ⚠️ AI SLOP DISCLAIMER
|
||||
This server is designed to facilitate the interaction between AI models and image datasets. While it provides tools for "previewing" and "inspecting" images, remember that AI models can still hallucinate visual details, especially when working with low-resolution previews. **Always verify critical visual information with the `read_image` tool (full resolution) before drawing final conclusions.**
|
||||
|
||||
## Tools Overview
|
||||
|
||||
### 📁 `list_directory`
|
||||
Provides a simplified file-browser view of a directory.
|
||||
* **Best for**: Getting a sense of the files present and their basic metadata (size, date).
|
||||
* **Features**: Pagination and sorting by name, size, or modification date.
|
||||
|
||||
### 🖼️ `contact_sheet`
|
||||
Generates a high-density grid of thumbnails.
|
||||
* **Best for**: Quickly scanning hundreds of images to find a specific one or get a general "vibe" of a folder.
|
||||
* **Note**: This tool is paginated. You must iterate through pages to see all images in a folder.
|
||||
* **Workflow**: Use the indices shown on the contact sheet to call `preview_image`.
|
||||
|
||||
### 🔍 `preview_image`
|
||||
Provides medium-detail previews optimized for the model's token budget.
|
||||
* **Best for**: Comparing a few candidates, inspecting specific details, or selecting a "favorite" image.
|
||||
* **Efficiency**: Automatically calculates dimensions to fit the model's patch size (e.g., 48px patches for Gemma 4), ensuring maximum detail without wasting tokens on padding.
|
||||
* **Workflow**: Pass indices from the `contact_sheet` or a direct file path.
|
||||
|
||||
### 📖 `read_png_metadata`
|
||||
Extracts AI generation parameters from PNG files.
|
||||
* **Best for**: Retrieving prompts, seeds, and model hashes from AI-generated images.
|
||||
|
||||
### 📸 `read_image`
|
||||
Returns the full-resolution image.
|
||||
* **Best for**: Final confirmation or deep visual analysis where every pixel counts.
|
||||
|
||||
---
|
||||
|
||||
## Installation & Requirements
|
||||
|
||||
### Dependencies
|
||||
This server requires Python 3.10+ and the following packages:
|
||||
* `uvicorn`: ASGI server for the SSE transport.
|
||||
* `starlette`: Lightweight ASGI framework.
|
||||
* `Pillow`: Image processing and thumbnail generation.
|
||||
|
||||
```bash
|
||||
pip install uvicorn starlette Pillow
|
||||
```
|
||||
|
||||
### Setup
|
||||
1. Clone this repository to your server.
|
||||
2. (Optional) Edit `config.py` to adjust the server port, log level, or token budgets if you are using a model other than Gemma 4.
|
||||
3. Run the server:
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
## Configuration (`config.py`)
|
||||
|
||||
You can tune the server's behavior in `config.py`:
|
||||
- **`PATCH_SIZE`**: Set this to `(clip.vision.patch_size * n_merge)` for your specific model to ensure token-perfect resizing.
|
||||
- **`PREVIEW_TOKEN_BUDGET`**: Controls how many tokens the `preview_image` tool aims for (default: 70).
|
||||
- **`CONTACT_SHEET_COLS/ROWS`**: Adjust the grid size to fit within your model's maximum context window.
|
||||
- **`IMAGE_QUALITY`**: Adjust JPEG compression (1-100).
|
||||
47
config.py
Normal file
47
config.py
Normal file
@ -0,0 +1,47 @@
|
||||
import logging
|
||||
|
||||
# --- Server & Logs ---
|
||||
HOST = "127.0.0.1"
|
||||
PORT = 8000
|
||||
LOG_LEVEL = "WARNING" # Options: "DEBUG", "INFO", "WARNING", "ERROR"
|
||||
LOG_FILE = "debug.log"
|
||||
|
||||
# --- Model Specific Token Tuning (Tuned for Gemma 4) ---
|
||||
# Patch size is typically (clip.vision.patch_size * n_merge)
|
||||
# For Gemma 4, this is 48px.
|
||||
PATCH_SIZE = 48
|
||||
|
||||
# The target token count for a single image preview.
|
||||
# The actual budget will be this or greater, but as small as possible.
|
||||
PREVIEW_TOKEN_BUDGET = 70
|
||||
|
||||
# Contact sheet grid optimized to fit within Gemma 4's 1120 max token budget
|
||||
# (10 cols * 7 rows) = 70 images.
|
||||
# Each thumb (192px) is 4x4 patches.
|
||||
# 70 images * (4*4) = 1120 tokens.
|
||||
CONTACT_SHEET_COLS = 10
|
||||
CONTACT_SHEET_ROWS = 7
|
||||
CONTACT_SHEET_THUMB_SIZE = 192
|
||||
|
||||
# --- Image Quality ---
|
||||
# JPEG image quality (usually max 95).
|
||||
# See: https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html#jpeg-saving
|
||||
IMAGE_QUALITY = 95
|
||||
|
||||
# --- Font Configuration ---
|
||||
# Generic font names for cross-platform compatibility
|
||||
# Pillow's truetype() can often find these by name in the system path
|
||||
SYSTEM_FONT_NAMES = [
|
||||
"arialbd.ttf",
|
||||
"DejaVuSans-Bold",
|
||||
"LiberationSans-Bold",
|
||||
"Verdana",
|
||||
"Tahoma",
|
||||
]
|
||||
|
||||
# Fallback absolute paths for Linux systems
|
||||
FALLBACK_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",
|
||||
]
|
||||
12
main.py
12
main.py
@ -10,11 +10,14 @@ from starlette.middleware.cors import CORSMiddleware
|
||||
|
||||
from mcp_logic import MCPServer
|
||||
from tools import TOOL_REGISTRY
|
||||
import config
|
||||
|
||||
|
||||
# --- Configuration & Logging ---
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
filename="debug.log",
|
||||
level=getattr(logging, config.LOG_LEVEL),
|
||||
filename=config.LOG_FILE,
|
||||
|
||||
filemode="a",
|
||||
format="%(asctime)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
@ -90,11 +93,12 @@ app.add_middleware(
|
||||
if __name__ == "__main__":
|
||||
config = uvicorn.Config(
|
||||
app=app,
|
||||
host="127.0.0.1",
|
||||
port=8000,
|
||||
host=config.HOST,
|
||||
port=config.PORT,
|
||||
log_level="info",
|
||||
timeout_graceful_shutdown=2 # Reduced from 5 to 2 seconds
|
||||
)
|
||||
|
||||
server = uvicorn.Server(config)
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
|
||||
5
start.sh
5
start.sh
@ -1,5 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Start the MCP Image Server in the background
|
||||
nohup /home/matt/code/llama.cpp/build/bin/mcp-image-server/venv/bin/python3 /home/matt/code/llama.cpp/build/bin/mcp-image-server/server.py > /home/matt/code/llama.cpp/build/bin/mcp-image-server/server.log 2>&1 &
|
||||
echo "MCP Image Server started in background on port 8000"
|
||||
echo "Log file: /home/matt/code/llama.cpp/build/bin/mcp-image-server/server.log"
|
||||
@ -5,7 +5,9 @@ import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
from PIL import Image, ImageDraw, ImageFont, ImageOps
|
||||
from tools.utils import format_relative_time, ToolError, COMMON_FONTS, get_file_info_list, sort_file_list, get_paginated_list
|
||||
import config
|
||||
from tools.utils import format_relative_time, ToolError, get_file_info_list, sort_file_list, get_paginated_list
|
||||
|
||||
|
||||
logger = logging.getLogger("MattCP")
|
||||
|
||||
@ -43,9 +45,10 @@ async def handle(args: Dict[str, Any]):
|
||||
sort_label = sort_labels.get(sort_by, "Name (alphabetical)")
|
||||
|
||||
# Fixed grid dimensions for token optimization
|
||||
COLS = 10
|
||||
ROWS = 7
|
||||
COLS = config.CONTACT_SHEET_COLS
|
||||
ROWS = config.CONTACT_SHEET_ROWS
|
||||
PAGE_SIZE = COLS * ROWS
|
||||
|
||||
|
||||
total_files = len(file_info_list)
|
||||
paged_files = get_paginated_list(file_info_list, page, PAGE_SIZE)
|
||||
@ -62,15 +65,25 @@ async def handle(args: Dict[str, Any]):
|
||||
|
||||
# Font loading
|
||||
font = None
|
||||
for font_candidate in COMMON_FONTS:
|
||||
# Try generic system font names first
|
||||
for font_name in config.SYSTEM_FONT_NAMES:
|
||||
try:
|
||||
font = ImageFont.truetype(font_candidate, 24)
|
||||
font = ImageFont.truetype(font_name, 24)
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
# Fallback to absolute paths
|
||||
if font is None:
|
||||
for font_path in config.FALLBACK_FONT_PATHS:
|
||||
try:
|
||||
font = ImageFont.truetype(font_path, 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"]
|
||||
@ -106,8 +119,8 @@ async def handle(args: Dict[str, Any]):
|
||||
draw.text((x + 5, y + thumb_size // 2), "Error", fill=(255, 0, 0), font=font)
|
||||
|
||||
buf = io.BytesIO()
|
||||
# Save as JPEG quality 95 to save data
|
||||
canvas.save(buf, format='JPEG', quality=95)
|
||||
# Save as JPEG to save data
|
||||
canvas.save(buf, format='JPEG', quality=config.IMAGE_QUALITY)
|
||||
img_data = base64.b64encode(buf.getvalue()).decode("utf-8")
|
||||
|
||||
total_pages = (total_files + PAGE_SIZE - 1) // PAGE_SIZE
|
||||
|
||||
@ -6,24 +6,29 @@ import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Tuple
|
||||
from PIL import Image, ImageOps
|
||||
import config
|
||||
from tools.utils import format_relative_time, ToolError, get_file_info_list, sort_file_list, parse_indices
|
||||
|
||||
|
||||
logger = logging.getLogger("MattCP")
|
||||
|
||||
def calculate_patch_dimensions(orig_w: int, orig_h: int, target_tokens: int = 70):
|
||||
def calculate_patch_dimensions(orig_w: int, orig_h: int, target_tokens: int = None):
|
||||
"""
|
||||
Calculates optimal pixel dimensions to hit a token budget of at least target_tokens.
|
||||
Validates budget based on actual rounded pixel dimensions to avoid float precision errors.
|
||||
"""
|
||||
if target_tokens is None:
|
||||
target_tokens = config.PREVIEW_TOKEN_BUDGET
|
||||
|
||||
ar = orig_w / orig_h
|
||||
|
||||
# Case A: Width is the anchor (non-padded)
|
||||
w_anchor = 1
|
||||
while True:
|
||||
w_px = w_anchor * 48
|
||||
w_px = w_anchor * config.PATCH_SIZE
|
||||
h_px = round(w_px / ar)
|
||||
# Calculate actual tokens based on resulting pixel dimensions
|
||||
tokens = math.ceil(w_px / 48) * math.ceil(h_px / 48)
|
||||
tokens = math.ceil(w_px / config.PATCH_SIZE) * math.ceil(h_px / config.PATCH_SIZE)
|
||||
if tokens >= target_tokens:
|
||||
res_a = {"w": w_px, "h": h_px, "tokens": tokens}
|
||||
break
|
||||
@ -32,10 +37,10 @@ def calculate_patch_dimensions(orig_w: int, orig_h: int, target_tokens: int = 70
|
||||
# Case B: Height is the anchor (non-padded)
|
||||
h_anchor = 1
|
||||
while True:
|
||||
h_px = h_anchor * 48
|
||||
h_px = h_anchor * config.PATCH_SIZE
|
||||
w_px = round(h_px * ar)
|
||||
# Calculate actual tokens based on resulting pixel dimensions
|
||||
tokens = math.ceil(w_px / 48) * math.ceil(h_px / 48)
|
||||
tokens = math.ceil(w_px / config.PATCH_SIZE) * math.ceil(h_px / config.PATCH_SIZE)
|
||||
if tokens >= target_tokens:
|
||||
res_b = {"w": w_px, "h": h_px, "tokens": tokens}
|
||||
break
|
||||
@ -100,7 +105,7 @@ async def handle(args: Dict[str, Any]):
|
||||
thumb.thumbnail((target_w, target_h), Image.Resampling.LANCZOS)
|
||||
|
||||
buf = io.BytesIO()
|
||||
thumb.save(buf, format='JPEG', quality=95)
|
||||
thumb.save(buf, format='JPEG', quality=config.IMAGE_QUALITY)
|
||||
img_data = base64.b64encode(buf.getvalue()).decode("utf-8")
|
||||
|
||||
# Gather info
|
||||
|
||||
@ -3,6 +3,7 @@ import datetime
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
|
||||
class ToolError(Exception):
|
||||
"""Custom exception for tool-related errors to be caught by the MCP server."""
|
||||
pass
|
||||
@ -39,20 +40,6 @@ def format_relative_time(timestamp: float) -> str:
|
||||
dt = datetime.datetime.fromtimestamp(timestamp)
|
||||
return dt.strftime('%Y-%m-%d')
|
||||
|
||||
# Cross-platform font candidates
|
||||
# ImageFont.truetype can often find these by name on Windows,
|
||||
# or we can provide paths for Linux.
|
||||
COMMON_FONTS = [
|
||||
# Linux paths
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
|
||||
"/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
|
||||
"/usr/share/fonts/truetype/freefont/FreeSansBold.ttf",
|
||||
# Windows filenames (Pillow often finds these in the system path)
|
||||
"arialbd.ttf",
|
||||
"calibrib.ttf",
|
||||
"verdanab.ttf",
|
||||
"tahomabd.ttf",
|
||||
]
|
||||
|
||||
def get_file_info_list(path: Path, extensions: Optional[tuple] = None) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user