A maubot plugin for AI-powered image generation in Matrix rooms. Supports both NovelAI and RunPod/ComfyUI backends simultaneously, with LLM-based prompt expansion via OpenRouter.
- Dual backend support — NovelAI and RunPod/ComfyUI can run side-by-side, selectable per request
- LLM prompt generation — Three built-in presets (Dormouse, Fossa, Hippo) expand short descriptions into detailed image prompts via OpenRouter
- Per-model max_tokens — Each LLM preset can specify its own
max_tokenslimit - Raw mode — Bypass the LLM and pass your text directly as the image prompt
- Reply-based context — Reply to any message with
!raccto use that message as input - Weighted style system — Define style tag pools with weights; styles are randomly selected or chosen by index
- Batch generation — Generate 1–4 images per request (RunPod backend, sequential or native mode)
- Custom sizing — Override image dimensions per request
- Per-room rate limiting — Configurable cooldown between generation requests
- NovelAI rate limit handling — Automatic retry on 429/520 errors with a serialized request queue per API key; multiple keys are load-balanced round-robin
- Progress feedback — Reaction indicators while generating (hourglass → checkmark/X)
- Bot-safe output — All text responses are prefixed with
~so other bots ignore them
Build the plugin and upload to your maubot instance:
mbc build
# or build and upload directly:
mbc build -uThen create a plugin instance in the maubot management UI and configure it.
!racc a fox sitting in a forest
Reply to any message with !racc to use that message's content as input for image generation.
All flags are optional and can appear in any order before the prompt text:
| Flag | Short | Description |
|---|---|---|
--dormouse |
-d |
Use the Dormouse LLM preset |
--fossa |
-f |
Use the Fossa LLM preset |
--hippo |
-h |
Use the Hippo LLM preset |
--raw |
-r |
Skip LLM — use text as-is for the prompt |
--nai |
-n |
Use the NovelAI backend |
--runpod |
-rp |
Use the RunPod backend |
--style N |
-s N |
Select style by index (1-based, 0 = none) |
--size WxH |
Set image dimensions (e.g. --size 1024x768) |
|
--batch N |
-b N |
Generate N images, 1–4 (RunPod only) |
!racc a cyberpunk cityscape at sunset
!racc -f --nai a fox in a forest
!racc -s 3 --size 1024x768 -b 2 a cat on a rooftop
!racc -r exact prompt tags here, detailed, masterpiece
!racc -s 0 a landscape with no style applied
!racc help Show usage information
!racc styles [nai|runpod] List configured styles for a backend
!racc presets List available LLM presets
!racc status Show which backends are enabled
All settings are managed through the maubot config editor. Key sections:
| Key | Default | Description |
|---|---|---|
command_prefix |
racc |
Command name (e.g. !racc) |
default_backend |
runpod |
Default backend when none specified |
rate_limit_seconds |
10 |
Per-room cooldown between requests |
room_whitelist |
[] |
Room IDs where the bot is allowed (empty = all rooms) |
llm:
api_key: "sk-or-..."
base_url: "https://openrouter.ai/api/v1/chat/completions"
default_preset: dormouse
max_retries: 3
max_tokens: 400 # Global default
presets:
dormouse:
model: "google/gemini-2.0-flash-001"
system_prompt: "..."
temperature: 1.0
max_tokens: 400 # Per-preset override
reasoning_enabled: false
fossa:
model: "anthropic/claude-3.5-sonnet"
system_prompt: "..."
temperature: 1.2
max_tokens: 400
reasoning_enabled: false
hippo:
model: "openai/gpt-4o"
system_prompt: "..."
temperature: 0.9
max_tokens: 400
reasoning_enabled: falseThe max_tokens setting can be specified per preset to override the global default. This is useful when different models need different token limits (e.g. a reasoning model may need more tokens).
The system prompts ship as placeholders. Replace them with your own prompts that describe how you want the LLM to generate image descriptions.
prompt:
prefix_prompt: "" # Prepended to every generated prompt
suffix_prompt: "" # Appended to every generated prompt
banned_words: [] # Words filtered from all promptsnai:
enabled: false
api_key: ""
api_keys: [] # Extra keys for load balancing (round-robin across all keys)
model: "nai-diffusion-5-full"
default_size: "1216x832"
steps: 28
scale: 5.0
sampler: "k_euler_ancestral"
scheduler: "karras"
quality_toggle: true # true/false, or v5 preset name (standard/light/none)
decrisper: false
sm: false # v4.5 and earlier only — ignored on v5
sm_dyn: false # v4.5 and earlier only — ignored on v5
auto_smea: false # v5 only — replaces sm/sm_dyn
uc_preset: "light" # name (none/heavy/light/furryFocus/humanFocus) or index
cfg_rescale: 0.0
negative_prompt: ""
max_pixels: 1048576 # Anlas guard pixel limit
max_steps: 28 # Anlas guard step limit
style_text: "" # Backend-specific styles
prefix_prompt: "" # Appended to common prefix
suffix_prompt: "" # Appended to common suffixThe anlas guard automatically clamps oversized dimensions and step counts to stay within NovelAI's free-tier limits.
nai-diffusion-5-* models use the v5 request format (ucPresetId/qualityPresetId, autoSmea, legacy_uc) automatically. img2img is not yet supported on v5 — requests with -i/--img2img are rejected with a notice; use a v4.5 model for image-to-image.
NovelAI allows only one concurrent generation per API key, so requests are serialized per key: a request checks a key out of a fair FIFO pool and returns it when done, meaning concurrent requests automatically land on whichever keys are unblocked. Configure api_keys (in addition to api_key) with any number of keys for round-robin load balancing — with a single key, behaviour is identical to before (one request at a time). If the API returns 429 (rate limit) or 520 (server error), the plugin retries up to 10 times with a random 1–5s delay between attempts.
runpod:
enabled: true
api_key: ""
worker_id: ""
default_size: "1024x1024"
batch_mode: "sequential" # "sequential" or "native"
max_batch: 4
poll_interval: 2.0 # Seconds between status polls
poll_timeout: 120.0 # Max wait time for a job
comfyui_workflow_file: "" # Absolute path to external ComfyUI workflow JSON
style_text: ""
prefix_prompt: ""
suffix_prompt: ""The comfyui_workflow_file should point to an absolute filesystem path containing a ComfyUI workflow (API format) with {prompt} as a placeholder in the text input node. The plugin automatically randomises seeds and updates dimensions/batch size in latent image nodes.
A minimal starting point is included as example_workflow.json — copy it somewhere on your maubot host, edit the ckpt_name and negative prompt to taste, and set comfyui_workflow_file to its absolute path.
Batch modes:
sequential— Makes N parallel single-image requests (faster overall)native— Single request withbatch_size=Nin the workflow (fewer API calls)
Styles are defined per backend. Each line is a style chunk, optionally ending with :weight:
style_text: |
colored pencil (medium), traditional media :3
impressionism, pastel (artwork), painting :2
digital art, sharp details, vibrant colors- Higher weight = higher selection probability
- Lines without
:Ndefault to weight 1 - 90% chance a random style is applied, 10% chance none is
- Use
--style 0/-s 0to force no style - If the LLM-generated prompt already contains a style tag, random selection is skipped
The final prompt is assembled as:
[common prefix_prompt], [backend prefix_prompt]
[LLM output or raw text]
[selected style tags]
[common suffix_prompt], [backend suffix_prompt]
racconic_general/
__init__.py Main plugin — commands, rate limiting, orchestration
config.py Config class (BaseProxyConfig)
types.py Dataclasses (GenerationRequest, GenerationResult, etc.)
input_parser.py GNU-style flag extraction from raw input
style_engine.py Weighted random style selection
llm_client.py OpenRouter API client with retry logic
prompt_pipeline.py LLM → clean → ban words → prefix + style + suffix
image_sender.py Upload to Matrix media repo, send m.image events
backends/
__init__.py ImageBackend ABC
novelai.py NovelAI backend (anlas guard, v4 payload, ZIP extraction, serialized queue)
runpod.py RunPod/ComfyUI backend (workflow injection, polling, batch)
MIT