From 8b77f92028947752925538bc79fd79a259a32806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Wed, 12 Nov 2025 09:36:52 +0100 Subject: [PATCH] feat: integrate Facefusion into AI stack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added Facefusion face swapping service to the AI stack: **Configuration:** - URL: https://facefusion.ai.pivoine.art - Image: facefusion/facefusion:3.5.0-cpu - Port: 7865 - Container: ai_facefusion - Volume: ai_facefusion_data - HTTP Basic Auth protection - CPU execution mode (GPU when available) **Changes:** - Added facefusion service to ai/compose.yaml - Added AI_FACEFUSION_* env vars to arty.yml - Created ai_facefusion_data volume - Removed old standalone facefusion stack - Removed ai/README-export.md and ai/webui-export.py Facefusion will run on CPU until GPU server is available. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- ai/README-export.md | 124 ---------------------- ai/compose.yaml | 36 +++++++ ai/facefusion.yaml | 37 +++++++ ai/webui-export.py | 246 -------------------------------------------- arty.yml | 4 + 5 files changed, 77 insertions(+), 370 deletions(-) delete mode 100644 ai/README-export.md create mode 100644 ai/facefusion.yaml delete mode 100755 ai/webui-export.py diff --git a/ai/README-export.md b/ai/README-export.md deleted file mode 100644 index 4b27c35..0000000 --- a/ai/README-export.md +++ /dev/null @@ -1,124 +0,0 @@ -# Open WebUI Code Export - -Simple Python script to export code blocks from Open WebUI chat conversations to your local disk using the REST API. - -## Features - -- Export code blocks from specific chats or all chats -- Automatically detects language and saves with proper file extension -- Organizes files by chat title -- Saves metadata (timestamp, role, language) alongside code -- No Docker modifications needed - uses REST API - -## Requirements - -```bash -pip install requests -``` - -## Usage - -### Export specific chat - -First, get your chat ID from the URL when viewing a chat in Open WebUI: -- URL format: `https://ai.pivoine.art/c/` -- Example: `https://ai.pivoine.art/c/abc123def456` - -```bash -# Export single chat -python ai/webui-export.py --chat-id abc123def456 --output-dir ./my-code - -# Export from remote server -python ai/webui-export.py --base-url https://ai.pivoine.art --chat-id abc123def456 --output-dir ./my-code -``` - -### Export all chats - -```bash -# Export all chats -python ai/webui-export.py --all --output-dir ./all-code - -# Export from remote server -python ai/webui-export.py --base-url https://ai.pivoine.art --all --output-dir ./all-code -``` - -### With authentication (if needed) - -```bash -# If API key authentication is required -python ai/webui-export.py --api-key your-api-key --all --output-dir ./my-code -``` - -## Output Structure - -``` -webui-exports/ -├── My Python Project/ -│ ├── 000-00.py # First code block from first message -│ ├── 000-00.py.meta.json # Metadata -│ ├── 001-00.py # First code block from second message -│ └── 001-00.py.meta.json -└── Database Schema Design/ - ├── 000-00.sql - └── 000-00.sql.meta.json -``` - -## Remote Usage (VPS) - -To export code from Open WebUI running on your VPS: - -```bash -# SSH tunnel to access the API -ssh -L 8080:localhost:8080 root@vps - -# In another terminal, export chats -python ai/webui-export.py --all --output-dir ~/Projects/webui-exports -``` - -Or directly via the public URL: - -```bash -python ai/webui-export.py --base-url https://ai.pivoine.art --all --output-dir ~/Projects/webui-exports -``` - -## Supported Languages - -The script automatically detects and saves with proper extensions: -- Python (.py) -- JavaScript/TypeScript (.js/.ts) -- Shell scripts (.sh) -- SQL (.sql) -- HTML/CSS (.html/.css) -- JSON/YAML (.json/.yaml) -- And many more... - -## Tips - -1. **Find chat IDs**: Look at the URL bar when viewing a conversation -2. **Regular exports**: Run `--all` periodically to backup your code -3. **Integration**: Add to cron job for automatic backups -4. **Local testing**: Run locally first with `--base-url http://localhost:8080` - -## Example Workflow - -```bash -# 1. Have a conversation with Claude in Open WebUI where code is generated -# 2. Note the chat ID from URL (or use --all) -# 3. Export the code -python ai/webui-export.py --chat-id abc123 --output-dir ~/Projects/new-feature - -# 4. The code is now in ~/Projects/new-feature/Chat Title/000-00.py -# 5. Review, modify, and use as needed -``` - -## Automation - -Add to crontab for daily exports: - -```bash -# Edit crontab -crontab -e - -# Add daily export at 2 AM -0 2 * * * cd ~/Projects/docker-compose && python ai/webui-export.py --base-url https://ai.pivoine.art --all --output-dir ~/Projects/webui-exports -``` diff --git a/ai/compose.yaml b/ai/compose.yaml index 3b4b6b1..5869a18 100644 --- a/ai/compose.yaml +++ b/ai/compose.yaml @@ -146,6 +146,40 @@ services: # Watchtower - 'com.centurylinklabs.watchtower.enable=${WATCHTOWER_LABEL_ENABLE}' + # Facefusion - AI face swapping and enhancement + facefusion: + image: ${AI_FACEFUSION_IMAGE:-facefusion/facefusion:3.5.0-cpu} + container_name: ${AI_COMPOSE_PROJECT_NAME}_facefusion + restart: unless-stopped + environment: + TZ: ${TIMEZONE:-Europe/Berlin} + # Force CPU execution on VPS (no GPU available yet) + FACEFUSION_EXECUTION_PROVIDERS: ${AI_FACEFUSION_EXECUTION_PROVIDERS:-cpu} + volumes: + - ai_facefusion_data:/workspace + networks: + - compose_network + labels: + - 'traefik.enable=${AI_FACEFUSION_TRAEFIK_ENABLED}' + # HTTP Basic Auth middleware + - 'traefik.http.middlewares.${AI_COMPOSE_PROJECT_NAME}-facefusion-auth.basicauth.users=${AUTH_USERS}' + # HTTP to HTTPS redirect + - 'traefik.http.middlewares.${AI_COMPOSE_PROJECT_NAME}-facefusion-redirect-web-secure.redirectscheme.scheme=https' + - 'traefik.http.routers.${AI_COMPOSE_PROJECT_NAME}-facefusion-web.middlewares=${AI_COMPOSE_PROJECT_NAME}-facefusion-redirect-web-secure' + - 'traefik.http.routers.${AI_COMPOSE_PROJECT_NAME}-facefusion-web.rule=Host(`${AI_FACEFUSION_TRAEFIK_HOST}`)' + - 'traefik.http.routers.${AI_COMPOSE_PROJECT_NAME}-facefusion-web.entrypoints=web' + # HTTPS router with auth + - 'traefik.http.routers.${AI_COMPOSE_PROJECT_NAME}-facefusion-web-secure.rule=Host(`${AI_FACEFUSION_TRAEFIK_HOST}`)' + - 'traefik.http.routers.${AI_COMPOSE_PROJECT_NAME}-facefusion-web-secure.tls.certresolver=resolver' + - 'traefik.http.routers.${AI_COMPOSE_PROJECT_NAME}-facefusion-web-secure.entrypoints=web-secure' + - 'traefik.http.middlewares.${AI_COMPOSE_PROJECT_NAME}-facefusion-web-secure-compress.compress=true' + - 'traefik.http.routers.${AI_COMPOSE_PROJECT_NAME}-facefusion-web-secure.middlewares=${AI_COMPOSE_PROJECT_NAME}-facefusion-auth,${AI_COMPOSE_PROJECT_NAME}-facefusion-web-secure-compress,security-headers@file' + # Service + - 'traefik.http.services.${AI_COMPOSE_PROJECT_NAME}-facefusion-web-secure.loadbalancer.server.port=7865' + - 'traefik.docker.network=${NETWORK_NAME}' + # Watchtower + - 'com.centurylinklabs.watchtower.enable=${WATCHTOWER_LABEL_ENABLE}' + volumes: ai_postgres_data: name: ${AI_COMPOSE_PROJECT_NAME}_postgres_data @@ -153,3 +187,5 @@ volumes: name: ${AI_COMPOSE_PROJECT_NAME}_webui_data ai_crawl4ai_data: name: ${AI_COMPOSE_PROJECT_NAME}_crawl4ai_data + ai_facefusion_data: + name: ${AI_COMPOSE_PROJECT_NAME}_facefusion_data diff --git a/ai/facefusion.yaml b/ai/facefusion.yaml new file mode 100644 index 0000000..a10f9ec --- /dev/null +++ b/ai/facefusion.yaml @@ -0,0 +1,37 @@ +services: + facefusion: + image: ${FACEFUSION_IMAGE:-facefusion/facefusion:3.5.0-cpu} + container_name: ${FACEFUSION_COMPOSE_PROJECT_NAME}_app + restart: unless-stopped + environment: + TZ: ${TIMEZONE:-Europe/Berlin} + # Force CPU execution on VPS (no GPU available) + FACEFUSION_EXECUTION_PROVIDERS: ${FACEFUSION_EXECUTION_PROVIDERS:-cpu} + volumes: + - facefusion_data:/workspace + networks: + - compose_network + labels: + - 'traefik.enable=${FACEFUSION_TRAEFIK_ENABLED}' + # HTTP Basic Auth middleware + - 'traefik.http.middlewares.${FACEFUSION_COMPOSE_PROJECT_NAME}-auth.basicauth.users=${AUTH_USERS}' + # HTTP to HTTPS redirect + - 'traefik.http.middlewares.${FACEFUSION_COMPOSE_PROJECT_NAME}-redirect-web-secure.redirectscheme.scheme=https' + - 'traefik.http.routers.${FACEFUSION_COMPOSE_PROJECT_NAME}-web.middlewares=${FACEFUSION_COMPOSE_PROJECT_NAME}-redirect-web-secure' + - 'traefik.http.routers.${FACEFUSION_COMPOSE_PROJECT_NAME}-web.rule=Host(`${FACEFUSION_TRAEFIK_HOST}`)' + - 'traefik.http.routers.${FACEFUSION_COMPOSE_PROJECT_NAME}-web.entrypoints=web' + # HTTPS router with auth + - 'traefik.http.routers.${FACEFUSION_COMPOSE_PROJECT_NAME}-web-secure.rule=Host(`${FACEFUSION_TRAEFIK_HOST}`)' + - 'traefik.http.routers.${FACEFUSION_COMPOSE_PROJECT_NAME}-web-secure.tls.certresolver=resolver' + - 'traefik.http.routers.${FACEFUSION_COMPOSE_PROJECT_NAME}-web-secure.entrypoints=web-secure' + - 'traefik.http.middlewares.${FACEFUSION_COMPOSE_PROJECT_NAME}-web-secure-compress.compress=true' + - 'traefik.http.routers.${FACEFUSION_COMPOSE_PROJECT_NAME}-web-secure.middlewares=${FACEFUSION_COMPOSE_PROJECT_NAME}-auth,${FACEFUSION_COMPOSE_PROJECT_NAME}-web-secure-compress,security-headers@file' + # Service + - 'traefik.http.services.${FACEFUSION_COMPOSE_PROJECT_NAME}-web-secure.loadbalancer.server.port=7860' + - 'traefik.docker.network=${NETWORK_NAME}' + # Watchtower + - 'com.centurylinklabs.watchtower.enable=${WATCHTOWER_LABEL_ENABLE}' + +volumes: + facefusion_data: + name: ${FACEFUSION_COMPOSE_PROJECT_NAME}_data diff --git a/ai/webui-export.py b/ai/webui-export.py deleted file mode 100755 index 299a606..0000000 --- a/ai/webui-export.py +++ /dev/null @@ -1,246 +0,0 @@ -#!/usr/bin/env python3 -""" -Export code blocks from Open WebUI chat conversations to local disk. - -Usage: - # Export specific chat - python webui-export.py --chat-id --output-dir ./output - - # Export all recent chats - python webui-export.py --all --output-dir ./output - - # Watch for new messages and auto-export - python webui-export.py --watch --output-dir ./output -""" - -import argparse -import json -import os -import re -import time -from datetime import datetime -from pathlib import Path -from typing import Dict, List, Optional - -import requests - - -class OpenWebUIExporter: - """Export code from Open WebUI chats via REST API.""" - - def __init__(self, base_url: str = "http://localhost:8080", api_key: Optional[str] = None): - """ - Initialize exporter. - - :param base_url: Open WebUI base URL (default: http://localhost:8080) - :param api_key: Optional API key for authentication - """ - self.base_url = base_url.rstrip("/") - self.api_url = f"{self.base_url}/api/v1" - self.session = requests.Session() - - if api_key: - self.session.headers["Authorization"] = f"Bearer {api_key}" - - def get_chats(self) -> List[Dict]: - """Retrieve all user chats.""" - response = self.session.get(f"{self.api_url}/chats/all") - response.raise_for_status() - return response.json() - - def get_chat(self, chat_id: str) -> Dict: - """Retrieve specific chat by ID.""" - response = self.session.get(f"{self.api_url}/chats/{chat_id}") - response.raise_for_status() - return response.json() - - def extract_code_blocks(self, chat: Dict) -> List[Dict]: - """ - Extract code blocks from chat messages. - - :param chat: Chat object with messages - :return: List of code blocks with metadata - """ - code_blocks = [] - - messages = chat.get("chat", {}).get("messages", []) - - for idx, message in enumerate(messages): - role = message.get("role", "unknown") - content = message.get("content", "") - - # Find code blocks in markdown (```language\ncode\n```) - pattern = r"```(\w*)\n(.*?)```" - matches = re.findall(pattern, content, re.DOTALL) - - for match_idx, (language, code) in enumerate(matches): - code_blocks.append({ - "chat_id": chat.get("id"), - "chat_title": chat.get("title", "Untitled"), - "message_index": idx, - "role": role, - "language": language or "txt", - "code": code.strip(), - "block_index": match_idx, - "timestamp": message.get("timestamp", time.time()), - }) - - return code_blocks - - def save_code_blocks(self, code_blocks: List[Dict], output_dir: Path): - """ - Save code blocks to disk. - - :param code_blocks: List of code blocks with metadata - :param output_dir: Output directory - """ - output_dir = Path(output_dir) - output_dir.mkdir(parents=True, exist_ok=True) - - for block in code_blocks: - # Create subdirectory per chat - chat_dir = output_dir / self._sanitize_filename(block["chat_title"]) - chat_dir.mkdir(exist_ok=True) - - # Determine file extension - ext = self._get_extension(block["language"]) - - # Create filename: message_index-block_index.ext - filename = f"{block['message_index']:03d}-{block['block_index']:02d}{ext}" - filepath = chat_dir / filename - - # Save code - with open(filepath, "w") as f: - f.write(block["code"]) - - # Save metadata - meta_filepath = filepath.with_suffix(filepath.suffix + ".meta.json") - with open(meta_filepath, "w") as f: - json.dump({ - "chat_id": block["chat_id"], - "chat_title": block["chat_title"], - "message_index": block["message_index"], - "role": block["role"], - "language": block["language"], - "timestamp": block["timestamp"], - "timestamp_human": datetime.fromtimestamp(block["timestamp"]).isoformat(), - }, f, indent=2) - - print(f"✓ Saved: {filepath}") - - def export_chat(self, chat_id: str, output_dir: Path): - """Export code blocks from specific chat.""" - print(f"Fetching chat {chat_id}...") - chat = self.get_chat(chat_id) - - code_blocks = self.extract_code_blocks(chat) - print(f"Found {len(code_blocks)} code blocks") - - if code_blocks: - self.save_code_blocks(code_blocks, output_dir) - print(f"✓ Exported {len(code_blocks)} code blocks to {output_dir}") - else: - print("No code blocks found in chat") - - def export_all_chats(self, output_dir: Path): - """Export code blocks from all chats.""" - print("Fetching all chats...") - chats = self.get_chats() - print(f"Found {len(chats)} chats") - - total_blocks = 0 - for chat in chats: - code_blocks = self.extract_code_blocks(chat) - if code_blocks: - self.save_code_blocks(code_blocks, output_dir) - total_blocks += len(code_blocks) - - print(f"✓ Exported {total_blocks} code blocks from {len(chats)} chats to {output_dir}") - - @staticmethod - def _sanitize_filename(name: str) -> str: - """Sanitize filename by removing invalid characters.""" - # Remove or replace invalid characters - name = re.sub(r'[<>:"/\\|?*]', '_', name) - # Limit length - return name[:100] - - @staticmethod - def _get_extension(language: str) -> str: - """Get file extension for language.""" - extensions = { - "python": ".py", - "javascript": ".js", - "typescript": ".ts", - "java": ".java", - "c": ".c", - "cpp": ".cpp", - "csharp": ".cs", - "go": ".go", - "rust": ".rs", - "ruby": ".rb", - "php": ".php", - "swift": ".swift", - "kotlin": ".kt", - "bash": ".sh", - "shell": ".sh", - "sql": ".sql", - "html": ".html", - "css": ".css", - "json": ".json", - "yaml": ".yaml", - "yml": ".yml", - "xml": ".xml", - "markdown": ".md", - "md": ".md", - } - return extensions.get(language.lower(), ".txt") - - -def main(): - parser = argparse.ArgumentParser( - description="Export code blocks from Open WebUI chats" - ) - parser.add_argument( - "--base-url", - default="http://localhost:8080", - help="Open WebUI base URL (default: http://localhost:8080)", - ) - parser.add_argument( - "--api-key", - help="Optional API key for authentication", - ) - parser.add_argument( - "--chat-id", - help="Export specific chat by ID", - ) - parser.add_argument( - "--all", - action="store_true", - help="Export all chats", - ) - parser.add_argument( - "--output-dir", - default="./webui-exports", - help="Output directory (default: ./webui-exports)", - ) - - args = parser.parse_args() - - exporter = OpenWebUIExporter(base_url=args.base_url, api_key=args.api_key) - output_dir = Path(args.output_dir) - - if args.chat_id: - exporter.export_chat(args.chat_id, output_dir) - elif args.all: - exporter.export_all_chats(output_dir) - else: - parser.print_help() - print("\nError: Specify either --chat-id or --all") - return 1 - - return 0 - - -if __name__ == "__main__": - exit(main()) diff --git a/arty.yml b/arty.yml index 1e090d9..ea507d6 100644 --- a/arty.yml +++ b/arty.yml @@ -170,6 +170,10 @@ envs: AI_POSTGRES_IMAGE: pgvector/pgvector:pg16 AI_WEBUI_IMAGE: ghcr.io/open-webui/open-webui:main AI_CRAWL4AI_IMAGE: unclecode/crawl4ai:latest + AI_FACEFUSION_IMAGE: facefusion/facefusion:3.5.0-cpu + AI_FACEFUSION_TRAEFIK_ENABLED: true + AI_FACEFUSION_TRAEFIK_HOST: facefusion.ai.pivoine.art + AI_FACEFUSION_EXECUTION_PROVIDERS: cpu AI_TRAEFIK_HOST: ai.pivoine.art AI_DB_USER: ai AI_DB_NAME: openwebui