In Starlette 0.52+, Mount('/') returns Match.FULL for every WebSocket
scope. If APIWebSocketRoute('/ws') is somehow not matched first, the
StaticFiles mount catches the connection and crashes with:
assert scope["type"] == "http" # AssertionError
Two-layer fix:
- _SPAStaticFiles.__call__: gracefully close non-HTTP connections with
WebSocketClose() and log a warning with the actual path/type so the
routing issue can be diagnosed.
- app.add_websocket_route('/ws', websocket_endpoint): belt-and-suspenders
registration using Starlette's base WebSocketRoute (simpler than
FastAPI's APIWebSocketRoute) right before the StaticFiles mount. If
include_router's APIWebSocketRoute doesn't match, this fallback will.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ComfyUI Discord Bot + Web UI
A Discord bot and web interface that integrates with ComfyUI to generate AI images and videos. Requests can be submitted through Discord commands or a browser-based UI with real-time progress updates.
Features
Discord Bot
- Image and video generation via simple prompts or full ComfyUI workflows
- Runtime workflow parameter changes (prompt, negative prompt, input images, seeds)
- Preset management — save and recall workflow configurations
- Generation history with full output retrieval
- Server control (start/stop ComfyUI via NSSM service)
- Automatic image compression to fit Discord's 8 MiB limit
Web UI
- Invite-token authentication with JWT session cookies
- Simple generate form and full dynamic workflow form (auto-discovers all node inputs)
- Input image library — upload, browse, and select images for generation
- Generation history with image preview
- Preset management
- Real-time status dashboard (ComfyUI connection, queue depth)
- WebSocket-based live progress updates during generation
- Admin panel for token and server management
Architecture
the-third-rev/
├── bot.py # Entry point — Discord bot + Uvicorn run via asyncio.gather
├── config.py # BotConfig dataclass, loads all env vars
├── comfy_client.py # ComfyUI REST + WebSocket client
├── workflow_inspector.py # Dynamic node discovery and override injection
├── workflow_manager.py # Workflow template storage (get/set)
├── workflow_state.py # Runtime override dict with file persistence
├── generation_db.py # SQLite generation history + file BLOBs
├── input_image_db.py # SQLite input image storage
├── token_store.py # SHA-256 hashed invite tokens
├── preset_manager.py # Workflow preset CRUD
├── user_state_registry.py # Per-user workflow state for web sessions
├── image_utils.py # PIL-based image compression
├── media_uploader.py # Optional external media upload
├── status_monitor.py # Background status polling and Discord log channel
├── discord_utils.py # Discord helpers, decorators, argument parsing
├── commands/ # Discord command handlers
│ ├── __init__.py
│ ├── generation.py # generate, workflow-gen
│ ├── workflow.py # workflow-load
│ ├── history.py # history, get-history
│ ├── input_images.py # input image upload/management
│ ├── presets.py # preset save/load/delete
│ ├── server.py # ComfyUI server control
│ ├── utility.py # test, info, misc
│ ├── workflow_changes.py # get/set workflow overrides
│ └── help_command.py # custom help
├── web/ # FastAPI application
│ ├── app.py # App factory, middleware, static file serving
│ ├── auth.py # JWT create/verify
│ ├── deps.py # Shared FastAPI dependencies (bot reference)
│ ├── login_guard.py # Auth decorators
│ ├── ws_bus.py # Per-user WebSocket broadcast bus
│ └── routers/ # API endpoints (generate, history, inputs, presets, ...)
└── frontend/ # React + TypeScript + Vite + Tailwind source
└── src/
├── pages/ # GeneratePage, WorkflowPage, HistoryPage, ...
├── components/ # Layout, DynamicWorkflowForm, LazyImage
├── hooks/ # useAuth, useWebSocket, useStatus
└── context/ # GenerationContext (pending count badge)
ComfyUI's own queue handles job ordering — no separate job queue exists in this bot. Generation callbacks are matched to requests via a prompt_id → callback map in ComfyClient.
Requirements
- Python 3.10+
- Node.js 18+ (for building the frontend)
- ComfyUI running and accessible
- Discord bot token with Message Content Intent enabled
Python dependencies:
pip install discord.py aiohttp websockets python-dotenv fastapi uvicorn pillow
Installation
1. Clone and install Python dependencies
git clone <repo-url>
cd the-third-rev
pip install discord.py aiohttp websockets python-dotenv fastapi uvicorn pillow
2. Build the frontend
cd frontend
npm install
npm run build
cd ..
The build output lands in web-static/ and is served automatically by FastAPI.
3. Configure environment
Create a .env file in the project root:
# ── Required ────────────────────────────────────────
DISCORD_BOT_TOKEN=your_discord_bot_token
COMFY_SERVER=localhost:8188
# ── ComfyUI paths ───────────────────────────────────
COMFY_OUTPUT_PATH=C:\path\to\ComfyUI\output
COMFY_INPUT_PATH=C:\path\to\ComfyUI\input
COMFY_HISTORY_LIMIT=10
# ── Startup workflow ────────────────────────────────
WORKFLOW_FILE=workflows/my_workflow.json # optional; loaded at startup
# ── Web UI ──────────────────────────────────────────
WEB_ENABLED=true
WEB_HOST=0.0.0.0
WEB_PORT=8080
WEB_SECRET_KEY=change-me-to-a-random-secret
WEB_JWT_EXPIRE_HOURS=720
WEB_SECURE_COOKIE=false # set true if serving over HTTPS
# ── Admin ───────────────────────────────────────────
ADMIN_PASSWORD=your_admin_password
# ── ComfyUI server control (optional) ───────────────
COMFY_SERVICE_NAME=ComfyUI # NSSM service name
COMFY_START_BAT=C:\path\to\run_nvidia_gpu.bat
COMFY_LOG_DIR=C:\path\to\logs
COMFY_AUTOSTART=false # auto-start ComfyUI on bot launch
# ── Discord status log channel (optional) ────────────
LOG_CHANNEL_ID=123456789012345678
# ── External media upload (optional) ────────────────
MEDIA_UPLOAD_USER=
MEDIA_UPLOAD_PASS=
Full configuration reference
| Variable | Required | Default | Description |
|---|---|---|---|
DISCORD_BOT_TOKEN |
Yes | — | Discord bot token |
COMFY_SERVER |
Yes | — | ComfyUI address (host:port) |
COMFY_OUTPUT_PATH |
No | ...\ComfyUI\output |
ComfyUI output directory |
COMFY_INPUT_PATH |
No | ...\ComfyUI\input |
ComfyUI input directory |
COMFY_HISTORY_LIMIT |
No | 10 |
Generations kept in history |
WORKFLOW_FILE |
No | — | Workflow JSON to load at startup |
WEB_ENABLED |
No | true |
Enable web UI |
WEB_HOST |
No | 0.0.0.0 |
Web server bind address |
WEB_PORT |
No | 8080 |
Web server port |
WEB_SECRET_KEY |
No | — | JWT signing secret (set in production) |
WEB_JWT_EXPIRE_HOURS |
No | 720 |
Session expiry in hours |
WEB_SECURE_COOKIE |
No | false |
Mark session cookie as Secure (HTTPS only) |
WEB_TOKEN_FILE |
No | invite_tokens.json |
Invite token storage path |
ADMIN_PASSWORD |
No | — | Admin panel password |
COMFY_SERVICE_NAME |
No | — | NSSM service name for server control |
COMFY_START_BAT |
No | — | ComfyUI launch script path |
COMFY_LOG_DIR |
No | — | Directory for ComfyUI logs |
COMFY_AUTOSTART |
No | false |
Auto-start ComfyUI on bot launch |
LOG_CHANNEL_ID |
No | — | Discord channel ID for status messages |
4. Create a web UI invite token
The web UI requires an invite token to register an account:
python -c "from token_store import create_token; print(create_token('username'))"
Copy the printed token — it is shown only once. Give it to the user who will register.
5. Run
python bot.py
The bot starts the Discord client and the web server concurrently. Navigate to http://localhost:8080 to access the web UI.
Discord Commands
All commands use the ttr! prefix.
Generation
| Command | Alias | Description |
|---|---|---|
ttr!generate prompt:<text> |
ttr!gen |
Generate using prompt mode |
ttr!generate prompt:<text> negative_prompt:<text> |
Generate with negative prompt | |
ttr!workflow-gen |
ttr!wfg |
Execute loaded workflow with current overrides |
ttr!workflow-gen queue:5 |
Queue 5 workflow runs |
Workflow management
| Command | Alias | Description |
|---|---|---|
ttr!workflow-load <path> |
ttr!wfl |
Load workflow from file path |
ttr!workflow-load (+ attachment) |
Load workflow from attached JSON | |
ttr!get-current-workflow-changes type:all |
ttr!gcwc |
Show current overrides |
ttr!set-current-workflow-changes type:prompt <text> |
ttr!scwc |
Set prompt override |
ttr!set-current-workflow-changes type:negative_prompt <text> |
Set negative prompt |
History
| Command | Alias | Description |
|---|---|---|
ttr!history |
List recent generations | |
ttr!get-history <id> |
ttr!gh |
Retrieve output from a past generation |
Presets
| Command | Description |
|---|---|
ttr!preset-save <name> |
Save current workflow overrides as a preset |
ttr!preset-load <name> |
Apply a saved preset |
ttr!preset-list |
List all presets |
ttr!preset-delete <name> |
Delete a preset |
Server control
| Command | Description |
|---|---|
ttr!server-start |
Start the ComfyUI NSSM service |
ttr!server-stop |
Stop the ComfyUI NSSM service |
ttr!server-status |
Show ComfyUI service status |
ttr!server-log |
Tail the ComfyUI log file |
Utility
| Command | Description |
|---|---|
ttr!test |
Verify bot is online |
ttr!help |
Show command list |
Workflow System
How node injection works
workflow_inspector.py dynamically discovers all controllable inputs in any workflow:
- Prompt —
CLIPTextEncodenode with title containing "Positive Prompt" - Negative prompt —
CLIPTextEncodenode with title containing "Negative Prompt" - Input image —
LoadImagenodes (first one =input_imagekey; additional ones get slugified title keys) - Seed — any node with
inputs.seedorinputs.noise_seed(auto-randomized unless explicitly set) - Steps, CFG, checkpoint, LoRA — discovered and injectable via the web workflow form
No hardcoded node IDs. Workflows only need to follow standard ComfyUI node title conventions.
Workflow overrides persist across restarts
Runtime changes are saved to current-workflow-changes.json automatically and restored on startup.
Loading a custom workflow
- Design and export your workflow in ComfyUI (Save → API Format)
- Load it in Discord:
ttr!workflow-load path/to/workflow.jsonor via the web UI: Workflow page → Upload - Set overrides and run:
ttr!workflow-genor use the web Generate/Workflow page
Development
Frontend development
cd frontend
npm run dev # HMR dev server on :5173, proxies /api + /ws to :8080
For production, rebuild with npm run build.
Adding a Discord command
- Add your handler to the appropriate module in
commands/(or create a new one) - Register it in
commands/__init__.py→register_all_commands() - Use
@require_comfy_clientfromdiscord_utils.pyif the command needsbot.comfy
See CLAUDE.md for full architectural details.
Adding a web API endpoint
- Create a router in
web/routers/ - Register it in
web/app.pyviaapp.include_router() - Use
require_auth/require_adminfromweb/auth.pyfor protected routes
Troubleshooting
Web UI shows a blank page in production
Windows may serve .js files as text/plain. This is fixed in web/app.py with explicit MIME type registration — ensure you are running the latest version.
ComfyUI connection refused
Check COMFY_SERVER in .env and confirm ComfyUI is running. Test with curl http://localhost:8188.
Commands not responding Ensure the bot has Message Content Intent enabled in the Discord Developer Portal and has sufficient channel permissions.
Videos not delivered
Set COMFY_OUTPUT_PATH to the correct ComfyUI output directory. The bot reads video files directly from disk.
Web UI WebSocket disconnects immediately
Set a proper WEB_SECRET_KEY — an empty secret causes JWT validation failures.
Credits
Built with:
- discord.py — Discord API
- ComfyUI — AI image/video generation backend
- FastAPI — Web API framework
- Uvicorn — ASGI server
- React + Vite + Tailwind CSS — Web frontend
- aiohttp — Async HTTP client
- Pillow — Image compression