diff --git a/docker-compose.yml b/docker-compose.yml index 05c189f8..9698ed29 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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} diff --git a/servers/fastapi/enums/image_provider.py b/servers/fastapi/enums/image_provider.py index 9d773ad5..76312b73 100644 --- a/servers/fastapi/enums/image_provider.py +++ b/servers/fastapi/enums/image_provider.py @@ -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" diff --git a/servers/fastapi/models/user_config.py b/servers/fastapi/models/user_config.py index c26a6cb0..8a1b4249 100644 --- a/servers/fastapi/models/user_config.py +++ b/servers/fastapi/models/user_config.py @@ -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 diff --git a/servers/fastapi/services/image_generation_service.py b/servers/fastapi/services/image_generation_service.py index f9ec1202..93255e36 100644 --- a/servers/fastapi/services/image_generation_service.py +++ b/servers/fastapi/services/image_generation_service.py @@ -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: diff --git a/servers/fastapi/utils/get_env.py b/servers/fastapi/utils/get_env.py index 74cf2e1f..be8d31b4 100644 --- a/servers/fastapi/utils/get_env.py +++ b/servers/fastapi/utils/get_env.py @@ -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") diff --git a/servers/fastapi/utils/image_provider.py b/servers/fastapi/utils/image_provider.py index 15469709..cb3525e0 100644 --- a/servers/fastapi/utils/image_provider.py +++ b/servers/fastapi/utils/image_provider.py @@ -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. diff --git a/servers/fastapi/utils/set_env.py b/servers/fastapi/utils/set_env.py index 6f26b34f..52c1278d 100644 --- a/servers/fastapi/utils/set_env.py +++ b/servers/fastapi/utils/set_env.py @@ -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 diff --git a/servers/fastapi/utils/user_config.py b/servers/fastapi/utils/user_config.py index f83d3047..a050163d 100644 --- a/servers/fastapi/utils/user_config.py +++ b/servers/fastapi/utils/user_config.py @@ -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: diff --git a/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/ImageProvider.tsx b/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/ImageProvider.tsx index 31bc11ea..59e8a21c 100644 --- a/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/ImageProvider.tsx +++ b/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/ImageProvider.tsx @@ -262,6 +262,33 @@ const ImageProvider = ({ llmConfig, setLlmConfig }: { llmConfig: LLMConfig, setL ); } + // Show Open WebUI configuration + if (provider.value === "open_webui") { + return ( +