Initial commit — ComfyUI Discord bot + web UI

Full source for the-third-rev: Discord bot (discord.py), FastAPI web UI
(React/TS/Vite/Tailwind), ComfyUI integration, generation history DB,
preset manager, workflow inspector, and all supporting modules.

Excluded from tracking: .env, invite_tokens.json, *.db (SQLite),
current-workflow-changes.json, user_settings/, presets/, logs/,
web-static/ (build output), frontend/node_modules/.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Khoa (Revenovich) Tran Gia
2026-03-02 09:55:48 +07:00
commit 1ed3c9ec4b
82 changed files with 20693 additions and 0 deletions

283
config.py Normal file
View File

@@ -0,0 +1,283 @@
"""
config.py
=========
Configuration module for the Discord ComfyUI bot.
This module centralizes all constants, magic strings, and environment
variable loading to make configuration management easier and more maintainable.
"""
from __future__ import annotations
import os
from dataclasses import dataclass
from pathlib import Path
from typing import Optional
try:
from dotenv import load_dotenv
load_dotenv()
except Exception:
pass
# ========================================
# Command and Argument Constants
# ========================================
COMMAND_PREFIX = os.getenv("BOT_PREFIX", "ttr!")
"""The command prefix used for Discord bot commands."""
ARG_PROMPT_KEY = "prompt:"
"""The keyword marker for prompt arguments in commands."""
ARG_NEG_PROMPT_KEY = "negative_prompt:"
"""The keyword marker for negative prompt arguments in commands."""
ARG_TYPE_KEY = "type:"
"""The keyword marker for type arguments in commands."""
ARG_QUEUE_KEY = "queue:"
"""The keyword marker for queue count arguments in commands."""
# ========================================
# Discord and Message Constants
# ========================================
MAX_IMAGES_PER_RESPONSE = 4
"""Maximum number of images to include in a single Discord response."""
DEFAULT_UPLOAD_TYPE = "input"
"""Default folder type for ComfyUI image uploads."""
MESSAGE_AUTO_DELETE_TIMEOUT = 60.0
"""Default timeout in seconds for auto-deleting temporary messages."""
# ========================================
# Error Messages
# ========================================
COMFY_NOT_CONFIGURED_MSG = "ComfyUI client is not configured. Please set environment variables."
"""Error message displayed when ComfyUI client is not properly configured."""
# ========================================
# Default Configuration Values
# ========================================
DEFAULT_COMFY_HISTORY_LIMIT = 10
"""Default number of generation history entries to keep."""
# Resolve paths relative to this file's location so both the bot project and
# the portable ComfyUI folder only need to share the same parent directory.
# Layout assumed:
# <parent>/
# ComfyUI_windows_portable/ComfyUI/output ← default output
# ComfyUI_windows_portable/ComfyUI/input ← default input
# the-third-rev/ ← this project
_COMFY_PORTABLE_ROOT = Path(__file__).resolve().parent.parent / "ComfyUI_windows_portable" / "ComfyUI"
DEFAULT_COMFY_OUTPUT_PATH = str(_COMFY_PORTABLE_ROOT / "output")
DEFAULT_COMFY_INPUT_PATH = str(_COMFY_PORTABLE_ROOT / "input")
# ========================================
# Configuration Class
# ========================================
@dataclass
class BotConfig:
"""
Configuration container for the Discord ComfyUI bot.
This dataclass holds all configuration values loaded from environment
variables. Use the `from_env()` class method to create an instance
with values loaded from the environment.
Attributes
----------
discord_bot_token : str
Discord bot authentication token (required).
comfy_server : str
ComfyUI server address in format "hostname:port" (required).
comfy_output_path : str
Path to ComfyUI output directory for reading generated files.
comfy_history_limit : int
Number of generation history entries to keep in memory.
workflow_file : Optional[str]
Path to a workflow JSON file to load at startup (optional).
"""
discord_bot_token: str
comfy_server: str
comfy_output_path: str
comfy_input_path: str
comfy_history_limit: int
comfy_input_channel_id: int = 1475791295665405962
comfy_service_name: str = "ComfyUI"
comfy_start_bat: str = ""
comfy_log_dir: str = ""
comfy_log_max_mb: int = 10
comfy_autostart: bool = True
workflow_file: Optional[str] = None
log_channel_id: Optional[int] = None
zip_password: Optional[str] = None
media_upload_user: Optional[str] = None
media_upload_pass: Optional[str] = None
# Web UI fields
web_enabled: bool = True
web_host: str = "0.0.0.0"
web_port: int = 8080
web_secret_key: str = ""
web_token_file: str = "invite_tokens.json"
web_jwt_expire_hours: int = 8
web_secure_cookie: bool = True
admin_password: Optional[str] = None
@classmethod
def from_env(cls) -> BotConfig:
"""
Create a BotConfig instance by loading values from environment variables.
Environment Variables
---------------------
DISCORD_BOT_TOKEN : str (required)
Discord bot authentication token.
COMFY_SERVER : str (required)
ComfyUI server address (e.g., "localhost:8188" or "example.com:8188").
COMFY_OUTPUT_PATH : str (optional)
Path to ComfyUI output directory. Defaults to DEFAULT_COMFY_OUTPUT_PATH
if not specified.
COMFY_HISTORY_LIMIT : int (optional)
Number of generation history entries to keep. Defaults to
DEFAULT_COMFY_HISTORY_LIMIT if not specified or invalid.
WORKFLOW_FILE : str (optional)
Path to a workflow JSON file to load at startup.
Returns
-------
BotConfig
A configured BotConfig instance.
Raises
------
RuntimeError
If required environment variables (DISCORD_BOT_TOKEN or COMFY_SERVER)
are not set.
"""
# Load required variables
discord_token = os.getenv("DISCORD_BOT_TOKEN")
if not discord_token:
raise RuntimeError(
"DISCORD_BOT_TOKEN environment variable is required. "
"Please set it in your .env file or environment."
)
comfy_server = os.getenv("COMFY_SERVER")
if not comfy_server:
raise RuntimeError(
"COMFY_SERVER environment variable is required. "
"Please set it in your .env file or environment."
)
# Load optional variables with defaults
comfy_output_path = os.getenv("COMFY_OUTPUT_PATH", DEFAULT_COMFY_OUTPUT_PATH)
comfy_input_path = os.getenv("COMFY_INPUT_PATH", DEFAULT_COMFY_INPUT_PATH)
# Parse history limit with fallback to default
try:
comfy_history_limit = int(os.getenv("COMFY_HISTORY_LIMIT", str(DEFAULT_COMFY_HISTORY_LIMIT)))
except ValueError:
comfy_history_limit = DEFAULT_COMFY_HISTORY_LIMIT
workflow_file = os.getenv("WORKFLOW_FILE")
log_channel_id_str = os.getenv("LOG_CHANNEL_ID", "1475408462740721809")
try:
log_channel_id = int(log_channel_id_str) if log_channel_id_str else None
except ValueError:
log_channel_id = None
zip_password = os.getenv("ZIP_PASSWORD", "0Revel512796@")
media_upload_user = os.getenv("MEDIA_UPLOAD_USER") or None
media_upload_pass = os.getenv("MEDIA_UPLOAD_PASS") or None
try:
comfy_input_channel_id = int(os.getenv("COMFY_INPUT_CHANNEL_ID", "1475791295665405962"))
except ValueError:
comfy_input_channel_id = 1475791295665405962
comfy_service_name = os.getenv("COMFY_SERVICE_NAME", "ComfyUI")
default_bat = str(_COMFY_PORTABLE_ROOT.parent / "run_nvidia_gpu.bat")
comfy_start_bat = os.getenv("COMFY_START_BAT", default_bat)
default_log_dir = str(_COMFY_PORTABLE_ROOT.parent / "logs")
comfy_log_dir = os.getenv("COMFY_LOG_DIR", default_log_dir)
try:
comfy_log_max_mb = int(os.getenv("COMFY_LOG_MAX_MB", "10"))
except ValueError:
comfy_log_max_mb = 10
comfy_autostart = os.getenv("COMFY_AUTOSTART", "true").lower() not in ("false", "0", "no")
# Web UI config
web_enabled = os.getenv("WEB_ENABLED", "true").lower() not in ("false", "0", "no")
web_host = os.getenv("WEB_HOST", "0.0.0.0")
try:
web_port = int(os.getenv("WEB_PORT", "8080"))
except ValueError:
web_port = 8080
web_secret_key = os.getenv("WEB_SECRET_KEY", "")
web_token_file = os.getenv("WEB_TOKEN_FILE", "invite_tokens.json")
try:
web_jwt_expire_hours = int(os.getenv("WEB_JWT_EXPIRE_HOURS", "8"))
except ValueError:
web_jwt_expire_hours = 8
web_secure_cookie = os.getenv("WEB_SECURE_COOKIE", "true").lower() not in ("false", "0", "no")
admin_password = os.getenv("ADMIN_PASSWORD") or None
return cls(
discord_bot_token=discord_token,
comfy_server=comfy_server,
comfy_output_path=comfy_output_path,
comfy_input_path=comfy_input_path,
comfy_history_limit=comfy_history_limit,
comfy_input_channel_id=comfy_input_channel_id,
comfy_service_name=comfy_service_name,
comfy_start_bat=comfy_start_bat,
comfy_log_dir=comfy_log_dir,
comfy_log_max_mb=comfy_log_max_mb,
comfy_autostart=comfy_autostart,
workflow_file=workflow_file,
log_channel_id=log_channel_id,
zip_password=zip_password,
media_upload_user=media_upload_user,
media_upload_pass=media_upload_pass,
web_enabled=web_enabled,
web_host=web_host,
web_port=web_port,
web_secret_key=web_secret_key,
web_token_file=web_token_file,
web_jwt_expire_hours=web_jwt_expire_hours,
web_secure_cookie=web_secure_cookie,
admin_password=admin_password,
)
def __repr__(self) -> str:
"""Return a string representation with sensitive data masked."""
return (
f"BotConfig("
f"discord_bot_token='***masked***', "
f"comfy_server='{self.comfy_server}', "
f"comfy_output_path='{self.comfy_output_path}', "
f"comfy_input_path='{self.comfy_input_path}', "
f"comfy_history_limit={self.comfy_history_limit}, "
f"comfy_input_channel_id={self.comfy_input_channel_id}, "
f"workflow_file={self.workflow_file!r}, "
f"log_channel_id={self.log_channel_id!r}, "
f"zip_password={'***masked***' if self.zip_password else None})"
)