Merge pull request #497 from hilotechinc/main
feat: add Open WebUI as image generation provider
This commit is contained in:
commit
8b63e407e1
15 changed files with 259 additions and 1 deletions
|
|
@ -36,6 +36,8 @@ services:
|
|||
- DISABLE_ANONYMOUS_TRACKING=${DISABLE_ANONYMOUS_TRACKING}
|
||||
- COMFYUI_URL=${COMFYUI_URL}
|
||||
- COMFYUI_WORKFLOW=${COMFYUI_WORKFLOW}
|
||||
- OPEN_WEBUI_IMAGE_URL=${OPEN_WEBUI_IMAGE_URL}
|
||||
- OPEN_WEBUI_IMAGE_API_KEY=${OPEN_WEBUI_IMAGE_API_KEY}
|
||||
|
||||
production-gpu:
|
||||
# image: ghcr.io/presenton/presenton:latest
|
||||
|
|
@ -81,7 +83,9 @@ services:
|
|||
- DISABLE_ANONYMOUS_TRACKING=${DISABLE_ANONYMOUS_TRACKING}
|
||||
- COMFYUI_URL=${COMFYUI_URL}
|
||||
- COMFYUI_WORKFLOW=${COMFYUI_WORKFLOW}
|
||||
|
||||
- OPEN_WEBUI_IMAGE_URL=${OPEN_WEBUI_IMAGE_URL}
|
||||
- OPEN_WEBUI_IMAGE_API_KEY=${OPEN_WEBUI_IMAGE_API_KEY}
|
||||
|
||||
development:
|
||||
build:
|
||||
context: .
|
||||
|
|
@ -118,6 +122,8 @@ services:
|
|||
- DISABLE_ANONYMOUS_TRACKING=${DISABLE_ANONYMOUS_TRACKING}
|
||||
- COMFYUI_URL=${COMFYUI_URL}
|
||||
- COMFYUI_WORKFLOW=${COMFYUI_WORKFLOW}
|
||||
- OPEN_WEBUI_IMAGE_URL=${OPEN_WEBUI_IMAGE_URL}
|
||||
- OPEN_WEBUI_IMAGE_API_KEY=${OPEN_WEBUI_IMAGE_API_KEY}
|
||||
|
||||
development-gpu:
|
||||
build:
|
||||
|
|
@ -162,3 +168,5 @@ services:
|
|||
- DISABLE_ANONYMOUS_TRACKING=${DISABLE_ANONYMOUS_TRACKING}
|
||||
- COMFYUI_URL=${COMFYUI_URL}
|
||||
- COMFYUI_WORKFLOW=${COMFYUI_WORKFLOW}
|
||||
- OPEN_WEBUI_IMAGE_URL=${OPEN_WEBUI_IMAGE_URL}
|
||||
- OPEN_WEBUI_IMAGE_API_KEY=${OPEN_WEBUI_IMAGE_API_KEY}
|
||||
|
|
|
|||
|
|
@ -9,3 +9,4 @@ class ImageProvider(Enum):
|
|||
DALLE3 = "dall-e-3"
|
||||
GPT_IMAGE_1_5 = "gpt-image-1.5"
|
||||
COMFYUI = "comfyui"
|
||||
OPEN_WEBUI = "open_webui"
|
||||
|
|
|
|||
|
|
@ -36,6 +36,10 @@ class UserConfig(BaseModel):
|
|||
COMFYUI_URL: Optional[str] = None
|
||||
COMFYUI_WORKFLOW: Optional[str] = None
|
||||
|
||||
# Open WebUI Image Provider
|
||||
OPEN_WEBUI_IMAGE_URL: Optional[str] = None
|
||||
OPEN_WEBUI_IMAGE_API_KEY: Optional[str] = None
|
||||
|
||||
# Dalle 3 Quality
|
||||
DALL_E_3_QUALITY: Optional[str] = None
|
||||
# Gpt Image 1.5 Quality
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ from utils.get_env import (
|
|||
get_dall_e_3_quality_env,
|
||||
get_gpt_image_1_5_quality_env,
|
||||
get_pexels_api_key_env,
|
||||
get_open_webui_image_url_env,
|
||||
get_open_webui_image_api_key_env,
|
||||
)
|
||||
from utils.get_env import get_pixabay_api_key_env
|
||||
from utils.get_env import get_comfyui_url_env
|
||||
|
|
@ -25,6 +27,7 @@ from utils.image_provider import (
|
|||
is_nanobanana_pro_selected,
|
||||
is_dalle3_selected,
|
||||
is_comfyui_selected,
|
||||
is_open_webui_selected,
|
||||
)
|
||||
import uuid
|
||||
|
||||
|
|
@ -53,6 +56,8 @@ class ImageGenerationService:
|
|||
return self.generate_image_openai_gpt_image_1_5
|
||||
elif is_comfyui_selected():
|
||||
return self.generate_image_comfyui
|
||||
elif is_open_webui_selected():
|
||||
return self.generate_image_open_webui
|
||||
return None
|
||||
|
||||
def is_stock_provider_selected(self):
|
||||
|
|
@ -141,6 +146,88 @@ class ImageGenerationService:
|
|||
get_gpt_image_1_5_quality_env() or "medium",
|
||||
)
|
||||
|
||||
async def generate_image_open_webui(
|
||||
self, prompt: str, output_directory: str
|
||||
) -> str:
|
||||
base_url = get_open_webui_image_url_env()
|
||||
if not base_url:
|
||||
raise ValueError("OPEN_WEBUI_IMAGE_URL environment variable is not set")
|
||||
|
||||
base_url = base_url.rstrip("/")
|
||||
api_key = get_open_webui_image_api_key_env() or ""
|
||||
|
||||
from urllib.parse import urlparse
|
||||
|
||||
parsed = urlparse(base_url)
|
||||
origin = f"{parsed.scheme}://{parsed.netloc}"
|
||||
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if api_key:
|
||||
headers["Authorization"] = f"Bearer {api_key}"
|
||||
|
||||
payload = {
|
||||
"prompt": prompt,
|
||||
"n": 1,
|
||||
"size": "1024x1024",
|
||||
}
|
||||
|
||||
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||
resp = await session.post(
|
||||
f"{base_url}/images/generations",
|
||||
json=payload,
|
||||
headers=headers,
|
||||
timeout=aiohttp.ClientTimeout(total=300),
|
||||
)
|
||||
|
||||
if resp.status != 200:
|
||||
error_text = await resp.text()
|
||||
raise Exception(
|
||||
f"Open WebUI image generation returned {resp.status}: {error_text}"
|
||||
)
|
||||
|
||||
body = await resp.json()
|
||||
|
||||
# Open WebUI returns a bare [...] array instead of {"data": [...]}.
|
||||
if isinstance(body, list):
|
||||
items = body
|
||||
elif isinstance(body, dict) and "data" in body:
|
||||
items = body["data"]
|
||||
else:
|
||||
raise Exception(f"Unexpected response format: {type(body)}")
|
||||
|
||||
if not items:
|
||||
raise Exception("Open WebUI returned empty results")
|
||||
|
||||
item = items[0]
|
||||
image_path = os.path.join(output_directory, f"{uuid.uuid4()}.png")
|
||||
|
||||
if item.get("b64_json"):
|
||||
with open(image_path, "wb") as f:
|
||||
f.write(base64.b64decode(item["b64_json"]))
|
||||
elif item.get("url"):
|
||||
image_url = item["url"]
|
||||
# Open WebUI returns relative URLs like /api/v1/files/.../content
|
||||
if image_url.startswith("/"):
|
||||
image_url = origin + image_url
|
||||
dl_headers = {}
|
||||
if api_key:
|
||||
dl_headers["Authorization"] = f"Bearer {api_key}"
|
||||
dl_resp = await session.get(
|
||||
image_url,
|
||||
headers=dl_headers,
|
||||
timeout=aiohttp.ClientTimeout(total=120),
|
||||
)
|
||||
if dl_resp.status != 200:
|
||||
raise Exception(
|
||||
f"Failed to download image: {dl_resp.status}"
|
||||
)
|
||||
with open(image_path, "wb") as f:
|
||||
f.write(await dl_resp.read())
|
||||
else:
|
||||
raise Exception("Open WebUI returned no image data")
|
||||
|
||||
return image_path
|
||||
|
||||
async def _generate_image_google(
|
||||
self, prompt: str, output_directory: str, model: str
|
||||
) -> str:
|
||||
|
|
|
|||
|
|
@ -142,3 +142,12 @@ def get_codex_model_env():
|
|||
|
||||
def get_migrate_database_on_startup_env():
|
||||
return os.getenv("MIGRATE_DATABASE_ON_STARTUP")
|
||||
|
||||
|
||||
# Open WebUI Image Provider
|
||||
def get_open_webui_image_url_env():
|
||||
return os.getenv("OPEN_WEBUI_IMAGE_URL")
|
||||
|
||||
|
||||
def get_open_webui_image_api_key_env():
|
||||
return os.getenv("OPEN_WEBUI_IMAGE_API_KEY")
|
||||
|
|
|
|||
|
|
@ -38,6 +38,10 @@ def is_comfyui_selected() -> bool:
|
|||
return ImageProvider.COMFYUI == get_selected_image_provider()
|
||||
|
||||
|
||||
def is_open_webui_selected() -> bool:
|
||||
return ImageProvider.OPEN_WEBUI == get_selected_image_provider()
|
||||
|
||||
|
||||
def get_selected_image_provider() -> ImageProvider | None:
|
||||
"""
|
||||
Get the selected image provider from environment variables.
|
||||
|
|
|
|||
|
|
@ -124,3 +124,12 @@ def set_codex_account_id_env(value: str):
|
|||
|
||||
def set_codex_model_env(value: str):
|
||||
os.environ["CODEX_MODEL"] = value
|
||||
|
||||
|
||||
# Open WebUI Image Provider
|
||||
def set_open_webui_image_url_env(value: str):
|
||||
os.environ["OPEN_WEBUI_IMAGE_URL"] = value
|
||||
|
||||
|
||||
def set_open_webui_image_api_key_env(value: str):
|
||||
os.environ["OPEN_WEBUI_IMAGE_API_KEY"] = value
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ from utils.get_env import (
|
|||
get_codex_token_expires_env,
|
||||
get_codex_account_id_env,
|
||||
get_codex_model_env,
|
||||
get_open_webui_image_url_env,
|
||||
get_open_webui_image_api_key_env,
|
||||
)
|
||||
from utils.parsers import parse_bool_or_none
|
||||
from utils.set_env import (
|
||||
|
|
@ -65,6 +67,8 @@ from utils.set_env import (
|
|||
set_codex_token_expires_env,
|
||||
set_codex_account_id_env,
|
||||
set_codex_model_env,
|
||||
set_open_webui_image_url_env,
|
||||
set_open_webui_image_api_key_env,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -133,6 +137,8 @@ def get_user_config():
|
|||
CODEX_REFRESH_TOKEN=existing_config.CODEX_REFRESH_TOKEN or get_codex_refresh_token_env(),
|
||||
CODEX_TOKEN_EXPIRES=existing_config.CODEX_TOKEN_EXPIRES or get_codex_token_expires_env(),
|
||||
CODEX_ACCOUNT_ID=existing_config.CODEX_ACCOUNT_ID or get_codex_account_id_env(),
|
||||
OPEN_WEBUI_IMAGE_URL=existing_config.OPEN_WEBUI_IMAGE_URL or get_open_webui_image_url_env(),
|
||||
OPEN_WEBUI_IMAGE_API_KEY=existing_config.OPEN_WEBUI_IMAGE_API_KEY or get_open_webui_image_api_key_env(),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -196,6 +202,10 @@ def update_env_with_user_config():
|
|||
set_codex_token_expires_env(user_config.CODEX_TOKEN_EXPIRES)
|
||||
if user_config.CODEX_ACCOUNT_ID:
|
||||
set_codex_account_id_env(user_config.CODEX_ACCOUNT_ID)
|
||||
if user_config.OPEN_WEBUI_IMAGE_URL:
|
||||
set_open_webui_image_url_env(user_config.OPEN_WEBUI_IMAGE_URL)
|
||||
if user_config.OPEN_WEBUI_IMAGE_API_KEY:
|
||||
set_open_webui_image_api_key_env(user_config.OPEN_WEBUI_IMAGE_API_KEY)
|
||||
|
||||
|
||||
def save_codex_tokens_to_user_config() -> None:
|
||||
|
|
|
|||
|
|
@ -262,6 +262,33 @@ const ImageProvider = ({ llmConfig, setLlmConfig }: { llmConfig: LLMConfig, setL
|
|||
);
|
||||
}
|
||||
|
||||
// Show Open WebUI configuration
|
||||
if (provider.value === "open_webui") {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className='w-[205px]'>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Open WebUI URL
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="http://localhost:3000/api/v1"
|
||||
className="w-full px-4 py-2.5 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
|
||||
value={llmConfig.OPEN_WEBUI_IMAGE_URL || ""}
|
||||
onChange={(e) => {
|
||||
input_field_changed(
|
||||
e.target.value,
|
||||
"OPEN_WEBUI_IMAGE_URL"
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show API key input for other providers
|
||||
return (
|
||||
<div className=" w-[205px]">
|
||||
|
|
@ -300,6 +327,31 @@ const ImageProvider = ({ llmConfig, setLlmConfig }: { llmConfig: LLMConfig, setL
|
|||
{!isImageGenerationDisabled && <div className='flex justify-end items-center mt-[18px]'>
|
||||
|
||||
{renderQualitySelector(llmConfig, input_field_changed)}
|
||||
{llmConfig.IMAGE_PROVIDER === "open_webui" && (
|
||||
<div className='w-[205px]'>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
API Key (optional)
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showApiKey ? 'text' : 'password'}
|
||||
placeholder="API key"
|
||||
className="w-full px-4 py-2.5 h-12 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
|
||||
value={llmConfig.OPEN_WEBUI_IMAGE_API_KEY || ""}
|
||||
onChange={(e) => {
|
||||
input_field_changed(e.target.value, "OPEN_WEBUI_IMAGE_API_KEY");
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowApiKey((prev) => !prev)}
|
||||
className='absolute right-2 top-1/2 -translate-y-1/2 bg-white px-2 py-1 cursor-pointer'
|
||||
>
|
||||
{showApiKey ? <Eye className='w-4 h-4 text-gray-500' /> : <EyeOff className='w-4 h-4 text-gray-500' />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{llmConfig.IMAGE_PROVIDER === "comfyui" && <div className='w-full'>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Workflow JSON
|
||||
|
|
|
|||
|
|
@ -91,6 +91,10 @@ export async function POST(request: Request) {
|
|||
userConfig.USE_CUSTOM_URL === undefined
|
||||
? existingConfig.USE_CUSTOM_URL
|
||||
: userConfig.USE_CUSTOM_URL,
|
||||
OPEN_WEBUI_IMAGE_URL:
|
||||
userConfig.OPEN_WEBUI_IMAGE_URL || existingConfig.OPEN_WEBUI_IMAGE_URL,
|
||||
OPEN_WEBUI_IMAGE_API_KEY:
|
||||
userConfig.OPEN_WEBUI_IMAGE_API_KEY || existingConfig.OPEN_WEBUI_IMAGE_API_KEY,
|
||||
CODEX_MODEL: userConfig.CODEX_MODEL || existingConfig.CODEX_MODEL,
|
||||
CODEX_ACCESS_TOKEN: existingConfig.CODEX_ACCESS_TOKEN,
|
||||
CODEX_REFRESH_TOKEN: existingConfig.CODEX_REFRESH_TOKEN,
|
||||
|
|
|
|||
|
|
@ -266,6 +266,56 @@ const ImageSelectionConfig = ({ isImageGenerationDisabled, openImageProviderSele
|
|||
return <></>;
|
||||
}
|
||||
|
||||
// Show Open WebUI configuration
|
||||
if (provider.value === "open_webui") {
|
||||
return (
|
||||
<div className="space-y-4 w-[295px]">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Open WebUI URL
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="http://localhost:3000/api/v1"
|
||||
className="w-full px-4 py-2.5 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
|
||||
value={llmConfig.OPEN_WEBUI_IMAGE_URL || ""}
|
||||
onChange={(e) => {
|
||||
input_field_changed(
|
||||
e.target.value,
|
||||
"open_webui_image_url"
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-gray-500 flex items-center gap-2">
|
||||
<span className="block w-1 h-1 rounded-full bg-gray-400"></span>
|
||||
Image model is configured in Open WebUI admin settings
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
API Key (optional)
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Open WebUI API key"
|
||||
className="w-full px-4 py-2.5 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
|
||||
value={llmConfig.OPEN_WEBUI_IMAGE_API_KEY || ""}
|
||||
onChange={(e) => {
|
||||
input_field_changed(
|
||||
e.target.value,
|
||||
"open_webui_image_api_key"
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show ComfyUI configuration
|
||||
if (provider.value === "comfyui") {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -32,6 +32,10 @@ export interface LLMConfig {
|
|||
COMFYUI_URL?: string;
|
||||
COMFYUI_WORKFLOW?: string;
|
||||
|
||||
// Open WebUI Image Provider
|
||||
OPEN_WEBUI_IMAGE_URL?: string;
|
||||
OPEN_WEBUI_IMAGE_API_KEY?: string;
|
||||
|
||||
// Dalle 3 Quality
|
||||
DALL_E_3_QUALITY?: string;
|
||||
// GPT Image 1.5 Quality
|
||||
|
|
|
|||
|
|
@ -90,6 +90,15 @@ export const IMAGE_PROVIDERS: Record<string, ImageProviderOption> = {
|
|||
apiKeyField: "COMFYUI_URL",
|
||||
apiKeyFieldLabel: "ComfyUI Server URL",
|
||||
},
|
||||
open_webui: {
|
||||
value: "open_webui",
|
||||
label: "Open WebUI",
|
||||
description: "Use your Open WebUI server for image generation",
|
||||
icon: "/icons/open-webui.png",
|
||||
requiresApiKey: false,
|
||||
apiKeyField: "OPEN_WEBUI_IMAGE_URL",
|
||||
apiKeyFieldLabel: "Open WebUI URL",
|
||||
},
|
||||
};
|
||||
|
||||
export const LLM_PROVIDERS: Record<string, LLMProviderOption> = {
|
||||
|
|
|
|||
|
|
@ -52,6 +52,8 @@ export const updateLLMConfig = (
|
|||
comfyui_workflow: "COMFYUI_WORKFLOW",
|
||||
dall_e_3_quality: "DALL_E_3_QUALITY",
|
||||
gpt_image_1_5_quality: "GPT_IMAGE_1_5_QUALITY",
|
||||
open_webui_image_url: "OPEN_WEBUI_IMAGE_URL",
|
||||
open_webui_image_api_key: "OPEN_WEBUI_IMAGE_API_KEY",
|
||||
codex_model: "CODEX_MODEL",
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -102,6 +102,11 @@ export const getLLMConfigValidationError = (
|
|||
return "ComfyUI server URL is required.";
|
||||
}
|
||||
break;
|
||||
case "open_webui":
|
||||
if (!isProvided(llmConfig.OPEN_WEBUI_IMAGE_URL)) {
|
||||
return "Open WebUI URL is required.";
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return "Select a valid image provider.";
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue