con un clic
st-gen-image-prompt
Build ST image gen prompt — booru tags from chat scene
Instalar con Codex o Claude Copia este prompt, pégalo en Codex, Claude u otro asistente, y deja que revise la página de la skill y la instale por ti.
Menú
Build ST image gen prompt — booru tags from chat scene
Instalar con Codex o Claude Copia este prompt, pégalo en Codex, Claude u otro asistente, y deja que revise la página de la skill y la instale por ti.
Basado en la clasificación ocupacional SOC
Search, download, and mine prompts from Civitai for Forge
Bake a completed RP arc into a SillyTavern lorebook as persistent memory. Run after each arc concludes.
Audit current SillyTavern config — explain settings, surface non-defaults, recommend changes for a goal. Read-only.
Convert a SillyTavern character into a user persona — or create a new persona from scratch with --new. Migrates/builds visuals, lorebook link, avatar.
Onboard a SillyTavern character — set SD visual baseline + audit. Optional: redistribute card fields, generate expressions, build lorebook.
Drain the home-server local-Forge image-ticket queue with deep Forge expertise (NoobAI, ADetailer, ControlNet, img2img, per-seed loops). USE on 'fulfill the image queue', 'process gen tickets', 'gen-fulfill', 'drain the forge queue', 'resolve image requests', or after gen-art queued a Forge ticket. The ONLY place the hub's Forge expertise lives.
| name | st-gen-image-prompt |
| model | sonnet |
| description | Build ST image gen prompt — booru tags from chat scene |
| argument-hint | [CharName] [--describe '<text>'] [--last N] [--no-clipboard] |
| allowed-tools | Bash, Read, mcp__st__st_get_character, mcp__st__st_get_settings, mcp__st__st_get_recent_chat |
Generate complete booru-tag image prompt cho SillyTavern. Replaces Magnum Mode 4 extraction. Output paste-ready for ST 🎨 Freestyle button (Mode FREE=6 pass-through).
Workflow:
--describe '<custom>'data/identity-baselines/<CharName>.txt~/.cache/)ST_DATA = /home/haint/Projects/home-server/sillytavern/data/default-user
SKILL_DIR = ~/Projects/home-server/.claude/skills/st-gen-image-prompt
SKILL_DATA = $SKILL_DIR/data
CACHE_DIR = ~/.cache/st-gen-image-prompt
DEFAULT_LAST_N = 10
TAG_DB_URL = https://raw.githubusercontent.com/DominikDoom/a1111-sd-webui-tagcomplete/main/tags/danbooru.csv
TAG_DB_TTL_DAYS = 30
MIN_TAG_COUNT = 50
Extract from $ARGUMENTS:
CharName = first non-flag positional. If absent → auto-detect from latest chats/<dir>/*.jsonl mtime.--describe '<text>' → custom scene (overrides chat reading)--last N → number of chat messages to read (default 10)--no-clipboard → skip auto-copy to clipboard (default = auto-copy enabled)Validate:
chats/<CharName>/ exists OR --describe given. If neither → error.Step 0 — Ensure tag DB cache:
import urllib.request, time
from pathlib import Path
CACHE = Path.home() / ".cache/st-gen-image-prompt"
CACHE.mkdir(parents=True, exist_ok=True)
csv_path = CACHE / "danbooru.csv"
ts_path = CACHE / ".last_fetch"
needs_fetch = not csv_path.exists()
if not needs_fetch and ts_path.exists():
needs_fetch = (time.time() - ts_path.stat().st_mtime) > 30 * 86400
if needs_fetch:
print("Fetching Danbooru tag DB (~5-10MB, one-time, cached 30d)...")
url = "https://raw.githubusercontent.com/DominikDoom/a1111-sd-webui-tagcomplete/main/tags/danbooru.csv"
urllib.request.urlretrieve(url, csv_path)
ts_path.touch()
print(f"✓ Cached: {csv_path} ({csv_path.stat().st_size // 1024}KB)")
Step 1 — Read char card via MCP (replaces legacy PNG tEXt parse):
import json
resp = mcp__st__st_get_character(name=CharName)
card = json.loads(resp) if isinstance(resp, str) else resp
d = card.get('data', card) # spec v3 nests under 'data'
print(f"NAME: {d.get('name', CharName)}")
print(f"DESCRIPTION: {d.get('description', '')[:1500]}")
print(f"PERSONALITY: {d.get('personality', '')[:300]}")
print(f"SCENARIO: {d.get('scenario', '')[:300]}")
Step 2 — Read persona via path-based MCP (each call returns a small subtree):
import json
avatar = json.loads(mcp__st__st_get_settings(path="user_avatar")) or ''
try:
persona_name = json.loads(mcp__st__st_get_settings(path=f"power_user.personas.{avatar}"))
except Exception:
persona_name = ''
try:
persona_desc = json.loads(mcp__st__st_get_settings(path=f"power_user.persona_descriptions.{avatar}.description"))
except Exception:
persona_desc = ''
print(f"PERSONA: {persona_name} (avatar={avatar})")
print(f"PERSONA_DESC: {persona_desc[:1000]}")
Step 3 — Read identity baseline:
from pathlib import Path
BASELINE_DIR = Path.home() / "Projects/home-server/.claude/skills/st-gen-image-prompt/data/identity-baselines"
char_baseline = ""
char_baseline_file = BASELINE_DIR / f"{CharName}.txt"
if char_baseline_file.exists():
char_baseline = char_baseline_file.read_text(encoding='utf-8').strip()
print(f"CHAR_BASELINE: {char_baseline}")
else:
print(f"WARN: No baseline for {CharName}. Run /st-setup first or skill will derive identity from card description.")
persona_baseline = ""
if persona_name:
persona_baseline_file = BASELINE_DIR / f"{persona_name}.txt"
if persona_baseline_file.exists():
persona_baseline = persona_baseline_file.read_text(encoding='utf-8').strip()
print(f"PERSONA_BASELINE: {persona_baseline}")
Step 4 — Read chat context via MCP (skip if --describe):
import json
# /api/chats/recent returns a list of {file_name, file_size, last_mes, ...}
recent_list = mcp__st__st_get_recent_chat(char_name=CharName)
recent_meta = json.loads(recent_list) if isinstance(recent_list, str) else recent_list
if not recent_meta:
print("ERROR: No chat files found. Use --describe '<text>' instead.")
else:
# Take top entry (most recent). To fetch full message array, use /api/chats/get
# via a thin Bash curl call (not yet exposed as MCP tool):
import subprocess
file_name = recent_meta[0].get('file_name', '').rsplit('.jsonl', 1)[0]
print(f"CHAT_FILE: {file_name}.jsonl")
# Fallback to direct file read for full chat content (the lightweight option until
# we add an st_get_chat MCP tool returning the full messages array):
from pathlib import Path
chat_path = Path("/home/haint/Projects/home-server/sillytavern/data/default-user/chats") / CharName / f"{file_name}.jsonl"
msgs = []
with open(chat_path, encoding='utf-8') as f:
for line in f:
try:
m = json.loads(line)
if 'mes' not in m or m.get('is_system'):
continue
role = "USER" if m.get('is_user') else "CHAR"
msgs.append(f"[{role}] {m['mes']}")
except Exception:
continue
LAST_N = 10 # or from --last arg
recent = msgs[-LAST_N:]
print(f"\n=== Recent {len(recent)} messages ===")
for m in recent:
print(m[:500])
Note: mcp__st__st_get_recent_chat returns chat metadata (file names, sizes), not full message bodies. The direct chats/{CharName}/{file}.jsonl read above pulls the actual messages. A future st_get_chat(char, file) MCP tool would replace this fully.
Step 5 — Read reference docs:
Use Read tool to load:
~/Projects/home-server/forge/knowledge/noobai-conventions.md — tag escape, prompt order, quality block (SHARED hub knowledge, also used by /gen-art)~/Projects/home-server/.claude/skills/st-gen-image-prompt/data/prompt-template.md — canonical exampleYou (Claude) have all context. Now produce a NoobAI XL-compatible booru-tag prompt.
looking_at_viewer and looking at viewer. Use spaces in output for readability (per Laxhar guide section 3).klee_(genshin_impact) → klee \(genshin_impact\)[[artist:wlop]] (brackets reduce weight, prevent style domination)[Subject count] → [Identity baseline] → [Scene/setting] → [Action] → [Expression] → [Outfit/items] → [Lighting/atmosphere] → [Quality block]
Example structure:
1girl, [identity baseline tags from data/identity-baselines/<CharName>.txt],
[scene tags: action, location, pose],
[expression: emotion, eye state, mouth state],
[outfit: clothing or undressed state],
[lighting: time, mood, light source],
(((masterpiece,best quality,newest,absurdres,highres)))
--describe chứa "no people", "empty", "background only", "no characters" → skip identitynaked, nude from scene)Pick AT MOST:
close-up | wide_shot | pov | side_view | from_below | from_above | dynamic_angle | foreshorteningx-ray | internal_view | cross-sectionNEVER stack 3+ view tags. Stacking causes split composition / inset panels / duplicate subjects.
Action:, Scene:)text, speech_bubble, dialogue, comic_panel, panels(((masterpiece,best quality,newest,absurdres,highres)))For EACH tag in the draft prompt:
def normalize_tag(tag):
"""Strip weights, escape, convert to canonical form."""
import re
t = tag.strip()
# Strip weight syntax
t = re.sub(r'^\(+', '', t)
t = re.sub(r'\)+$', '', t)
t = re.sub(r':\d+\.?\d*$', '', t)
t = re.sub(r'^\[+', '', t)
t = re.sub(r'\]+$', '', t)
# Strip escape backslashes
t = t.replace('\\(', '(').replace('\\)', ')')
# Skip artist: prefix
if t.startswith('artist:'):
t = t[7:]
# Convert spaces → underscores for CSV lookup
return t.lower().replace(' ', '_').strip()
def load_tag_db(csv_path):
db = {}
with open(csv_path, encoding='utf-8') as f:
for line in f:
parts = line.rstrip().split(',', 3)
if len(parts) < 3:
continue
try:
tag, cat, count = parts[0], int(parts[1]), int(parts[2])
except ValueError:
continue
db[tag] = (cat, count)
# Index aliases too
if len(parts) > 3:
aliases = parts[3].strip('"').split(',')
for alias in aliases:
a = alias.strip()
if a:
db[a] = (cat, count) # alias maps to same metadata
return db
# Verify each tag
db = load_tag_db(csv_path)
verified, rare, removed = [], [], []
SKIP_VERIFY = {'masterpiece', 'best_quality', 'newest', 'absurdres', 'highres'}
for tag in draft_tags:
norm = normalize_tag(tag)
if norm in SKIP_VERIFY:
verified.append(tag)
continue
if norm in db:
cat, count = db[norm]
if count >= 50:
verified.append(tag)
else:
rare.append((tag, count))
else:
removed.append(tag)
Skip verification for:
masterpiece, best_quality, etc.)--describe user input (assume user knows)Offline fallback: If ~/.cache/st-gen-image-prompt/danbooru.csv không tồn tại + no internet → skip verification, warn user "VERIFICATION SKIPPED".
Read ~/Projects/home-server/forge/knowledge/lora-catalog.md (SHARED hub knowledge) để biết LoRAs nào available + scene triggers nào → inject <lora:name:weight> syntax + trigger words vào prompt.
Always-on quality LoRAs — append to every gen:
<lora:anima-preview-3-masterpieces-v5:0.5>, <lora:AddMicroDetails_Illustrious_v6:0.4>, addmicrodetailsprompt_prefix ("masterpiece, very aesthetic") + add fine detail.Concept-triggered LoRAs — scan verified scene tags (Phase 2.5 output) against catalog:
Compatibility check:
Inject order in final prompt (after scene tags, before quality block):
[scene tags from Phase 2 + verification]
,
[concept LoRAs + their add_tags]
,
[always-on quality LoRAs + their triggers]
,
(((masterpiece,best quality,newest,absurdres,highres)))
If no match found → only inject always-on quality LoRAs.
from pathlib import Path
import re
CATALOG = Path.home() / "Projects/home-server/forge/knowledge/lora-catalog.md"
catalog_text = CATALOG.read_text(encoding='utf-8')
# Parse catalog rows (simplified — extract from markdown tables)
# Each row: keywords (list), lora (str), add_tags (str)
ALWAYS_ON = [
"<lora:anima-preview-3-masterpieces-v5:0.5>",
"<lora:AddMicroDetails_Illustrious_v6:0.4>",
"addmicrodetails",
]
scene_text = ", ".join(verified_tags).lower()
scene_text_norm = scene_text.replace(' ', '_') # normalize for keyword match
matched = [] # list of (priority_score, lora_block, add_tags_block)
for row in parse_catalog_rows(catalog_text):
hits = sum(1 for kw in row['keywords'] if kw.lower() in scene_text_norm or kw.lower() in scene_text)
if hits > 0:
matched.append((hits, row['lora'], row['add_tags']))
# Sort by priority, take top 2
matched.sort(key=lambda x: x[0], reverse=True)
top_matches = matched[:2]
# Compose injection block
parts = list(verified_tags)
for _, lora, add_tags in top_matches:
parts.append(lora)
if add_tags:
parts.append(add_tags)
parts.extend(ALWAYS_ON)
parts.append("(((masterpiece,best quality,newest,absurdres,highres)))")
final_prompt = ", ".join(parts)
Add to verification report:
═══ LoRA Injection ═══
✓ Always-on (2): Aesthetic Quality, Add Micro Details
✓ Concept matches (1): Parasite Horror Transformation [keywords: parasite, transformation]
⊘ No match: Oviposition (no egg/ovi keywords in scene)
import subprocess, shutil, os
def copy_to_clipboard(text):
"""Auto-copy to clipboard. Detect Wayland/X11 + fall back gracefully.
Returns: tool_name on success, None on failure."""
session = os.environ.get('XDG_SESSION_TYPE', '').lower()
# Try Wayland first (KDE Plasma default 2026)
if session == 'wayland' and shutil.which('wl-copy'):
try:
subprocess.run(['wl-copy'], input=text, text=True, check=True, timeout=5)
return 'wl-copy'
except Exception:
pass
# Fall back to X11 tools
for tool in ['xclip', 'xsel']:
if shutil.which(tool):
try:
args = [tool, '-selection', 'clipboard'] if tool == 'xclip' else [tool, '-b', '-i']
subprocess.run(args, input=text, text=True, check=True, timeout=5)
return tool
except Exception:
pass
return None
# Usage in skill:
if not no_clipboard_flag:
tool = copy_to_clipboard(final_prompt)
if tool:
clipboard_status = f"✓ Auto-copied to clipboard via {tool}"
else:
clipboard_status = "⚠ Auto-copy unavailable. Install: sudo dnf install wl-clipboard (Wayland) or xclip (X11)"
else:
clipboard_status = "⊘ Clipboard skipped (--no-clipboard flag)"
═══════════════════════════════════════
GENERATED IMAGE PROMPT — {CharName}
Context: {chat ({N} msgs from {chat_file}) | --describe '{text}'}
═══════════════════════════════════════
{final verified prompt with quality block}
═══════════════════════════════════════
TAG VERIFICATION
═══════════════════════════════════════
✓ Verified ({N} tags, count ≥50)
⚠ Rare ({M} tags, count <50): {tag1 (count), tag2 (count), ...}
✗ Removed ({K} tags, not in DB): {tag1, tag2, ...}
═══════════════════════════════════════
CLIPBOARD: {clipboard_status}
═══════════════════════════════════════
NEXT STEPS
═══════════════════════════════════════
1. ST: paste into input box (Ctrl+V — clipboard already has prompt)
2. Click 🎨 Freestyle button (or type /sd <Ctrl+V>)
NOTE: ST sẽ auto-prepend prompt_prefix + character_negative_prompts[{CharName}]
═══════════════════════════════════════
| Case | Handling |
|---|---|
| No baseline file | Warn, skill derives identity từ card description on the fly |
No chat history + no --describe | Error, ask user to provide --describe |
| Tag DB fetch fails (offline) | Warn "VERIFICATION SKIPPED", output unverified prompt |
| LLM hallucinates >5 tags | Show all in Removed section, suggest user override với --describe |
| Persona not set | Skip persona identity injection, output scene-only prompt |
| Char folder không có .png | Error "Character {CharName}.png not found in characters/" |
--describe rỗng | Same as no --describe flag |
| Clipboard tool not installed | Display prompt + warn "Install: sudo dnf install wl-clipboard". User copy thủ công bằng terminal selection. |
| Wayland session but wl-copy missing | Fall back try xclip/xsel (rare on Wayland but possible) |
/st-setup <CharName> → set initial char visual baseline (writes to data/identity-baselines/<CharName>.txt)/st-persona <CharName> → convert char → persona (also creates persona baseline)/st-arc-save → bake RP arc into lorebook (independent, separate from image gen)public/scripts/extensions/stable-diffusion/index.js getGenerationType() — /sd mode dispatchersillytavern/PROMPT-PLAYBOOK.md gotchas 5.32 (POV dedup), 5.37 (Magnum retired), 5.38 (char_prompts emptied), 5.39 (Mode FREE pass-through)