Compare commits

..
5 Commits
Author SHA1 Message Date
moosecrap 81fef17af2 Tag not found update 2026-07-26 22:59:25 -07:00
moosecrap aa59df60d9 Model info and res preset fix 2026-07-26 22:42:20 -07:00
moosecrap e9ee89f8a3 Contact sheet small size fixes 2026-07-26 22:17:34 -07:00
moosecrap f676a07a71 Model switch but not quite working yet 2026-07-26 22:17:15 -07:00
moosecrap 51a4708092 ZIT presets 2026-07-26 19:41:16 -07:00
7 changed files with 193 additions and 46 deletions
+21 -4
View File
@@ -3,12 +3,12 @@
# Description and Guide are mandatory. # Description and Guide are mandatory.
# Other keys should be the Labels found in the /info endpoint. # Other keys should be the Labels found in the /info endpoint.
["noobaiXLNAIXL_vPred10Version"] ["NoobAI XL"]
description = "Anime-style model, very stylistic and not aesthetic-tuned. Can do any NSFW." description = "Anime-style model, very good at specific artist styles and character knowledge. Not aesthetic-tuned: needs precise, explicit prompting for best results. Can do any sort of NSFW."
guide = """ guide = """
This model is not aesthetic tuned, it must be given explicit tags for everything. Omitted parts of the prompt will not default to something 'good'. This model is very fickle, you will almost always need to iterate on the prompt or resubmit to roll the best picture. This model is not aesthetic tuned, it must be given explicit tags for everything. Omitted parts of the prompt will not default to something 'good'. This model shows high variance, you can often get a very different image by just resubmitting the same prompt, so do not hesitate to try again.
Has very excellent understanding of characters and artists down to extremely niche. Unless prompting an original character, their name is enough to decribe their appearance completely except for clothing. Has very excellent understanding of characters and artists down to extremely niche. Unless prompting an original character, their name is enough to decribe their appearance completely except for clothing.
Accepts a list of comma-separated booru-style tags. Use spaces, not underscores, for tags. Accepts a list of comma-separated booru-style tags. Use spaces, not underscores, for tags. **Use the `search_tags` tool to verify your tags**.
Prompts MUST follow this format: <1girl/1boy/1other/solo/couple/(can use multiple)>, <character(s)>, <series>, <artist>, <tags> Prompts MUST follow this format: <1girl/1boy/1other/solo/couple/(can use multiple)>, <character(s)>, <series>, <artist>, <tags>
Every prompt MUST include every one of the above sections. Every prompt MUST include every one of the above sections.
Quality tags such as "masterpiece", "best quality", "very awa", are a LAST RESORT, they override the artist tags. If absolutely required they should be prepended. Quality tags such as "masterpiece", "best quality", "very awa", are a LAST RESORT, they override the artist tags. If absolutely required they should be prepended.
@@ -19,7 +19,9 @@ Has the SDXL problem with hands, works best if hand posture is explicitly prompt
No default background, so prompts should include "outdoors", "indoors", or something like "patterned background" etc. No default background, so prompts should include "outdoors", "indoors", or something like "patterned background" etc.
CFG Scale from 3.0 - 5.5 but the default 4.0 is usually fine. CFG Scale from 3.0 - 5.5 but the default 4.0 is usually fine.
""" """
filename = "noobaiXLNAIXL_vPred10Version.safetensors"
"Resolution Set" = "sdxl" "Resolution Set" = "sdxl"
preset = "xl"
"CFG Scale" = 4.0 "CFG Scale" = 4.0
"Hires. fix" = true "Hires. fix" = true
"Denoising strength" = 0.5 "Denoising strength" = 0.5
@@ -31,3 +33,18 @@ CFG Scale from 3.0 - 5.5 but the default 4.0 is usually fine.
"Sampling Method" = "Euler a" "Sampling Method" = "Euler a"
"Schedule Type" = "Uniform" "Schedule Type" = "Uniform"
"Rescale CFG" = 0.3 "Rescale CFG" = 0.3
["Z-Image-Turbo"]
description = "General image generation model. Aesthetic tuned, gets good results first try. NSFW is quite limited."
guide = """
This model is aesthetic tuned, regenerating with the same prompt will yield essentially the same image. Change the prompt before resubmitting.
Understands natural language very well. Characters can be described by naming them and using this name later in the prompt. Longer, detailed prompts work better.
"""
preset = "zit"
filename = "z_image_turbo_bf16.safetensors"
modules = ["ae.safetensors", "qwen_3_4b_abliterated.safetensors"]
"Resolution Set" = "2k"
"CFG Scale" = 1.0
"Sampling Steps" = 9
"Sampling Method" = "Euler"
"Schedule Type" = "Beta"
+6
View File
@@ -7,3 +7,9 @@ widescreen = "1344x768"
landscape = "1152x896" landscape = "1152x896"
square = "1024x1024" square = "1024x1024"
portrait = "896x1152" portrait = "896x1152"
[2k]
widescreen = "2048x1152"
landscape = "2048x1536"
square = "2048x2048"
portrait = "1536x2048"
+1 -1
View File
@@ -151,7 +151,7 @@ TOOL_REGISTRY = [
"resolution_preset": {"type": "string", "description": "A named resolution preset (e.g., 'square', 'portrait'). Available options depend on the model."}, "resolution_preset": {"type": "string", "description": "A named resolution preset (e.g., 'square', 'portrait'). Available options depend on the model."},
"cfg_scale": {"type": "number", "description": "CFG scale for prompt adherence. Usually should be left omitted to select the default."}, "cfg_scale": {"type": "number", "description": "CFG scale for prompt adherence. Usually should be left omitted to select the default."},
}, },
"required": ["model_name", "prompt"], "required": ["model_name", "prompt", "resolution_preset"],
}, },
handler=generate_image_handler handler=generate_image_handler
), ),
+24 -11
View File
@@ -2,7 +2,9 @@ import base64
import logging import logging
import io import io
import datetime import datetime
import math
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List from typing import Any, Dict, List
from PIL import Image, ImageDraw, ImageFont, ImageOps from PIL import Image, ImageDraw, ImageFont, ImageOps
import config import config
@@ -14,8 +16,7 @@ logger = logging.getLogger("MooseCP")
async def handle(args: Dict[str, Any]): async def handle(args: Dict[str, Any]):
""" """
Generates a contact sheet of images. Generates a contact sheet of images.
Grid: 10 columns x 7 rows (70 images total). Sized for optimal token usage according to config values.
Sized for optimal token usage (1120 tokens) on llama.cpp.
""" """
dir_path_str = args.get("path") dir_path_str = args.get("path")
page = int(args.get("page", 1)) page = int(args.get("page", 1))
@@ -44,10 +45,10 @@ async def handle(args: Dict[str, Any]):
} }
sort_label = sort_labels.get(sort_by, "Name (alphabetical)") sort_label = sort_labels.get(sort_by, "Name (alphabetical)")
# Fixed grid dimensions for token optimization # Grid dimensions for token optimization (see config.py)
COLS = config.CONTACT_SHEET_COLS MAX_COLS = config.CONTACT_SHEET_COLS
ROWS = config.CONTACT_SHEET_ROWS MAX_ROWS = config.CONTACT_SHEET_ROWS
PAGE_SIZE = COLS * ROWS PAGE_SIZE = MAX_COLS * MAX_ROWS
total_files = len(file_info_list) total_files = len(file_info_list)
@@ -56,9 +57,21 @@ async def handle(args: Dict[str, Any]):
if not paged_files: if not paged_files:
return {"text": f"No images found on page {page}."} return {"text": f"No images found on page {page}."}
thumb_size = 192 # 192 / 48 = 4 patches per side # Dynamic grid sizing to avoid blank space
canvas_w = COLS * thumb_size n_images = len(paged_files)
canvas_h = ROWS * thumb_size thumb_size = config.CONTACT_SHEET_THUMB_SIZE
if n_images <= config.CONTACT_SHEET_ROWS ** 2:
# Smallest square-ish grid that fits all
cols = math.ceil(math.sqrt(n_images))
rows = math.ceil(n_images / cols)
else:
# Lock rows, expand columns
rows = config.CONTACT_SHEET_ROWS
cols = math.ceil(n_images / config.CONTACT_SHEET_ROWS)
canvas_w = cols * thumb_size
canvas_h = rows * thumb_size
canvas = Image.new('RGB', (canvas_w, canvas_h), (30, 30, 30)) canvas = Image.new('RGB', (canvas_w, canvas_h), (30, 30, 30))
draw = ImageDraw.Draw(canvas) draw = ImageDraw.Draw(canvas)
@@ -87,8 +100,8 @@ async def handle(args: Dict[str, Any]):
start_idx = (page - 1) * PAGE_SIZE start_idx = (page - 1) * PAGE_SIZE
for i, info in enumerate(paged_files): for i, info in enumerate(paged_files):
full_path = info["path"] full_path = info["path"]
row = i // COLS row = i // cols
col = i % COLS col = i % cols
x = col * thumb_size x = col * thumb_size
y = row * thumb_size y = row * thumb_size
+61 -3
View File
@@ -1,9 +1,60 @@
import requests import requests
import base64 import base64
import asyncio
import config import config
from typing import Any, Dict, List from typing import Any, Dict, List
from tools.utils import ToolError, load_toml from tools.utils import ToolError, load_toml
async def ensure_model_state(model_name: str, model_preset: Dict[str, Any]):
"""
Checks the current server state and switches model/modules if they differ from the preset.
"""
try:
config_resp = await asyncio.to_thread(requests.get, f"{config.SD_URL}/config", timeout=10)
config_resp.raise_for_status()
cfg_data = config_resp.json()
components = cfg_data.get("components", [])
# Extract current state from components
current_state = {}
for comp in components:
elem_id = comp.get("props", {}).get("elem_id")
if elem_id:
current_state[elem_id] = comp.get("props", {}).get("value")
# 1. Check and change Checkpoint
active_ckpt = current_state.get("setting_sd_model_checkpoint")
# Use 'filename' from TOML if available, otherwise fall back to the model_name key
target_ckpt = model_preset.get("filename", model_name)
if active_ckpt != target_ckpt:
preset = model_preset.get("preset", "xl")
await asyncio.to_thread(
requests.post,
f"{config.SD_URL}/api/predict/checkpoint_change",
json={"data": [target_ckpt, preset]},
timeout=30
)
# 2. Check and change VAE / Text Encoders
active_modules = current_state.get("setting_sd_modules", [])
target_modules = model_preset.get("modules", [])
if active_modules != target_modules:
preset = model_preset.get("preset", "xl")
await asyncio.to_thread(
requests.post,
f"{config.SD_URL}/api/predict/modules_change",
json={"data": [target_modules, preset]},
timeout=30
)
except Exception as e:
# We log this but don't necessarily raise a ToolError unless the generation itself fails,
# as the server might still be able to generate if it's just a state-check failure.
print(f"Warning: Failed to sync model state: {e}")
async def handle(args: Dict[str, Any]) -> List[Dict[str, Any]]: async def handle(args: Dict[str, Any]) -> List[Dict[str, Any]]:
""" """
Generates an image using the specified model and parameters. Generates an image using the specified model and parameters.
@@ -17,9 +68,13 @@ async def handle(args: Dict[str, Any]) -> List[Dict[str, Any]]:
if not model_name: if not model_name:
raise ToolError("The 'model_name' argument is required. Use get_model_info to see available models.") raise ToolError("The 'model_name' argument is required. Use get_model_info to see available models.")
res_preset_name = args.get("resolution_preset")
if not res_preset_name:
raise ToolError("The 'resolution_preset' argument is required. Use get_model_info to see available resolutions for the chosen model.")
# 2. FETCH CURRENT SERVER DEFAULTS & MAP LABELS # 2. FETCH CURRENT SERVER DEFAULTS & MAP LABELS
try: try:
info_resp = requests.get(f"{config.SD_URL}/info", timeout=10) info_resp = await asyncio.to_thread(requests.get, f"{config.SD_URL}/info", timeout=10)
info_resp.raise_for_status() info_resp.raise_for_status()
info_data = info_resp.json() info_data = info_resp.json()
params_info = info_data["named_endpoints"]["/txt2img"]["parameters"] params_info = info_data["named_endpoints"]["/txt2img"]["parameters"]
@@ -55,6 +110,9 @@ async def handle(args: Dict[str, Any]) -> List[Dict[str, Any]]:
model_preset = models_cfg[model_name] model_preset = models_cfg[model_name]
# Ensure server state (Checkpoint, VAE, etc.) matches the preset before generating
await ensure_model_state(model_name, model_preset)
# 4. MERGE PIPELINE # 4. MERGE PIPELINE
for key, value in model_preset.items(): for key, value in model_preset.items():
if key in ["description", "guide", "Resolution Set"]: if key in ["description", "guide", "Resolution Set"]:
@@ -111,7 +169,7 @@ async def handle(args: Dict[str, Any]) -> List[Dict[str, Any]]:
# 6. EXECUTE GENERATION # 6. EXECUTE GENERATION
try: try:
gen_payload = {"data": payload} gen_payload = {"data": payload}
gen_resp = requests.post(f"{config.SD_URL}/api/txt2img", json=gen_payload, timeout=300) gen_resp = await asyncio.to_thread(requests.post, f"{config.SD_URL}/api/txt2img", json=gen_payload, timeout=300)
gen_resp.raise_for_status() gen_resp.raise_for_status()
res_data = gen_resp.json() res_data = gen_resp.json()
@@ -134,7 +192,7 @@ async def handle(args: Dict[str, Any]) -> List[Dict[str, Any]]:
proxy_url = f"http://localhost:{config.PORT}/file{raw_path}" proxy_url = f"http://localhost:{config.PORT}/file{raw_path}"
# Download the image and convert to base64 for the AI's vision # Download the image and convert to base64 for the AI's vision
img_resp = requests.get(sd_img_url, timeout=30) img_resp = await asyncio.to_thread(requests.get, sd_img_url, timeout=30)
img_resp.raise_for_status() img_resp.raise_for_status()
b64_data = base64.b64encode(img_resp.content).decode('utf-8') b64_data = base64.b64encode(img_resp.content).decode('utf-8')
+12 -1
View File
@@ -26,7 +26,18 @@ async def handle(args: Dict[str, Any]) -> Dict[str, Any]:
} }
if model_name not in models: if model_name not in models:
raise ToolError(f"Model '{model_name}' not found in presets. Available models: {', '.join(models.keys())}") # Return the full catalog if the specific model isn't found
catalog = []
for name, data in models.items():
catalog.append({
"name": name,
"description": data.get("description", "No description provided.")
})
return {
"text": f"Model '{model_name}' not found.\n\nAvailable models:\n\n" +
"\n".join([f"- {m['name']}: {m['description']}" for m in catalog]) +
"\n\nTo get a detailed prompting guide and available resolutions for a specific model, call this tool again with the 'model_name' argument."
}
model_data = models[model_name] model_data = models[model_name]
res_set_name = model_data.get("Resolution Set") res_set_name = model_data.get("Resolution Set")
+68 -26
View File
@@ -1,23 +1,22 @@
import csv import csv
import os import os
from typing import List, Dict, Any import config
import difflib
from typing import List, Dict, Any, Optional
from tools.utils import ToolError, format_count, get_type_suffix from tools.utils import ToolError, format_count, get_type_suffix
# Tag database path is now managed in config.py # Global cache to avoid reading the CSV from disk on every request
async def handle(args: Dict[str, Any]) -> Dict[str, Any]: _TAG_CACHE: Optional[List[Dict[str, Any]]] = None
"""
Searches the Danbooru tag database for tags matching a query.
Returns the most popular tags including alias matches.
"""
query = args.get("query", "").lower()
if not query:
raise ToolError("The 'query' argument is required.")
def _load_tags() -> List[Dict[str, Any]]:
"""Reads the Danbooru tag CSV and caches it in memory."""
tag_file_path = config.TAG_DATABASE_PATH tag_file_path = config.TAG_DATABASE_PATH
tags = []
if not os.path.exists(tag_file_path): if not os.path.exists(tag_file_path):
# We raise ToolError here because it's a fatal configuration issue
raise ToolError(f"Tag database not found at {tag_file_path}. Please ensure the tag-autocomplete extension is installed.") raise ToolError(f"Tag database not found at {tag_file_path}. Please ensure the tag-autocomplete extension is installed.")
matches = []
try: try:
with open(tag_file_path, mode='r', encoding='utf-8') as f: with open(tag_file_path, mode='r', encoding='utf-8') as f:
reader = csv.reader(f) reader = csv.reader(f)
@@ -25,26 +24,52 @@ async def handle(args: Dict[str, Any]) -> Dict[str, Any]:
if not row or len(row) < 3: if not row or len(row) < 3:
continue continue
name = row[0].lower() name = row[0]
tag_type = row[1] tag_type = row[1]
count = int(row[2]) if row[2].isdigit() else 0 count = int(row[2]) if row[2].isdigit() else 0
aliases = row[3].lower().split(',') if len(row) > 3 else [] aliases = row[3].lower().split(',') if len(row) > 3 else []
# Check for match in name or aliases tags.append({
is_direct = query in name "name": name,
is_alias = any(query in alias.strip() for alias in aliases) "name_lower": name.lower(),
"type": tag_type,
if is_direct or is_alias: "count": count,
matches.append({ "aliases": aliases
"name": row[0], })
"type": tag_type,
"count": count,
"matched_via": name if is_direct else "alias",
"alias_match": "" if is_direct else next((a.strip() for a in aliases if query in a.strip()), query)
})
except Exception as e: except Exception as e:
raise ToolError(f"Error reading tag database: {str(e)}") raise ToolError(f"Error reading tag database: {str(e)}")
return tags
async def handle(args: Dict[str, Any]) -> Dict[str, Any]:
"""
Searches the Danbooru tag database for tags matching a query.
Returns the most popular tags including alias matches.
"""
global _TAG_CACHE
query = args.get("query", "").lower()
if not query:
raise ToolError("The 'query' argument is required.")
# Lazy-load the tags into memory
if _TAG_CACHE is None:
_TAG_CACHE = _load_tags()
matches = []
for tag in _TAG_CACHE:
# Check for match in name or aliases
is_direct = query in tag["name_lower"]
is_alias = any(query in alias.strip() for alias in tag["aliases"])
if is_direct or is_alias:
matches.append({
"name": tag["name"],
"type": tag["type"],
"count": tag["count"],
"matched_via": "name" if is_direct else "alias",
"alias_match": "" if is_direct else next((a.strip() for a in tag["aliases"] if query in a.strip()), query)
})
# Sort by count descending # Sort by count descending
matches.sort(key=lambda x: x["count"], reverse=True) matches.sort(key=lambda x: x["count"], reverse=True)
@@ -63,6 +88,23 @@ async def handle(args: Dict[str, Any]) -> Dict[str, Any]:
results.append(line) results.append(line)
if not results: if not results:
return {"text": f"No tags found matching '{query}'."} # Attempt to find similar tags using difflib
all_names_lower = [t["name_lower"] for t in _TAG_CACHE]
suggestions_lower = difflib.get_close_matches(query, all_names_lower, n=10, cutoff=0.5)
if not suggestions_lower:
return {"text": f"No tags found matching '{query}'."}
# Map lowercased suggestions back to original tag objects
suggestions = []
for s_lower in suggestions_lower:
# Find the first tag that matches this lowercased name
tag = next((t for t in _TAG_CACHE if t["name_lower"] == s_lower), None)
if tag:
count_fmt = format_count(str(tag["count"]))
type_sfx = get_type_suffix(tag["type"])
suggestions.append(f"{tag['name']} ({count_fmt}){type_sfx}")
return {"text": f"No exact matches for '{query}'. Did you mean:\n" + "\n".join(suggestions)}
return {"text": "Top matches:\n" + "\n".join(results)} return {"text": "Top matches:\n" + "\n".join(results)}