docs: update README for current architecture, remove stale docs

Rewrote README.md to reflect the actual project state: dual Discord
bot + FastAPI web UI, WorkflowInspector-based dynamic node injection,
no SerialJobQueue, full env var table, frontend build steps, web auth
setup, and updated credits/troubleshooting.

Removed three files that were actively misleading:
- DEVELOPMENT.md and QUICK_START.md referenced deleted modules
  (job_queue.py, upload.py) and old WorkflowManager/StateManager APIs
- backfill_image_data.py was a completed one-shot migration script

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Khoa (Revenovich) Tran Gia
2026-03-02 12:00:13 +07:00
parent 1ed3c9ec4b
commit f6907d2c39
4 changed files with 254 additions and 1411 deletions

View File

@@ -1,780 +0,0 @@
# Development Guide
This guide explains how to add new commands, features, and modules to the Discord ComfyUI bot.
## Table of Contents
- [Adding a New Command](#adding-a-new-command)
- [Adding a New Feature Module](#adding-a-new-feature-module)
- [Adding Configuration Options](#adding-configuration-options)
- [Working with Workflows](#working-with-workflows)
- [Best Practices](#best-practices)
- [Common Patterns](#common-patterns)
- [Testing Your Changes](#testing-your-changes)
---
## Adding a New Command
Commands are organized in the `commands/` directory by functionality. Here's how to add a new command:
### Step 1: Choose the Right Module
Determine which existing command module your command belongs to:
- **generation.py** - Image/video generation commands
- **workflow.py** - Workflow template management
- **upload.py** - File upload commands
- **history.py** - History viewing and retrieval
- **workflow_changes.py** - Runtime workflow parameter management
If none fit, create a new module (see [Adding a New Feature Module](#adding-a-new-feature-module)).
### Step 2: Add Your Command Function
Edit the appropriate module in `commands/` and add your command to the `setup_*_commands()` function:
```python
# commands/generation.py
def setup_generation_commands(bot, config):
# ... existing commands ...
@bot.command(name="my-new-command", aliases=["mnc", "my-cmd"])
@require_comfy_client # Use this decorator if you need bot.comfy
async def my_new_command(ctx: commands.Context, *, args: str = "") -> None:
"""
Brief description of what your command does.
Usage:
ttr!my-new-command [arguments]
Longer description with examples and details.
"""
# Parse arguments if needed
if not args:
await ctx.reply("Please provide arguments!", mention_author=False)
return
try:
# Your command logic here
result = await bot.comfy.some_method(args)
# Send response
await ctx.reply(f"Success! Result: {result}", mention_author=False)
except Exception as exc:
logger.exception("Failed to execute my-new-command")
await ctx.reply(
f"An error occurred: {type(exc).__name__}: {exc}",
mention_author=False,
)
```
### Step 3: Import Required Dependencies
At the top of your command module, import what you need:
```python
import logging
from discord.ext import commands
from discord_utils import require_comfy_client, parse_labeled_args
from config import ARG_PROMPT_KEY, ARG_TYPE_KEY
from job_queue import Job
logger = logging.getLogger(__name__)
```
### Step 4: Test Your Command
Run the bot and test your command:
```bash
python bot.py
```
Then in Discord:
```
ttr!my-new-command test arguments
```
---
## Adding a New Feature Module
If your commands don't fit existing modules, create a new one:
### Step 1: Create the Module File
Create `commands/your_feature.py`:
```python
"""
commands/your_feature.py
========================
Description of what this module handles.
"""
from __future__ import annotations
import logging
from discord.ext import commands
from discord_utils import require_comfy_client
logger = logging.getLogger(__name__)
def setup_your_feature_commands(bot, config):
"""
Register your feature commands with the bot.
Parameters
----------
bot : commands.Bot
The Discord bot instance.
config : BotConfig
The bot configuration object.
"""
@bot.command(name="feature-command")
@require_comfy_client
async def feature_command(ctx: commands.Context, *, args: str = "") -> None:
"""Command description."""
await ctx.reply("Feature command executed!", mention_author=False)
@bot.command(name="another-command", aliases=["ac"])
async def another_command(ctx: commands.Context) -> None:
"""Another command description."""
await ctx.reply("Another command!", mention_author=False)
```
### Step 2: Register in commands/__init__.py
Edit `commands/__init__.py` to import and register your module:
```python
from .generation import setup_generation_commands
from .workflow import setup_workflow_commands
from .upload import setup_upload_commands
from .history import setup_history_commands
from .workflow_changes import setup_workflow_changes_commands
from .your_feature import setup_your_feature_commands # ADD THIS
def register_all_commands(bot, config):
"""Register all bot commands."""
setup_generation_commands(bot, config)
setup_workflow_commands(bot)
setup_upload_commands(bot)
setup_history_commands(bot)
setup_workflow_changes_commands(bot)
setup_your_feature_commands(bot, config) # ADD THIS
```
### Step 3: Update Documentation
Add your module to `CLAUDE.md`:
```markdown
### File Structure
```
commands/
├── __init__.py
├── generation.py
├── workflow.py
├── upload.py
├── history.py
├── workflow_changes.py
└── your_feature.py # Your new module
```
```
---
## Adding Configuration Options
Configuration is centralized in `config.py`. Here's how to add new options:
### Step 1: Add Constants (if needed)
Edit `config.py` and add constants in the appropriate section:
```python
# ========================================
# Your Feature Constants
# ========================================
MY_FEATURE_DEFAULT_VALUE = 42
"""Default value for my feature."""
MY_FEATURE_MAX_LIMIT = 100
"""Maximum limit for my feature."""
```
### Step 2: Add to BotConfig (if environment variable)
If your config comes from environment variables, add it to `BotConfig`:
```python
@dataclass
class BotConfig:
"""Configuration container for the Discord ComfyUI bot."""
discord_bot_token: str
comfy_server: str
comfy_output_path: str
comfy_history_limit: int
workflow_file: Optional[str] = None
my_feature_enabled: bool = False # ADD THIS
my_feature_value: int = MY_FEATURE_DEFAULT_VALUE # ADD THIS
```
### Step 3: Load in from_env()
Add loading logic in `BotConfig.from_env()`:
```python
@classmethod
def from_env(cls) -> BotConfig:
"""Create a BotConfig instance by loading from environment."""
# ... existing code ...
# Load your feature config
my_feature_enabled = os.getenv("MY_FEATURE_ENABLED", "false").lower() == "true"
try:
my_feature_value = int(os.getenv("MY_FEATURE_VALUE", str(MY_FEATURE_DEFAULT_VALUE)))
except ValueError:
my_feature_value = MY_FEATURE_DEFAULT_VALUE
return cls(
# ... existing parameters ...
my_feature_enabled=my_feature_enabled,
my_feature_value=my_feature_value,
)
```
### Step 4: Use in Your Commands
Access config in your commands:
```python
def setup_your_feature_commands(bot, config):
@bot.command(name="feature")
async def feature_command(ctx: commands.Context):
if not config.my_feature_enabled:
await ctx.reply("Feature is disabled!", mention_author=False)
return
value = config.my_feature_value
await ctx.reply(f"Feature value: {value}", mention_author=False)
```
### Step 5: Document the Environment Variable
Update `CLAUDE.md` and add to `.env.example` (if you create one):
```bash
# Feature Configuration
MY_FEATURE_ENABLED=true
MY_FEATURE_VALUE=42
```
---
## Working with Workflows
The bot has separate concerns for workflows:
- **WorkflowManager** (`workflow_manager.py`) - Template storage and node manipulation
- **WorkflowStateManager** (`workflow_state.py`) - Runtime state (prompt, negative_prompt, input_image)
- **ComfyClient** (`comfy_client.py`) - Uses both managers to generate images
### Adding New Workflow Node Types
If you need to manipulate new types of nodes in workflows:
#### Step 1: Add Method to WorkflowManager
Edit `workflow_manager.py`:
```python
def find_and_replace_my_node(
self, workflow: Dict[str, Any], my_value: str
) -> Dict[str, Any]:
"""
Find and replace my custom node type.
This searches for nodes of a specific class_type and updates their inputs.
Parameters
----------
workflow : Dict[str, Any]
The workflow definition to modify.
my_value : str
The value to inject.
Returns
-------
Dict[str, Any]
The modified workflow.
"""
for node_id, node in workflow.items():
if node.get("class_type") == "MyCustomNodeType" and node.get("inputs"):
# Check metadata for specific node identification
meta = node.get("_meta", {})
if "My Custom Node" in meta.get("title", ""):
workflow[node_id]["inputs"]["my_input"] = my_value
logger.debug("Replaced my_value in node %s", node_id)
return workflow
```
#### Step 2: Add to apply_state_changes()
Update `apply_state_changes()` to include your new manipulation:
```python
def apply_state_changes(
self,
workflow: Dict[str, Any],
prompt: Optional[str] = None,
negative_prompt: Optional[str] = None,
input_image: Optional[str] = None,
my_custom_value: Optional[str] = None, # ADD THIS
randomize_seed: bool = True,
) -> Dict[str, Any]:
"""Apply multiple state changes to a workflow in one pass."""
if randomize_seed:
workflow = self.find_and_replace_seed(workflow)
if prompt is not None:
workflow = self.find_and_replace_prompt(workflow, prompt)
if negative_prompt is not None:
workflow = self.find_and_replace_negative_prompt(workflow, negative_prompt)
if input_image is not None:
workflow = self.find_and_replace_input_image(workflow, input_image)
# ADD THIS
if my_custom_value is not None:
workflow = self.find_and_replace_my_node(workflow, my_custom_value)
return workflow
```
#### Step 3: Add State to WorkflowStateManager
Edit `workflow_state.py` to track the new state:
```python
def __init__(self, state_file: Optional[str] = None):
"""Initialize the workflow state manager."""
self._state: Dict[str, Any] = {
"prompt": None,
"negative_prompt": None,
"input_image": None,
"my_custom_value": None, # ADD THIS
}
# ... rest of init ...
def set_my_custom_value(self, value: str) -> None:
"""Set the custom value."""
self._state["my_custom_value"] = value
if self._state_file:
try:
self.save_to_file()
except Exception:
pass
def get_my_custom_value(self) -> Optional[str]:
"""Get the custom value."""
return self._state.get("my_custom_value")
```
#### Step 4: Use in ComfyClient
The ComfyClient will automatically use your new state if you update `generate_image_with_workflow()`:
```python
async def generate_image_with_workflow(self) -> tuple[List[bytes], List[dict[str, Any]], str]:
# ... existing code ...
# Get current state changes
changes = self.state_manager.get_changes()
prompt = changes.get("prompt")
negative_prompt = changes.get("negative_prompt")
input_image = changes.get("input_image")
my_custom_value = changes.get("my_custom_value") # ADD THIS
# Apply changes using WorkflowManager
workflow = self.workflow_manager.apply_state_changes(
workflow,
prompt=prompt,
negative_prompt=negative_prompt,
input_image=input_image,
my_custom_value=my_custom_value, # ADD THIS
randomize_seed=True,
)
# ... rest of method ...
```
---
## Best Practices
### Command Design
1. **Use descriptive names**: `ttr!generate` is better than `ttr!gen` (but provide aliases)
2. **Validate inputs early**: Check arguments before starting long operations
3. **Provide clear feedback**: Tell users what's happening and when it's done
4. **Handle errors gracefully**: Catch exceptions and show user-friendly messages
5. **Use decorators**: `@require_comfy_client` eliminates boilerplate
### Code Organization
1. **One responsibility per module**: Don't mix unrelated commands
2. **Keep functions small**: If a function is > 50 lines, consider splitting it
3. **Use type hints**: Help future developers understand your code
4. **Document with docstrings**: Explain what, why, and how
### Discord Best Practices
1. **Use `mention_author=False`**: Prevents spam from @mentions
2. **Use `delete_after=X`**: For temporary status messages
3. **Use `ephemeral=True`**: For interaction responses (buttons/modals)
4. **Limit file attachments**: Discord has a 4-file limit (use `MAX_IMAGES_PER_RESPONSE`)
### Performance
1. **Use job queue for long operations**: Queue generation requests
2. **Use typing indicator**: `async with ctx.typing():` shows bot is working
3. **Batch operations**: Don't send 10 separate messages when 1 will do
4. **Close resources**: Always close aiohttp sessions, file handles
---
## Common Patterns
### Pattern 1: Labeled Argument Parsing
For commands with `key:value` syntax:
```python
from discord_utils import parse_labeled_args
from config import ARG_PROMPT_KEY, ARG_TYPE_KEY
@bot.command(name="my-command")
async def my_command(ctx: commands.Context, *, args: str = ""):
# Parse labeled arguments
parsed = parse_labeled_args(args, [ARG_PROMPT_KEY, ARG_TYPE_KEY])
prompt = parsed.get("prompt") # None if not provided
image_type = parsed.get("type") or "input" # Default to "input"
if not prompt:
await ctx.reply("Please provide a prompt!", mention_author=False)
return
```
### Pattern 2: Queued Job Execution
For long-running operations:
```python
from job_queue import Job
@bot.command(name="long-operation")
@require_comfy_client
async def long_operation(ctx: commands.Context, *, args: str = ""):
try:
# Define the job function
async def _run_job():
async with ctx.typing():
result = await bot.comfy.some_long_operation(args)
await ctx.reply(f"Done! Result: {result}", mention_author=False)
# Submit to queue
position = await bot.jobq.submit(
Job(
label=f"long-operation:{ctx.author.id}",
run=_run_job,
)
)
await ctx.reply(
f"Queued ✅ (position: {position})",
mention_author=False,
delete_after=2.0,
)
except Exception as exc:
logger.exception("Failed to queue long operation")
await ctx.reply(
f"An error occurred: {type(exc).__name__}: {exc}",
mention_author=False,
)
```
### Pattern 3: File Attachments
For uploading files to ComfyUI:
```python
@bot.command(name="upload-and-process")
@require_comfy_client
async def upload_and_process(ctx: commands.Context):
if not ctx.message.attachments:
await ctx.reply("Please attach a file!", mention_author=False)
return
for attachment in ctx.message.attachments:
try:
# Download attachment
data = await attachment.read()
# Upload to ComfyUI
result = await bot.comfy.upload_image(
data,
attachment.filename,
image_type="input",
)
# Process the uploaded file
filename = result.get("name")
await ctx.reply(f"Uploaded: {filename}", mention_author=False)
except Exception as exc:
logger.exception("Failed to process attachment")
await ctx.reply(
f"Failed: {attachment.filename}: {exc}",
mention_author=False,
)
```
### Pattern 4: Interactive UI (Buttons)
For adding buttons to messages:
```python
from discord.ui import View, Button
import discord
class MyView(View):
def __init__(self, data: str):
super().__init__(timeout=None)
self.data = data
@discord.ui.button(label="Click Me", style=discord.ButtonStyle.primary)
async def button_callback(
self, interaction: discord.Interaction, button: discord.ui.Button
):
# Handle button click
await interaction.response.send_message(
f"You clicked! Data: {self.data}",
ephemeral=True,
)
@bot.command(name="interactive")
async def interactive(ctx: commands.Context):
view = MyView(data="example")
await ctx.reply("Click the button:", view=view, mention_author=False)
```
### Pattern 5: Using Configuration
Access bot configuration in commands:
```python
def setup_my_commands(bot, config):
@bot.command(name="check-config")
async def check_config(ctx: commands.Context):
# Access config values
server = config.comfy_server
output_path = config.comfy_output_path
await ctx.reply(
f"Server: {server}\nOutput: {output_path}",
mention_author=False,
)
```
---
## Testing Your Changes
### Manual Testing Checklist
1. **Start the bot**: `python bot.py`
- Verify no import errors
- Check configuration loads correctly
- Confirm all commands register
2. **Test basic functionality**:
```
ttr!test
ttr!help
ttr!your-new-command
```
3. **Test error handling**:
- Run command with missing arguments
- Run command with invalid arguments
- Test when ComfyUI is unavailable (if applicable)
4. **Test edge cases**:
- Very long inputs
- Special characters
- Concurrent command execution
- Commands while queue is full
### Syntax Validation
Check for syntax errors without running:
```bash
python -m py_compile bot.py
python -m py_compile commands/your_feature.py
```
### Check Imports
Verify all imports work:
```bash
python -c "from commands.your_feature import setup_your_feature_commands; print('OK')"
```
### Code Style
Follow these conventions:
- **Indentation**: 4 spaces (no tabs)
- **Line length**: Max 100 characters (documentation can be longer)
- **Docstrings**: Use Google style or NumPy style (match existing code)
- **Imports**: Group stdlib, third-party, local (separated by blank lines)
- **Type hints**: Use modern syntax (`dict[str, Any]` not `Dict[str, Any]`)
---
## Example: Complete Feature Addition
Here's a complete example adding a "status" command:
### 1. Create commands/status.py
```python
"""
commands/status.py
==================
Bot status and diagnostics commands.
"""
from __future__ import annotations
import logging
import asyncio
from discord.ext import commands
from discord_utils import require_comfy_client
logger = logging.getLogger(__name__)
def setup_status_commands(bot, config):
"""Register status commands with the bot."""
@bot.command(name="status", aliases=["s", "stat"])
async def status_command(ctx: commands.Context) -> None:
"""
Show bot status and queue information.
Usage:
ttr!status
Displays:
- Bot connection status
- ComfyUI connection status
- Current queue size
- Configuration info
"""
# Check bot status
latency_ms = round(bot.latency * 1000)
# Check queue
if hasattr(bot, "jobq"):
queue_size = await bot.jobq.get_queue_size()
else:
queue_size = 0
# Check ComfyUI
comfy_status = "✅ Connected" if hasattr(bot, "comfy") else "❌ Not configured"
# Build status message
status_msg = [
"**Bot Status**",
f"• Latency: {latency_ms}ms",
f"• Queue size: {queue_size}",
f"• ComfyUI: {comfy_status}",
f"• Server: {config.comfy_server}",
]
await ctx.reply("\n".join(status_msg), mention_author=False)
@bot.command(name="ping")
async def ping_command(ctx: commands.Context) -> None:
"""
Check bot responsiveness.
Usage:
ttr!ping
"""
latency_ms = round(bot.latency * 1000)
await ctx.reply(f"🏓 Pong! Latency: {latency_ms}ms", mention_author=False)
```
### 2. Register in commands/__init__.py
```python
from .status import setup_status_commands
def register_all_commands(bot, config):
# ... existing registrations ...
setup_status_commands(bot, config)
```
### 3. Update CLAUDE.md
```markdown
- **commands/status.py** - Bot status and diagnostics commands
```
### 4. Test
```bash
python bot.py
```
In Discord:
```
ttr!status
ttr!ping
ttr!s (alias test)
```
---
## Getting Help
If you're stuck:
1. **Check CLAUDE.md**: Architecture and patterns documented there
2. **Read existing commands**: See how similar features are implemented
3. **Check logs**: Run bot and check console output for errors
4. **Test incrementally**: Add small pieces and test frequently
Remember: The refactored architecture makes adding features straightforward. Follow the patterns, and your code will fit right in! 🚀

View File

@@ -1,196 +0,0 @@
# Quick Start Guide
Quick reference for common development tasks.
## Add a Simple Command
```python
# commands/your_module.py
def setup_your_commands(bot, config):
@bot.command(name="hello")
async def hello(ctx):
await ctx.reply("Hello!", mention_author=False)
```
Register it:
```python
# commands/__init__.py
from .your_module import setup_your_commands
def register_all_commands(bot, config):
# ... existing ...
setup_your_commands(bot, config)
```
## Add a Command That Uses ComfyUI
```python
from discord_utils import require_comfy_client
@bot.command(name="my-cmd")
@require_comfy_client # Validates bot.comfy exists
async def my_cmd(ctx):
result = await bot.comfy.some_method()
await ctx.reply(f"Result: {result}", mention_author=False)
```
## Add a Long-Running Command
```python
from job_queue import Job
@bot.command(name="generate")
@require_comfy_client
async def generate(ctx, *, args: str = ""):
async def _run():
async with ctx.typing():
result = await bot.comfy.generate_image(args)
await ctx.reply(f"Done! {result}", mention_author=False)
pos = await bot.jobq.submit(Job(label="generate", run=_run))
await ctx.reply(f"Queued ✅ (position: {pos})", mention_author=False)
```
## Add Configuration
```python
# config.py
MY_FEATURE_ENABLED = True
@dataclass
class BotConfig:
# ... existing fields ...
my_feature_enabled: bool = MY_FEATURE_ENABLED
@classmethod
def from_env(cls) -> BotConfig:
# ... existing code ...
my_feature = os.getenv("MY_FEATURE_ENABLED", "true").lower() == "true"
return cls(
# ... existing params ...
my_feature_enabled=my_feature
)
```
Use in commands:
```python
def setup_my_commands(bot, config):
@bot.command(name="feature")
async def feature(ctx):
if config.my_feature_enabled:
await ctx.reply("Enabled!", mention_author=False)
```
## Parse Command Arguments
```python
from discord_utils import parse_labeled_args
from config import ARG_PROMPT_KEY, ARG_TYPE_KEY
@bot.command(name="cmd")
async def cmd(ctx, *, args: str = ""):
# Parse "prompt:text type:value" format
parsed = parse_labeled_args(args, [ARG_PROMPT_KEY, ARG_TYPE_KEY])
prompt = parsed.get("prompt")
img_type = parsed.get("type") or "input" # Default
if not prompt:
await ctx.reply("Missing prompt!", mention_author=False)
return
```
## Handle File Uploads
```python
@bot.command(name="upload")
async def upload(ctx):
if not ctx.message.attachments:
await ctx.reply("Attach a file!", mention_author=False)
return
for attachment in ctx.message.attachments:
data = await attachment.read()
# Process data...
```
## Access Bot State
```python
@bot.command(name="info")
async def info(ctx):
# Queue size
queue_size = await bot.jobq.get_queue_size()
# Config
server = bot.config.comfy_server
# Last generation
last_id = bot.comfy.last_prompt_id
await ctx.reply(
f"Queue: {queue_size}, Server: {server}, Last: {last_id}",
mention_author=False
)
```
## Add Buttons
```python
from discord.ui import View, Button
import discord
class MyView(View):
@discord.ui.button(label="Click", style=discord.ButtonStyle.primary)
async def button_callback(self, interaction, button):
await interaction.response.send_message("Clicked!", ephemeral=True)
@bot.command(name="interactive")
async def interactive(ctx):
await ctx.reply("Press button:", view=MyView(), mention_author=False)
```
## Common Imports
```python
from __future__ import annotations
import logging
from discord.ext import commands
from discord_utils import require_comfy_client
from config import ARG_PROMPT_KEY, ARG_TYPE_KEY
from job_queue import Job
logger = logging.getLogger(__name__)
```
## Test Your Changes
```bash
# Syntax check
python -m py_compile commands/your_module.py
# Run bot
python bot.py
# Test in Discord
ttr!your-command
```
## File Structure
```
commands/
├── __init__.py # Register all commands here
├── generation.py # Generation commands
├── workflow.py # Workflow management
├── upload.py # File uploads
├── history.py # History retrieval
├── workflow_changes.py # State management
└── your_module.py # Your new module
```
## Need More Details?
See `DEVELOPMENT.md` for comprehensive guide with examples, patterns, and best practices.

533
README.md
View File

@@ -1,345 +1,320 @@
# Discord ComfyUI Bot
# ComfyUI Discord Bot + Web UI
A Discord bot that integrates with ComfyUI to generate AI images and videos through Discord commands.
A Discord bot and web interface that integrates with [ComfyUI](https://github.com/comfyanonymous/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
- 🎨 **Image Generation** - Generate images using simple prompts or complex workflows
- 🎬 **Video Generation** - Support for video output workflows
- 📝 **Workflow Management** - Load, modify, and execute ComfyUI workflows
- 📤 **Image Upload** - Upload reference images directly through Discord
- 📊 **Generation History** - Track and retrieve past generations
- ⚙️ **Runtime Workflow Modification** - Change prompts, negative prompts, and input images on the fly
- 🔄 **Job Queue System** - Sequential execution prevents server overload
**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
## Quick Start
**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
### Prerequisites
## Architecture
- Python 3.9+
- Discord Bot Token ([create one here](https://discord.com/developers/applications))
- ComfyUI Server running and accessible
- Required packages: `discord.py`, `aiohttp`, `websockets`, `python-dotenv`
```
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)
```
### Installation
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`.
1. **Clone or download this repository**
## Requirements
2. **Install dependencies**:
```bash
pip install discord.py aiohttp websockets python-dotenv
```
- Python 3.10+
- Node.js 18+ (for building the frontend)
- ComfyUI running and accessible
- Discord bot token with **Message Content Intent** enabled
3. **Create `.env` file** with your credentials:
```bash
DISCORD_BOT_TOKEN=your_discord_bot_token_here
COMFY_SERVER=localhost:8188
```
**Python dependencies:**
```bash
pip install discord.py aiohttp websockets python-dotenv fastapi uvicorn pillow
```
4. **Run the bot**:
```bash
python bot.py
```
## Installation
## Configuration
### 1. Clone and install Python dependencies
```bash
git clone <repo-url>
cd the-third-rev
pip install discord.py aiohttp websockets python-dotenv fastapi uvicorn pillow
```
### 2. Build the frontend
```bash
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:
```bash
# Required
# ── Required ────────────────────────────────────────
DISCORD_BOT_TOKEN=your_discord_bot_token
COMFY_SERVER=localhost:8188
# Optional
WORKFLOW_FILE=wan2.2-fast.json
# ── ComfyUI paths ───────────────────────────────────
COMFY_OUTPUT_PATH=C:\path\to\ComfyUI\output
COMFY_INPUT_PATH=C:\path\to\ComfyUI\input
COMFY_HISTORY_LIMIT=10
COMFY_OUTPUT_PATH=C:\Users\YourName\Documents\ComfyUI\output
# ── 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=
```
### Configuration Options
### Full configuration reference
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `DISCORD_BOT_TOKEN` | ✅ Yes | - | Discord bot authentication token |
| `COMFY_SERVER` | Yes | - | ComfyUI server address (host:port) |
| `WORKFLOW_FILE` | ❌ No | - | Path to workflow JSON to load at startup |
| `COMFY_HISTORY_LIMIT` | ❌ No | `10` | Number of generations to keep in history |
| `COMFY_OUTPUT_PATH` | No | `C:\Users\...\ComfyUI\output` | Path to ComfyUI output directory |
|---|---|---|---|
| `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 |
## Usage
### 4. Create a web UI invite token
The web UI requires an invite token to register an account:
```bash
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
```bash
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.
### Basic Commands
### Generation
```bash
# Test if bot is working
ttr!test
| 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 |
# Generate an image with a prompt
ttr!generate prompt:a beautiful sunset over mountains
### Workflow management
# Generate with negative prompt
ttr!generate prompt:a cat negative_prompt:blurry, low quality
# Execute loaded workflow
ttr!workflow-gen
# Queue multiple workflow runs
ttr!workflow-gen queue:5
```
### Workflow Management
```bash
# Load a workflow from file
ttr!workflow-load path/to/workflow.json
# Or attach a JSON file to the message:
ttr!workflow-load
[Attach: my_workflow.json]
# View current workflow changes
ttr!get-current-workflow-changes type:all
# Set workflow parameters
ttr!set-current-workflow-changes type:prompt A new prompt
ttr!set-current-workflow-changes type:negative_prompt blurry
ttr!set-current-workflow-changes type:input_image input/image.png
```
### Image Upload
```bash
# Upload images to ComfyUI
ttr!upload
[Attach: image1.png, image2.png]
# Upload to specific folder
ttr!upload type:temp
[Attach: reference.png]
```
| 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
```bash
# View recent generations
ttr!history
| Command | Alias | Description |
|---|---|---|
| `ttr!history` | | List recent generations |
| `ttr!get-history <id>` | `ttr!gh` | Retrieve output from a past generation |
# Retrieve images from a past generation
ttr!get-history <prompt_id>
ttr!get-history 1 # By index
```
### Presets
### Command Aliases
| 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 |
Many commands have shorter aliases:
### Server control
- `ttr!generate` → `ttr!gen`
- `ttr!workflow-gen` → `ttr!wfg`
- `ttr!workflow-load` → `ttr!wfl`
- `ttr!get-history` → `ttr!gh`
- `ttr!get-current-workflow-changes` → `ttr!gcwc`
- `ttr!set-current-workflow-changes` → `ttr!scwc`
| 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 |
## Architecture
### Utility
The bot is organized into focused, maintainable modules:
```
the-third-rev/
├── config.py # Configuration and constants
├── job_queue.py # Job queue system
├── workflow_manager.py # Workflow manipulation
├── workflow_state.py # Runtime state management
├── discord_utils.py # Discord utilities
├── bot.py # Main entry point (~150 lines)
├── comfy_client.py # ComfyUI API client (~650 lines)
└── commands/ # Command handlers
├── generation.py # Image/video generation
├── workflow.py # Workflow management
├── upload.py # File uploads
├── history.py # History retrieval
└── workflow_changes.py # State management
```
### Key Design Principles
- **Dependency Injection** - Dependencies passed via constructor
- **Single Responsibility** - Each module has one clear purpose
- **Configuration Centralization** - All config in `config.py`
- **Command Separation** - Commands grouped by functionality
- **Type Safety** - Modern Python type hints throughout
## Development
### Adding a New Command
See `QUICK_START.md` for quick examples or `DEVELOPMENT.md` for comprehensive guide.
Basic example:
```python
# commands/your_module.py
def setup_your_commands(bot, config):
@bot.command(name="hello")
async def hello(ctx):
await ctx.reply("Hello!", mention_author=False)
```
Register in `commands/__init__.py`:
```python
from .your_module import setup_your_commands
def register_all_commands(bot, config):
# ... existing ...
setup_your_commands(bot, config)
```
### Documentation
- **README.md** (this file) - Project overview and setup
- **QUICK_START.md** - Quick reference for common tasks
- **DEVELOPMENT.md** - Comprehensive development guide
- **CLAUDE.md** - Architecture documentation for Claude Code
| Command | Description |
|---|---|
| `ttr!test` | Verify bot is online |
| `ttr!help` | Show command list |
## Workflow System
The bot supports two generation modes:
### How node injection works
### 1. Prompt Mode (Simple)
`workflow_inspector.py` dynamically discovers all controllable inputs in any workflow:
Uses a workflow template with a KSampler node:
- **Prompt** — `CLIPTextEncode` node with title containing "Positive Prompt"
- **Negative prompt** — `CLIPTextEncode` node with title containing "Negative Prompt"
- **Input image** — `LoadImage` nodes (first one = `input_image` key; additional ones get slugified title keys)
- **Seed** — any node with `inputs.seed` or `inputs.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
1. Design and export your workflow in ComfyUI (Save → API Format)
2. Load it in Discord: `ttr!workflow-load path/to/workflow.json`
or via the web UI: Workflow page → Upload
3. Set overrides and run: `ttr!workflow-gen` or use the web Generate/Workflow page
## Development
### Frontend development
```bash
ttr!generate prompt:a cat negative_prompt:blurry
cd frontend
npm run dev # HMR dev server on :5173, proxies /api + /ws to :8080
```
The bot automatically finds and replaces:
- Positive prompt in CLIPTextEncode node (title: "Positive Prompt")
- Negative prompt in CLIPTextEncode node (title: "Negative Prompt")
- Seed values (randomized each run)
For production, rebuild with `npm run build`.
### 2. Workflow Mode (Advanced)
### Adding a Discord command
Execute full workflow with runtime modifications:
1. Add your handler to the appropriate module in `commands/` (or create a new one)
2. Register it in `commands/__init__.py``register_all_commands()`
3. Use `@require_comfy_client` from `discord_utils.py` if the command needs `bot.comfy`
```bash
# Set workflow parameters
ttr!set-current-workflow-changes type:prompt A beautiful landscape
ttr!set-current-workflow-changes type:input_image input/reference.png
See `CLAUDE.md` for full architectural details.
# Execute workflow
ttr!workflow-gen
```
### Adding a web API endpoint
The bot:
1. Loads the workflow template
2. Applies runtime changes from WorkflowStateManager
3. Randomizes seeds
4. Executes on ComfyUI server
5. Returns images/videos
### Node Naming Conventions
For workflows to work with dynamic updates, nodes must follow naming conventions:
- **Positive Prompt**: CLIPTextEncode node with title containing "Positive Prompt"
- **Negative Prompt**: CLIPTextEncode node with title containing "Negative Prompt"
- **Input Image**: LoadImage node (any title)
- **Seeds**: Any node with `inputs.seed` or `inputs.noise_seed`
1. Create a router in `web/routers/`
2. Register it in `web/app.py` via `app.include_router()`
3. Use `require_auth` / `require_admin` from `web/auth.py` for protected routes
## Troubleshooting
### Bot won't start
**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.
**Issue**: `AttributeError: module 'queue' has no attribute 'SimpleQueue'`
**ComfyUI connection refused**
Check `COMFY_SERVER` in `.env` and confirm ComfyUI is running. Test with `curl http://localhost:8188`.
**Solution**: This was fixed by renaming `queue.py` to `job_queue.py`. Make sure you're using the latest version.
**Commands not responding**
Ensure the bot has **Message Content Intent** enabled in the Discord Developer Portal and has sufficient channel permissions.
### ComfyUI connection issues
**Videos not delivered**
Set `COMFY_OUTPUT_PATH` to the correct ComfyUI output directory. The bot reads video files directly from disk.
**Issue**: `ComfyUI client is not configured`
**Solution**:
1. Check `.env` file has `DISCORD_BOT_TOKEN` and `COMFY_SERVER`
2. Verify ComfyUI server is running
3. Test connection: `curl http://localhost:8188`
### Commands not responding
**Issue**: Bot online but commands don't work
**Solution**:
1. Check bot has Message Content Intent enabled in Discord Developer Portal
2. Verify bot has permissions in Discord server
3. Check console logs for errors
### Video files not found
**Issue**: `Failed to read video file`
**Solution**:
1. Set `COMFY_OUTPUT_PATH` in `.env` to your ComfyUI output directory
2. Check path uses correct format for your OS
## Advanced Usage
### Batch Generation
Queue multiple workflow runs:
```bash
ttr!workflow-gen queue:10
```
Each run uses randomized seeds for variation.
### Custom Workflows
1. Design workflow in ComfyUI
2. Export as API format (Save → API Format)
3. Load in bot:
```bash
ttr!workflow-load path/to/workflow.json
```
4. Modify at runtime:
```bash
ttr!set-current-workflow-changes type:prompt My prompt
ttr!workflow-gen
```
### State Persistence
Workflow changes are automatically saved to `current-workflow-changes.json` and persist across bot restarts.
## Contributing
We welcome contributions! Please:
1. Read `DEVELOPMENT.md` for coding guidelines
2. Follow existing code style and patterns
3. Test your changes thoroughly
4. Update documentation as needed
## License
[Your License Here]
## Support
For issues or questions:
- Check the troubleshooting section above
- Review `DEVELOPMENT.md` for implementation details
- Check ComfyUI documentation for workflow issues
- Open an issue on GitHub
**Web UI WebSocket disconnects immediately**
Set a proper `WEB_SECRET_KEY` — an empty secret causes JWT validation failures.
## Credits
Built with:
- [discord.py](https://github.com/Rapptz/discord.py) - Discord API wrapper
- [ComfyUI](https://github.com/comfyanonymous/ComfyUI) - Stable Diffusion GUI
- [aiohttp](https://github.com/aio-libs/aiohttp) - Async HTTP client
- [websockets](https://github.com/python-websockets/websockets) - WebSocket implementation
- [discord.py](https://github.com/Rapptz/discord.py) Discord API
- [ComfyUI](https://github.com/comfyanonymous/ComfyUI) — AI image/video generation backend
- [FastAPI](https://fastapi.tiangolo.com/) — Web API framework
- [Uvicorn](https://www.uvicorn.org/) — ASGI server
- [React](https://react.dev/) + [Vite](https://vitejs.dev/) + [Tailwind CSS](https://tailwindcss.com/) — Web frontend
- [aiohttp](https://github.com/aio-libs/aiohttp) — Async HTTP client
- [Pillow](https://python-pillow.org/) — Image compression

View File

@@ -1,156 +0,0 @@
"""
backfill_image_data.py
======================
One-shot script to download image bytes from Discord and store them in
input_images.db for rows that currently have image_data = NULL.
These rows were created before the BLOB-storage migration, so their bytes
were never persisted. The script re-fetches each bot-reply message from
Discord and writes the raw attachment bytes back into the DB.
Rows with bot_reply_id = 0 (web uploads that pre-date the migration) have
no Discord source and are skipped — re-upload them via the web UI to
backfill.
Usage
-----
python backfill_image_data.py
Requires:
DISCORD_BOT_TOKEN in .env (same token the bot uses)
"""
from __future__ import annotations
import asyncio
import logging
import sqlite3
import discord
try:
from dotenv import load_dotenv
load_dotenv()
except Exception:
pass
from config import BotConfig
from input_image_db import DB_PATH
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logger = logging.getLogger(__name__)
def _load_null_rows() -> list[dict]:
"""Return all rows that are missing image_data."""
conn = sqlite3.connect(str(DB_PATH))
conn.row_factory = sqlite3.Row
rows = conn.execute(
"SELECT id, bot_reply_id, channel_id, filename"
" FROM input_images WHERE image_data IS NULL"
).fetchall()
conn.close()
return [dict(r) for r in rows]
def _save_image_data(row_id: int, data: bytes) -> None:
conn = sqlite3.connect(str(DB_PATH))
conn.execute("UPDATE input_images SET image_data = ? WHERE id = ?", (data, row_id))
conn.commit()
conn.close()
async def _backfill(client: discord.Client) -> None:
rows = _load_null_rows()
discord_rows = [r for r in rows if r["bot_reply_id"] != 0]
web_rows = [r for r in rows if r["bot_reply_id"] == 0]
logger.info(
"Rows missing image_data: %d total (%d from Discord, %d web-uploads skipped)",
len(rows), len(discord_rows), len(web_rows),
)
if web_rows:
logger.info(
"Skipped row IDs (no Discord source — re-upload via web UI): %s",
[r["id"] for r in web_rows],
)
if not discord_rows:
logger.info("Nothing to fetch. Exiting.")
return
ok = 0
failed = 0
for row in discord_rows:
row_id = row["id"]
ch_id = row["channel_id"]
msg_id = row["bot_reply_id"]
filename = row["filename"]
try:
channel = client.get_channel(ch_id) or await client.fetch_channel(ch_id)
message = await channel.fetch_message(msg_id)
attachment = next(
(a for a in message.attachments if a.filename == filename), None
)
if attachment is None:
logger.warning(
"Row %d: attachment '%s' not found on message %d — skipping",
row_id, filename, msg_id,
)
failed += 1
continue
data = await attachment.read()
_save_image_data(row_id, data)
logger.info("Row %d: saved '%s' (%d bytes)", row_id, filename, len(data))
ok += 1
except discord.NotFound:
logger.warning("Row %d: message %d not found (deleted?) — skipping", row_id, msg_id)
failed += 1
except discord.Forbidden:
logger.warning("Row %d: no access to channel %d — skipping", row_id, ch_id)
failed += 1
except Exception as exc:
logger.error("Row %d: unexpected error — %s", row_id, exc)
failed += 1
logger.info(
"Done. %d saved, %d failed/skipped, %d web-upload rows not touched.",
ok, failed, len(web_rows),
)
async def _main(token: str) -> None:
intents = discord.Intents.none() # no gateway events needed beyond connect
client = discord.Client(intents=intents)
@client.event
async def on_ready():
logger.info("Logged in as %s", client.user)
try:
await _backfill(client)
finally:
await client.close()
await client.start(token)
def main() -> None:
try:
config = BotConfig.from_env()
except RuntimeError as exc:
logger.error("Config error: %s", exc)
return
asyncio.run(_main(config.discord_bot_token))
if __name__ == "__main__":
main()