mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-12 04:15:25 +00:00
feat: comfyui image edit support
This commit is contained in:
parent
74db2b9f36
commit
63e8ab7a05
5 changed files with 296 additions and 46 deletions
|
|
@ -3351,6 +3351,29 @@ IMAGES_EDIT_GEMINI_API_KEY = PersistentConfig(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
IMAGES_EDIT_COMFYUI_BASE_URL = PersistentConfig(
|
||||||
|
"IMAGES_EDIT_COMFYUI_BASE_URL",
|
||||||
|
"images.edit.comfyui.base_url",
|
||||||
|
os.getenv("IMAGES_EDIT_COMFYUI_BASE_URL", ""),
|
||||||
|
)
|
||||||
|
IMAGES_EDIT_COMFYUI_API_KEY = PersistentConfig(
|
||||||
|
"IMAGES_EDIT_COMFYUI_API_KEY",
|
||||||
|
"images.edit.comfyui.api_key",
|
||||||
|
os.getenv("IMAGES_EDIT_COMFYUI_API_KEY", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
IMAGES_EDIT_COMFYUI_WORKFLOW = PersistentConfig(
|
||||||
|
"IMAGES_EDIT_COMFYUI_WORKFLOW",
|
||||||
|
"images.edit.comfyui.workflow",
|
||||||
|
os.getenv("IMAGES_EDIT_COMFYUI_WORKFLOW", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
IMAGES_EDIT_COMFYUI_WORKFLOW_NODES = PersistentConfig(
|
||||||
|
"IMAGES_EDIT_COMFYUI_WORKFLOW_NODES",
|
||||||
|
"images.edit.comfyui.nodes",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# Audio
|
# Audio
|
||||||
####################################
|
####################################
|
||||||
|
|
|
||||||
|
|
@ -171,6 +171,10 @@ from open_webui.config import (
|
||||||
IMAGES_EDIT_OPENAI_API_VERSION,
|
IMAGES_EDIT_OPENAI_API_VERSION,
|
||||||
IMAGES_EDIT_GEMINI_API_BASE_URL,
|
IMAGES_EDIT_GEMINI_API_BASE_URL,
|
||||||
IMAGES_EDIT_GEMINI_API_KEY,
|
IMAGES_EDIT_GEMINI_API_KEY,
|
||||||
|
IMAGES_EDIT_COMFYUI_BASE_URL,
|
||||||
|
IMAGES_EDIT_COMFYUI_API_KEY,
|
||||||
|
IMAGES_EDIT_COMFYUI_WORKFLOW,
|
||||||
|
IMAGES_EDIT_COMFYUI_WORKFLOW_NODES,
|
||||||
# Audio
|
# Audio
|
||||||
AUDIO_STT_ENGINE,
|
AUDIO_STT_ENGINE,
|
||||||
AUDIO_STT_MODEL,
|
AUDIO_STT_MODEL,
|
||||||
|
|
@ -1106,6 +1110,10 @@ app.state.config.IMAGES_EDIT_OPENAI_API_KEY = IMAGES_EDIT_OPENAI_API_KEY
|
||||||
app.state.config.IMAGES_EDIT_OPENAI_API_VERSION = IMAGES_EDIT_OPENAI_API_VERSION
|
app.state.config.IMAGES_EDIT_OPENAI_API_VERSION = IMAGES_EDIT_OPENAI_API_VERSION
|
||||||
app.state.config.IMAGES_EDIT_GEMINI_API_BASE_URL = IMAGES_EDIT_GEMINI_API_BASE_URL
|
app.state.config.IMAGES_EDIT_GEMINI_API_BASE_URL = IMAGES_EDIT_GEMINI_API_BASE_URL
|
||||||
app.state.config.IMAGES_EDIT_GEMINI_API_KEY = IMAGES_EDIT_GEMINI_API_KEY
|
app.state.config.IMAGES_EDIT_GEMINI_API_KEY = IMAGES_EDIT_GEMINI_API_KEY
|
||||||
|
app.state.config.IMAGES_EDIT_COMFYUI_BASE_URL = IMAGES_EDIT_COMFYUI_BASE_URL
|
||||||
|
app.state.config.IMAGES_EDIT_COMFYUI_API_KEY = IMAGES_EDIT_COMFYUI_API_KEY
|
||||||
|
app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW = IMAGES_EDIT_COMFYUI_WORKFLOW
|
||||||
|
app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES = IMAGES_EDIT_COMFYUI_WORKFLOW_NODES
|
||||||
|
|
||||||
|
|
||||||
########################################
|
########################################
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,11 @@ from open_webui.utils.auth import get_admin_user, get_verified_user
|
||||||
from open_webui.utils.headers import include_user_info_headers
|
from open_webui.utils.headers import include_user_info_headers
|
||||||
from open_webui.utils.images.comfyui import (
|
from open_webui.utils.images.comfyui import (
|
||||||
ComfyUICreateImageForm,
|
ComfyUICreateImageForm,
|
||||||
|
ComfyUIEditImageForm,
|
||||||
ComfyUIWorkflow,
|
ComfyUIWorkflow,
|
||||||
|
comfyui_upload_image,
|
||||||
comfyui_create_image,
|
comfyui_create_image,
|
||||||
|
comfyui_edit_image,
|
||||||
)
|
)
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
@ -126,6 +129,10 @@ class ImagesConfig(BaseModel):
|
||||||
IMAGES_EDIT_OPENAI_API_VERSION: str
|
IMAGES_EDIT_OPENAI_API_VERSION: str
|
||||||
IMAGES_EDIT_GEMINI_API_BASE_URL: str
|
IMAGES_EDIT_GEMINI_API_BASE_URL: str
|
||||||
IMAGES_EDIT_GEMINI_API_KEY: str
|
IMAGES_EDIT_GEMINI_API_KEY: str
|
||||||
|
IMAGES_EDIT_COMFYUI_BASE_URL: str
|
||||||
|
IMAGES_EDIT_COMFYUI_API_KEY: str
|
||||||
|
IMAGES_EDIT_COMFYUI_WORKFLOW: str
|
||||||
|
IMAGES_EDIT_COMFYUI_WORKFLOW_NODES: list[dict]
|
||||||
|
|
||||||
|
|
||||||
@router.get("/config", response_model=ImagesConfig)
|
@router.get("/config", response_model=ImagesConfig)
|
||||||
|
|
@ -158,6 +165,10 @@ async def get_config(request: Request, user=Depends(get_admin_user)):
|
||||||
"IMAGES_EDIT_OPENAI_API_VERSION": request.app.state.config.IMAGES_EDIT_OPENAI_API_VERSION,
|
"IMAGES_EDIT_OPENAI_API_VERSION": request.app.state.config.IMAGES_EDIT_OPENAI_API_VERSION,
|
||||||
"IMAGES_EDIT_GEMINI_API_BASE_URL": request.app.state.config.IMAGES_EDIT_GEMINI_API_BASE_URL,
|
"IMAGES_EDIT_GEMINI_API_BASE_URL": request.app.state.config.IMAGES_EDIT_GEMINI_API_BASE_URL,
|
||||||
"IMAGES_EDIT_GEMINI_API_KEY": request.app.state.config.IMAGES_EDIT_GEMINI_API_KEY,
|
"IMAGES_EDIT_GEMINI_API_KEY": request.app.state.config.IMAGES_EDIT_GEMINI_API_KEY,
|
||||||
|
"IMAGES_EDIT_COMFYUI_BASE_URL": request.app.state.config.IMAGES_EDIT_COMFYUI_BASE_URL,
|
||||||
|
"IMAGES_EDIT_COMFYUI_API_KEY": request.app.state.config.IMAGES_EDIT_COMFYUI_API_KEY,
|
||||||
|
"IMAGES_EDIT_COMFYUI_WORKFLOW": request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW,
|
||||||
|
"IMAGES_EDIT_COMFYUI_WORKFLOW_NODES": request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -253,6 +264,19 @@ async def update_config(
|
||||||
form_data.IMAGES_EDIT_GEMINI_API_KEY
|
form_data.IMAGES_EDIT_GEMINI_API_KEY
|
||||||
)
|
)
|
||||||
|
|
||||||
|
request.app.state.config.IMAGES_EDIT_COMFYUI_BASE_URL = (
|
||||||
|
form_data.IMAGES_EDIT_COMFYUI_BASE_URL.strip("/")
|
||||||
|
)
|
||||||
|
request.app.state.config.IMAGES_EDIT_COMFYUI_API_KEY = (
|
||||||
|
form_data.IMAGES_EDIT_COMFYUI_API_KEY
|
||||||
|
)
|
||||||
|
request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW = (
|
||||||
|
form_data.IMAGES_EDIT_COMFYUI_WORKFLOW
|
||||||
|
)
|
||||||
|
request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES = (
|
||||||
|
form_data.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"ENABLE_IMAGE_GENERATION": request.app.state.config.ENABLE_IMAGE_GENERATION,
|
"ENABLE_IMAGE_GENERATION": request.app.state.config.ENABLE_IMAGE_GENERATION,
|
||||||
"ENABLE_IMAGE_PROMPT_GENERATION": request.app.state.config.ENABLE_IMAGE_PROMPT_GENERATION,
|
"ENABLE_IMAGE_PROMPT_GENERATION": request.app.state.config.ENABLE_IMAGE_PROMPT_GENERATION,
|
||||||
|
|
@ -281,6 +305,10 @@ async def update_config(
|
||||||
"IMAGES_EDIT_OPENAI_API_VERSION": request.app.state.config.IMAGES_EDIT_OPENAI_API_VERSION,
|
"IMAGES_EDIT_OPENAI_API_VERSION": request.app.state.config.IMAGES_EDIT_OPENAI_API_VERSION,
|
||||||
"IMAGES_EDIT_GEMINI_API_BASE_URL": request.app.state.config.IMAGES_EDIT_GEMINI_API_BASE_URL,
|
"IMAGES_EDIT_GEMINI_API_BASE_URL": request.app.state.config.IMAGES_EDIT_GEMINI_API_BASE_URL,
|
||||||
"IMAGES_EDIT_GEMINI_API_KEY": request.app.state.config.IMAGES_EDIT_GEMINI_API_KEY,
|
"IMAGES_EDIT_GEMINI_API_KEY": request.app.state.config.IMAGES_EDIT_GEMINI_API_KEY,
|
||||||
|
"IMAGES_EDIT_COMFYUI_BASE_URL": request.app.state.config.IMAGES_EDIT_COMFYUI_BASE_URL,
|
||||||
|
"IMAGES_EDIT_COMFYUI_API_KEY": request.app.state.config.IMAGES_EDIT_COMFYUI_API_KEY,
|
||||||
|
"IMAGES_EDIT_COMFYUI_WORKFLOW": request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW,
|
||||||
|
"IMAGES_EDIT_COMFYUI_WORKFLOW_NODES": request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -790,6 +818,20 @@ async def image_edits(
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
|
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
|
||||||
|
|
||||||
|
def get_image_file_item(base64_string):
|
||||||
|
data = base64_string
|
||||||
|
header, encoded = data.split(",", 1)
|
||||||
|
mime_type = header.split(";")[0].lstrip("data:")
|
||||||
|
image_data = base64.b64decode(encoded)
|
||||||
|
return (
|
||||||
|
"image",
|
||||||
|
(
|
||||||
|
f"{uuid.uuid4()}.png",
|
||||||
|
io.BytesIO(image_data),
|
||||||
|
mime_type if mime_type else "image/png",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
r = None
|
r = None
|
||||||
try:
|
try:
|
||||||
if request.app.state.config.IMAGE_EDIT_ENGINE == "openai":
|
if request.app.state.config.IMAGE_EDIT_ENGINE == "openai":
|
||||||
|
|
@ -807,25 +849,11 @@ async def image_edits(
|
||||||
**({"size": size} if size else {}),
|
**({"size": size} if size else {}),
|
||||||
**(
|
**(
|
||||||
{}
|
{}
|
||||||
if "gpt-image-1" in request.app.state.config.IMAGE_GENERATION_MODEL
|
if "gpt-image-1" in request.app.state.config.IMAGE_EDIT_MODEL
|
||||||
else {"response_format": "b64_json"}
|
else {"response_format": "b64_json"}
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_image_file_item(base64_string):
|
|
||||||
data = base64_string
|
|
||||||
header, encoded = data.split(",", 1)
|
|
||||||
mime_type = header.split(";")[0].lstrip("data:")
|
|
||||||
image_data = base64.b64decode(encoded)
|
|
||||||
return (
|
|
||||||
"image",
|
|
||||||
(
|
|
||||||
f"{uuid.uuid4()}.png",
|
|
||||||
io.BytesIO(image_data),
|
|
||||||
mime_type if mime_type else "image/png",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
files = []
|
files = []
|
||||||
if isinstance(form_data.image, str):
|
if isinstance(form_data.image, str):
|
||||||
files = [get_image_file_item(form_data.image)]
|
files = [get_image_file_item(form_data.image)]
|
||||||
|
|
@ -840,7 +868,7 @@ async def image_edits(
|
||||||
# Use asyncio.to_thread for the requests.post call
|
# Use asyncio.to_thread for the requests.post call
|
||||||
r = await asyncio.to_thread(
|
r = await asyncio.to_thread(
|
||||||
requests.post,
|
requests.post,
|
||||||
url=f"{request.app.state.config.IMAGES_OPENAI_API_BASE_URL}/images/edits{url_search_params}",
|
url=f"{request.app.state.config.IMAGES_EDIT_OPENAI_API_BASE_URL}/images/edits{url_search_params}",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
files=files,
|
files=files,
|
||||||
data=data,
|
data=data,
|
||||||
|
|
@ -860,10 +888,10 @@ async def image_edits(
|
||||||
images.append({"url": url})
|
images.append({"url": url})
|
||||||
return images
|
return images
|
||||||
|
|
||||||
elif request.app.state.config.IMAGE_GENERATION_ENGINE == "gemini":
|
elif request.app.state.config.IMAGE_EDIT_ENGINE == "gemini":
|
||||||
headers = {
|
headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"x-goog-api-key": request.app.state.config.IMAGES_GEMINI_API_KEY,
|
"x-goog-api-key": request.app.state.config.IMAGES_EDIT_GEMINI_API_KEY,
|
||||||
}
|
}
|
||||||
|
|
||||||
model = f"{model}:generateContent"
|
model = f"{model}:generateContent"
|
||||||
|
|
@ -894,7 +922,7 @@ async def image_edits(
|
||||||
# Use asyncio.to_thread for the requests.post call
|
# Use asyncio.to_thread for the requests.post call
|
||||||
r = await asyncio.to_thread(
|
r = await asyncio.to_thread(
|
||||||
requests.post,
|
requests.post,
|
||||||
url=f"{request.app.state.config.IMAGES_GEMINI_API_BASE_URL}/models/{model}",
|
url=f"{request.app.state.config.IMAGES_EDIT_GEMINI_API_BASE_URL}/models/{model}",
|
||||||
json=data,
|
json=data,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
)
|
)
|
||||||
|
|
@ -916,50 +944,77 @@ async def image_edits(
|
||||||
|
|
||||||
return images
|
return images
|
||||||
|
|
||||||
elif request.app.state.config.IMAGE_GENERATION_ENGINE == "comfyui":
|
elif request.app.state.config.IMAGE_EDIT_ENGINE == "comfyui":
|
||||||
|
try:
|
||||||
|
files = []
|
||||||
|
if isinstance(form_data.image, str):
|
||||||
|
files = [get_image_file_item(form_data.image)]
|
||||||
|
elif isinstance(form_data.image, list):
|
||||||
|
for img in form_data.image:
|
||||||
|
files.append(get_image_file_item(img))
|
||||||
|
|
||||||
|
# Upload images to ComfyUI and get their names
|
||||||
|
comfyui_images = []
|
||||||
|
for file_item in files:
|
||||||
|
res = await comfyui_upload_image(
|
||||||
|
file_item,
|
||||||
|
request.app.state.config.IMAGES_EDIT_COMFYUI_BASE_URL,
|
||||||
|
request.app.state.config.IMAGES_EDIT_COMFYUI_API_KEY,
|
||||||
|
)
|
||||||
|
comfyui_images.append(res.get("name", file_item[1][0]))
|
||||||
|
except Exception as e:
|
||||||
|
log.debug(f"Error uploading images to ComfyUI: {e}")
|
||||||
|
raise Exception("Failed to upload images to ComfyUI.")
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
|
"image": comfyui_images,
|
||||||
"prompt": form_data.prompt,
|
"prompt": form_data.prompt,
|
||||||
"width": width,
|
**({"width": width} if width is not None else {}),
|
||||||
"height": height,
|
**({"height": height} if height is not None else {}),
|
||||||
"n": form_data.n,
|
**({"n": form_data.n} if form_data.n else {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
if request.app.state.config.IMAGE_EDIT_STEPS is not None:
|
form_data = ComfyUIEditImageForm(
|
||||||
data["steps"] = request.app.state.config.IMAGE_EDIT_STEPS
|
|
||||||
|
|
||||||
if form_data.negative_prompt is not None:
|
|
||||||
data["negative_prompt"] = form_data.negative_prompt
|
|
||||||
|
|
||||||
form_data = ComfyUICreateImageForm(
|
|
||||||
**{
|
**{
|
||||||
"workflow": ComfyUIWorkflow(
|
"workflow": ComfyUIWorkflow(
|
||||||
**{
|
**{
|
||||||
"workflow": request.app.state.config.COMFYUI_WORKFLOW,
|
"workflow": request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW,
|
||||||
"nodes": request.app.state.config.COMFYUI_WORKFLOW_NODES,
|
"nodes": request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
**data,
|
**data,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
res = await comfyui_create_image(
|
res = await comfyui_edit_image(
|
||||||
model,
|
model,
|
||||||
form_data,
|
form_data,
|
||||||
user.id,
|
user.id,
|
||||||
request.app.state.config.COMFYUI_BASE_URL,
|
request.app.state.config.IMAGES_EDIT_COMFYUI_BASE_URL,
|
||||||
request.app.state.config.COMFYUI_API_KEY,
|
request.app.state.config.IMAGES_EDIT_COMFYUI_API_KEY,
|
||||||
)
|
)
|
||||||
log.debug(f"res: {res}")
|
log.debug(f"res: {res}")
|
||||||
|
|
||||||
|
image_urls = set()
|
||||||
|
for image in res["data"]:
|
||||||
|
image_urls.add(image["url"])
|
||||||
|
image_urls = list(image_urls)
|
||||||
|
|
||||||
|
# Prioritize output type URLs if available
|
||||||
|
output_type_urls = [url for url in image_urls if "type=output" in url]
|
||||||
|
if output_type_urls:
|
||||||
|
image_urls = output_type_urls
|
||||||
|
|
||||||
|
log.debug(f"Image URLs: {image_urls}")
|
||||||
images = []
|
images = []
|
||||||
|
|
||||||
for image in res["data"]:
|
for image_url in image_urls:
|
||||||
headers = None
|
headers = None
|
||||||
if request.app.state.config.COMFYUI_API_KEY:
|
if request.app.state.config.IMAGES_EDIT_COMFYUI_API_KEY:
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"Bearer {request.app.state.config.COMFYUI_API_KEY}"
|
"Authorization": f"Bearer {request.app.state.config.IMAGES_EDIT_COMFYUI_API_KEY}"
|
||||||
}
|
}
|
||||||
|
|
||||||
image_data, content_type = get_image_data(image["url"], headers)
|
image_data, content_type = get_image_data(image_url, headers)
|
||||||
url = upload_image(
|
url = upload_image(
|
||||||
request,
|
request,
|
||||||
image_data,
|
image_data,
|
||||||
|
|
@ -968,6 +1023,7 @@ async def image_edits(
|
||||||
user,
|
user,
|
||||||
)
|
)
|
||||||
images.append({"url": url})
|
images.append({"url": url})
|
||||||
|
|
||||||
return images
|
return images
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error = e
|
error = e
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
|
import requests
|
||||||
|
import aiohttp
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
@ -91,6 +93,25 @@ def get_images(ws, prompt, client_id, base_url, api_key):
|
||||||
return {"data": output_images}
|
return {"data": output_images}
|
||||||
|
|
||||||
|
|
||||||
|
async def comfyui_upload_image(image_file_item, base_url, api_key):
|
||||||
|
url = f"{base_url}/api/upload/image"
|
||||||
|
headers = {}
|
||||||
|
|
||||||
|
if api_key:
|
||||||
|
headers["Authorization"] = f"Bearer {api_key}"
|
||||||
|
|
||||||
|
_, (filename, file_bytes, mime_type) = image_file_item
|
||||||
|
|
||||||
|
form = aiohttp.FormData()
|
||||||
|
form.add_field("image", file_bytes, filename=filename, content_type=mime_type)
|
||||||
|
form.add_field("type", "input") # required by ComfyUI
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.post(url, data=form, headers=headers) as resp:
|
||||||
|
resp.raise_for_status()
|
||||||
|
return await resp.json()
|
||||||
|
|
||||||
|
|
||||||
class ComfyUINodeInput(BaseModel):
|
class ComfyUINodeInput(BaseModel):
|
||||||
type: Optional[str] = None
|
type: Optional[str] = None
|
||||||
node_ids: list[str] = []
|
node_ids: list[str] = []
|
||||||
|
|
@ -191,3 +212,102 @@ async def comfyui_create_image(
|
||||||
ws.close()
|
ws.close()
|
||||||
|
|
||||||
return images
|
return images
|
||||||
|
|
||||||
|
|
||||||
|
class ComfyUIEditImageForm(BaseModel):
|
||||||
|
workflow: ComfyUIWorkflow
|
||||||
|
|
||||||
|
image: str | list[str]
|
||||||
|
prompt: str
|
||||||
|
width: Optional[int] = None
|
||||||
|
height: Optional[int] = None
|
||||||
|
n: Optional[int] = None
|
||||||
|
|
||||||
|
steps: Optional[int] = None
|
||||||
|
seed: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
async def comfyui_edit_image(
|
||||||
|
model: str, payload: ComfyUIEditImageForm, client_id, base_url, api_key
|
||||||
|
):
|
||||||
|
ws_url = base_url.replace("http://", "ws://").replace("https://", "wss://")
|
||||||
|
workflow = json.loads(payload.workflow.workflow)
|
||||||
|
|
||||||
|
for node in payload.workflow.nodes:
|
||||||
|
if node.type:
|
||||||
|
if node.type == "model":
|
||||||
|
for node_id in node.node_ids:
|
||||||
|
workflow[node_id]["inputs"][node.key] = model
|
||||||
|
elif node.type == "image":
|
||||||
|
if isinstance(payload.image, list):
|
||||||
|
# check if multiple images are provided
|
||||||
|
for idx, node_id in enumerate(node.node_ids):
|
||||||
|
if idx < len(payload.image):
|
||||||
|
workflow[node_id]["inputs"][node.key] = payload.image[idx]
|
||||||
|
else:
|
||||||
|
for node_id in node.node_ids:
|
||||||
|
workflow[node_id]["inputs"][node.key] = payload.image
|
||||||
|
elif node.type == "prompt":
|
||||||
|
for node_id in node.node_ids:
|
||||||
|
workflow[node_id]["inputs"][
|
||||||
|
node.key if node.key else "text"
|
||||||
|
] = payload.prompt
|
||||||
|
elif node.type == "negative_prompt":
|
||||||
|
for node_id in node.node_ids:
|
||||||
|
workflow[node_id]["inputs"][
|
||||||
|
node.key if node.key else "text"
|
||||||
|
] = payload.negative_prompt
|
||||||
|
elif node.type == "width":
|
||||||
|
for node_id in node.node_ids:
|
||||||
|
workflow[node_id]["inputs"][
|
||||||
|
node.key if node.key else "width"
|
||||||
|
] = payload.width
|
||||||
|
elif node.type == "height":
|
||||||
|
for node_id in node.node_ids:
|
||||||
|
workflow[node_id]["inputs"][
|
||||||
|
node.key if node.key else "height"
|
||||||
|
] = payload.height
|
||||||
|
elif node.type == "n":
|
||||||
|
for node_id in node.node_ids:
|
||||||
|
workflow[node_id]["inputs"][
|
||||||
|
node.key if node.key else "batch_size"
|
||||||
|
] = payload.n
|
||||||
|
elif node.type == "steps":
|
||||||
|
for node_id in node.node_ids:
|
||||||
|
workflow[node_id]["inputs"][
|
||||||
|
node.key if node.key else "steps"
|
||||||
|
] = payload.steps
|
||||||
|
elif node.type == "seed":
|
||||||
|
seed = (
|
||||||
|
payload.seed
|
||||||
|
if payload.seed
|
||||||
|
else random.randint(0, 1125899906842624)
|
||||||
|
)
|
||||||
|
for node_id in node.node_ids:
|
||||||
|
workflow[node_id]["inputs"][node.key] = seed
|
||||||
|
else:
|
||||||
|
for node_id in node.node_ids:
|
||||||
|
workflow[node_id]["inputs"][node.key] = node.value
|
||||||
|
|
||||||
|
try:
|
||||||
|
ws = websocket.WebSocket()
|
||||||
|
headers = {"Authorization": f"Bearer {api_key}"}
|
||||||
|
ws.connect(f"{ws_url}/ws?clientId={client_id}", header=headers)
|
||||||
|
log.info("WebSocket connection established.")
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Failed to connect to WebSocket server: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
log.info("Sending workflow to WebSocket server.")
|
||||||
|
log.info(f"Workflow: {workflow}")
|
||||||
|
images = await asyncio.to_thread(
|
||||||
|
get_images, ws, workflow, client_id, base_url, api_key
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Error while receiving images: {e}")
|
||||||
|
images = None
|
||||||
|
|
||||||
|
ws.close()
|
||||||
|
|
||||||
|
return images
|
||||||
|
|
|
||||||
|
|
@ -63,14 +63,19 @@
|
||||||
];
|
];
|
||||||
|
|
||||||
let REQUIRED_EDIT_WORKFLOW_NODES = [
|
let REQUIRED_EDIT_WORKFLOW_NODES = [
|
||||||
|
{
|
||||||
|
type: 'image',
|
||||||
|
key: 'image',
|
||||||
|
node_ids: ''
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'prompt',
|
type: 'prompt',
|
||||||
key: 'text',
|
key: 'prompt',
|
||||||
node_ids: ''
|
node_ids: ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'model',
|
type: 'model',
|
||||||
key: 'ckpt_name',
|
key: 'unet_name',
|
||||||
node_ids: ''
|
node_ids: ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -157,10 +162,25 @@
|
||||||
loading = false;
|
loading = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.COMFYUI_WORKFLOW_NODES = REQUIRED_WORKFLOW_NODES.map((node) => {
|
||||||
|
return {
|
||||||
|
type: node.type,
|
||||||
|
key: node.key,
|
||||||
|
node_ids:
|
||||||
|
node.node_ids.trim() === '' ? [] : node.node_ids.split(',').map((id) => id.trim())
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config?.COMFYUI_WORKFLOW) {
|
if (config?.IMAGES_EDIT_COMFYUI_WORKFLOW) {
|
||||||
config.COMFYUI_WORKFLOW_NODES = REQUIRED_WORKFLOW_NODES.map((node) => {
|
if (!validateJSON(config?.IMAGES_EDIT_COMFYUI_WORKFLOW)) {
|
||||||
|
toast.error($i18n.t('Invalid JSON format for ComfyUI Edit Workflow.'));
|
||||||
|
loading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES = REQUIRED_EDIT_WORKFLOW_NODES.map((node) => {
|
||||||
return {
|
return {
|
||||||
type: node.type,
|
type: node.type,
|
||||||
key: node.key,
|
key: node.key,
|
||||||
|
|
@ -211,6 +231,30 @@
|
||||||
node_ids: typeof n.node_ids === 'string' ? n.node_ids : n.node_ids.join(',')
|
node_ids: typeof n.node_ids === 'string' ? n.node_ids : n.node_ids.join(',')
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (config.IMAGES_EDIT_COMFYUI_WORKFLOW) {
|
||||||
|
try {
|
||||||
|
config.IMAGES_EDIT_COMFYUI_WORKFLOW = JSON.stringify(
|
||||||
|
JSON.parse(config.IMAGES_EDIT_COMFYUI_WORKFLOW),
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
REQUIRED_EDIT_WORKFLOW_NODES = REQUIRED_EDIT_WORKFLOW_NODES.map((node) => {
|
||||||
|
const n =
|
||||||
|
config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES.find((n) => n.type === node.type) ?? node;
|
||||||
|
console.debug(n);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: n.type,
|
||||||
|
key: n.key,
|
||||||
|
node_ids: typeof n.node_ids === 'string' ? n.node_ids : n.node_ids.join(',')
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -814,7 +858,7 @@
|
||||||
placeholder={$i18n.t('Select Engine')}
|
placeholder={$i18n.t('Select Engine')}
|
||||||
>
|
>
|
||||||
<option value="openai">{$i18n.t('Default (Open AI)')}</option>
|
<option value="openai">{$i18n.t('Default (Open AI)')}</option>
|
||||||
<!-- <option value="comfyui">{$i18n.t('ComfyUI')}</option> -->
|
<option value="comfyui">{$i18n.t('ComfyUI')}</option>
|
||||||
<option value="gemini">{$i18n.t('Gemini')}</option>
|
<option value="gemini">{$i18n.t('Gemini')}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -835,7 +879,6 @@
|
||||||
class="text-right text-sm bg-transparent outline-hidden max-w-full w-52"
|
class="text-right text-sm bg-transparent outline-hidden max-w-full w-52"
|
||||||
bind:value={config.IMAGE_EDIT_MODEL}
|
bind:value={config.IMAGE_EDIT_MODEL}
|
||||||
placeholder={$i18n.t('Select a model')}
|
placeholder={$i18n.t('Select a model')}
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<datalist id="model-list">
|
<datalist id="model-list">
|
||||||
|
|
@ -1086,7 +1129,7 @@
|
||||||
<div class="flex w-full flex-col">
|
<div class="flex w-full flex-col">
|
||||||
<div class="shrink-0">
|
<div class="shrink-0">
|
||||||
<div class=" capitalize line-clamp-1 w-20 text-gray-400 dark:text-gray-500">
|
<div class=" capitalize line-clamp-1 w-20 text-gray-400 dark:text-gray-500">
|
||||||
{node.type}{node.type === 'prompt' ? '*' : ''}
|
{node.type}{['prompt', 'image'].includes(node.type) ? '*' : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue