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:
194
web/routers/inputs_router.py
Normal file
194
web/routers/inputs_router.py
Normal file
@@ -0,0 +1,194 @@
|
||||
"""GET/POST/DELETE /api/inputs; GET /api/inputs/{id}/image; POST /api/inputs/{id}/activate"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import mimetypes
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, File, Form, HTTPException, UploadFile
|
||||
from fastapi.responses import Response
|
||||
|
||||
from web.auth import require_auth
|
||||
from web.deps import get_config, get_user_registry
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_inputs(_: dict = Depends(require_auth)):
|
||||
"""List all input images (Discord + web uploads)."""
|
||||
from input_image_db import get_all_images
|
||||
rows = get_all_images()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def upload_input(
|
||||
file: UploadFile = File(...),
|
||||
slot_key: Optional[str] = Form(default=None),
|
||||
user: dict = Depends(require_auth),
|
||||
):
|
||||
"""
|
||||
Upload an input image.
|
||||
|
||||
Stores image bytes directly in SQLite. If *slot_key* is provided the
|
||||
image is immediately activated for that slot (writes to ComfyUI input
|
||||
folder and updates the user's state override).
|
||||
|
||||
The physical slot file uses a namespaced key ``<user_label>_<slot_key>``
|
||||
so concurrent users each get their own active image file.
|
||||
"""
|
||||
config = get_config()
|
||||
if config is None:
|
||||
raise HTTPException(503, "Config not available")
|
||||
|
||||
data = await file.read()
|
||||
filename = file.filename or "upload.png"
|
||||
|
||||
from input_image_db import upsert_image, activate_image_for_slot
|
||||
row_id = upsert_image(
|
||||
original_message_id=0, # sentinel for web uploads
|
||||
bot_reply_id=0,
|
||||
channel_id=0,
|
||||
filename=filename,
|
||||
image_data=data,
|
||||
)
|
||||
|
||||
activated_filename: str | None = None
|
||||
if slot_key:
|
||||
user_label: str = user["sub"]
|
||||
namespaced_key = f"{user_label}_{slot_key}"
|
||||
activated_filename = activate_image_for_slot(
|
||||
row_id, namespaced_key, config.comfy_input_path
|
||||
)
|
||||
registry = get_user_registry()
|
||||
if registry:
|
||||
registry.get_state_manager(user_label).set_override(slot_key, activated_filename)
|
||||
else:
|
||||
from web.deps import get_comfy
|
||||
comfy = get_comfy()
|
||||
if comfy:
|
||||
comfy.state_manager.set_override(slot_key, activated_filename)
|
||||
|
||||
return {"id": row_id, "filename": filename, "slot_key": slot_key, "activated_filename": activated_filename}
|
||||
|
||||
|
||||
@router.post("/{row_id}/activate")
|
||||
async def activate_input(
|
||||
row_id: int,
|
||||
slot_key: str = Body(default="input_image", embed=True),
|
||||
user: dict = Depends(require_auth),
|
||||
):
|
||||
"""Write the stored image to the ComfyUI input folder and set the user's slot override."""
|
||||
config = get_config()
|
||||
if config is None:
|
||||
raise HTTPException(503, "Config not available")
|
||||
|
||||
from input_image_db import get_image, activate_image_for_slot
|
||||
row = get_image(row_id)
|
||||
if row is None:
|
||||
raise HTTPException(404, "Image not found")
|
||||
|
||||
user_label: str = user["sub"]
|
||||
namespaced_key = f"{user_label}_{slot_key}"
|
||||
|
||||
try:
|
||||
filename = activate_image_for_slot(row_id, namespaced_key, config.comfy_input_path)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(409, str(exc))
|
||||
|
||||
registry = get_user_registry()
|
||||
if registry:
|
||||
registry.get_state_manager(user_label).set_override(slot_key, filename)
|
||||
else:
|
||||
from web.deps import get_comfy
|
||||
comfy = get_comfy()
|
||||
if comfy is None:
|
||||
raise HTTPException(503, "State manager not available")
|
||||
comfy.state_manager.set_override(slot_key, filename)
|
||||
|
||||
return {"ok": True, "slot_key": slot_key, "filename": filename}
|
||||
|
||||
|
||||
@router.delete("/{row_id}")
|
||||
async def delete_input(row_id: int, _: dict = Depends(require_auth)):
|
||||
"""Delete an input image record (and its active slot file if applicable)."""
|
||||
from input_image_db import get_image, delete_image
|
||||
row = get_image(row_id)
|
||||
if row is None:
|
||||
raise HTTPException(404, "Image not found")
|
||||
|
||||
config = get_config()
|
||||
delete_image(row_id, comfy_input_path=config.comfy_input_path if config else None)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.get("/{row_id}/image")
|
||||
async def get_input_image(row_id: int, _: dict = Depends(require_auth)):
|
||||
"""Serve the raw image bytes stored in the database for a given input image row."""
|
||||
from input_image_db import get_image, get_image_data
|
||||
row = get_image(row_id)
|
||||
if row is None:
|
||||
raise HTTPException(404, "Image not found")
|
||||
|
||||
data = get_image_data(row_id)
|
||||
if data is None:
|
||||
raise HTTPException(404, "Image data not available — re-upload to backfill")
|
||||
|
||||
mime, _ = mimetypes.guess_type(row["filename"])
|
||||
return Response(content=data, media_type=mime or "application/octet-stream")
|
||||
|
||||
|
||||
def _pil_resize_response(data: bytes, filename: str, max_size: int, quality: int) -> Response:
|
||||
"""Resize image bytes with Pillow and return a JPEG Response. Raises on failure."""
|
||||
import io
|
||||
from PIL import Image as _PIL
|
||||
img = _PIL.open(io.BytesIO(data))
|
||||
img.thumbnail((max_size, max_size), _PIL.LANCZOS)
|
||||
buf = io.BytesIO()
|
||||
img.convert("RGB").save(buf, "JPEG", quality=quality, optimize=True)
|
||||
return Response(
|
||||
content=buf.getvalue(),
|
||||
media_type="image/jpeg",
|
||||
headers={"Cache-Control": "public, max-age=86400"},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{row_id}/thumb")
|
||||
async def get_input_thumb(row_id: int, _: dict = Depends(require_auth)):
|
||||
"""Serve a small compressed thumbnail (max 200 px, JPEG 65 %) for fast previews."""
|
||||
from input_image_db import get_image, get_image_data
|
||||
row = get_image(row_id)
|
||||
if row is None:
|
||||
raise HTTPException(404, "Image not found")
|
||||
|
||||
data = get_image_data(row_id)
|
||||
if data is None:
|
||||
raise HTTPException(404, "Image data not available — re-upload to backfill")
|
||||
|
||||
try:
|
||||
return _pil_resize_response(data, row["filename"], max_size=200, quality=65)
|
||||
except Exception:
|
||||
mime, _ = mimetypes.guess_type(row["filename"])
|
||||
return Response(content=data, media_type=mime or "application/octet-stream")
|
||||
|
||||
|
||||
@router.get("/{row_id}/mid")
|
||||
async def get_input_mid(row_id: int, _: dict = Depends(require_auth)):
|
||||
"""Serve a medium compressed image (max 800 px, JPEG 80 %) for progressive loading."""
|
||||
from input_image_db import get_image, get_image_data
|
||||
row = get_image(row_id)
|
||||
if row is None:
|
||||
raise HTTPException(404, "Image not found")
|
||||
|
||||
data = get_image_data(row_id)
|
||||
if data is None:
|
||||
raise HTTPException(404, "Image data not available — re-upload to backfill")
|
||||
|
||||
try:
|
||||
return _pil_resize_response(data, row["filename"], max_size=800, quality=80)
|
||||
except Exception:
|
||||
mime, _ = mimetypes.guess_type(row["filename"])
|
||||
return Response(content=data, media_type=mime or "application/octet-stream")
|
||||
Reference in New Issue
Block a user