From a5b7f0eff7a2a323767ceaf11ed48375d03f3ca9 Mon Sep 17 00:00:00 2001 From: e-maks Date: Sat, 7 Mar 2026 19:03:03 +0300 Subject: [PATCH] Create comfy ui loader --- .gitignore | 4 + README.md | 208 ++++++++++++++++++++++++ client.py | 132 ++++++++++++++++ dasiwa-api.service | 18 +++ generate_keys.py | 47 ++++++ hmac_auth.py | 109 +++++++++++++ nginx.conf | 2 + requirements.txt | 5 + server.py | 386 +++++++++++++++++++++++++++++++++++++++++++++ setup.sh | 181 +++++++++++++++++++++ 10 files changed, 1092 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 client.py create mode 100644 dasiwa-api.service create mode 100644 generate_keys.py create mode 100644 hmac_auth.py create mode 100644 nginx.conf create mode 100644 requirements.txt create mode 100644 server.py create mode 100644 setup.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..46e9af3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +keys.json +workflow_api.json +__pycache__/ +*.pyc diff --git a/README.md b/README.md new file mode 100644 index 0000000..53a872c --- /dev/null +++ b/README.md @@ -0,0 +1,208 @@ +# 🎬 DaSiWa API Server для ComfyUI + +Автономный API сервер для генерации видео через ComfyUI на выделенном GPU сервере (Intelion Cloud и т.д.). + +**Безопасность:** HMAC подпись каждого запроса (timestamp + nonce + body). Перехват бесполезен — подпись уникальна. + +--- + +## 📁 Структура + +``` +custom_comfyui/ +├── server.py # API сервер (ставится на GPU машину) +├── client.py # Клиент (запускается на твоём ПК) +├── hmac_auth.py # HMAC авторизация (нужен и там, и там) +├── generate_keys.py # Генерация ключей (один раз) +├── setup.sh # Автоустановка: Python, зависимости, UFW, systemd +├── dasiwa-api.service # Systemd сервис (автозапуск) +├── requirements.txt # Python зависимости +├── keys.json # 🔒 Ключи (НЕ коммитить!) +└── workflow_api.json # 🎨 ComfyUI workflow (сделай сам) +``` + +--- + +## 🚀 Быстрый старт + +### 1. Подготовка workflow + +В ComfyUI на сервере: +1. Загрузи `DaSiWa WAN 2.2 i2v FastFidelity C-AiO-59.json` через UI +2. Настрой, проверь что работает +3. **Экспортируй API версию:** Menu → `Save (API Format)` → назови `workflow_api.json` +4. Положи файл в эту папку (`custom_comfyui/`) + +### 2. Генерация ключей (на любом ПК с Python) + +```bash +cd custom_comfyui +python generate_keys.py +``` + +Появится файл `keys.json`. **Нужен и на сервере, и на клиенте.** + +### 3. Загрузка на сервер + +```powershell +# С Windows на Ubuntu сервер +$SERVER = "root@" + +# Загрузить всю папку +scp -r custom_comfyui/ ${SERVER}:/root/ + +# ИЛИ по файлам +scp server.py hmac_auth.py generate_keys.py setup.sh nginx.conf requirements.txt keys.json workflow_api.json ${SERVER}:/root/custom_comfyui/ +``` + +### 4. Установка на сервере (одна команда) + +```bash +ssh root@ +cd /root/custom_comfyui +chmod +x setup.sh +sudo ./setup.sh +``` + +Скрипт автоматически: +- ✅ Установит Python, pip, nginx, wget +- ✅ Установит Python зависимости +- ✅ Настроит Nginx (порт 5000 → API) +- ✅ Настроит Firewall (открыт 22 + 5000, закрыт 8188) +- ✅ Создаст systemd сервис с автозапуском +- ✅ Сгенерирует ключи (если нет) + +### 5. Скопируй keys.json на свой ПК + +```powershell +scp root@:/root/custom_comfyui/keys.json . +``` + +Положи в папку `custom_comfyui/` на своём ПК. + +### 6. Проверка + +```bash +# На сервере: +curl http://localhost:8080/health + +# Ответ: +# {"comfyui": "ok", "status": "ok", "timestamp": 1234567890} +``` + +### 7. Генерация видео (с клиента) + +```bash +cd custom_comfyui + +# I2V — из одного изображения +python client.py \ + --server http://:8080 \ + --image photo.png \ + --prompt "woman dancing gracefully" \ + --output video.mp4 + +# FLF2V — из двух кадров +python client.py \ + --server http://:8080 \ + --image start.png \ + --last-image end.png \ + --prompt "smooth transition between poses" \ + --output transition.mp4 +``` + +--- + +## ⚙️ Параметры генерации + +| Параметр | По умолчанию | Описание | +|----------|-------------|----------| +| `--width` | 528 | Ширина (кратно 16) | +| `--height` | 768 | Высота (кратно 16) | +| `--length` | 81 | Кол-во кадров (~5 сек при 16fps) | +| `--steps` | 4 | Шаги (DaSiWa оптимизирован под 4) | +| `--cfg` | 1.0 | CFG scale (DaSiWa работает с 1.0) | +| `--seed` | -1 | Сид (-1 = рандом) | +| `--fps` | 16 | Кадров в секунду | + +--- + +## 🔐 Как работает безопасность + +``` +Клиент: +1. Берёт тело запроса (JSON) +2. Создаёт timestamp + nonce (случайная строка) +3. Подписывает: HMAC-SHA256(secret_key, timestamp.nonce.body) +4. Отправляет: body + заголовки (client_id, timestamp, nonce, signature) + +Сервер: +1. Проверяет client_id +2. Проверяет timestamp (не старше 5 минут) +3. Проверяет nonce (не использован ранее — защита от replay) +4. Вычисляет подпись и сравнивает +5. Если всё ОК — выполняет запрос +``` + +**Почему это безопасно:** +- Без secret_key нельзя создать валидную подпись +- Каждый запрос уникален (nonce) — replay-атака невозможна +- Timestamp — протухшие запросы отклоняются +- secret_key **никогда** не передаётся по сети + +--- + +## 🔧 Управление + +```bash +# Статус API +systemctl status dasiwa-api + +# Перезапуск +systemctl restart dasiwa-api + +# Логи (live) +journalctl -u dasiwa-api -f + +# Стоп +systemctl stop dasiwa-api +``` + +--- + +## 🔥 Firewall + +Открытые порты: +- **22** — SSH +- **8080** — API (Python напрямую) + +Закрытые: +- **8188** — ComfyUI UI (только localhost) + +--- + +## ❓ Troubleshooting + +**ComfyUI недоступен:** +```bash +# Проверь что ComfyUI запущен +curl http://localhost:8188 +# Посмотри процесс +ps aux | grep comfy +``` + +**API не отвечает:** +```bash +journalctl -u dasiwa-api -n 50 +systemctl restart dasiwa-api +``` + +**Ошибка авторизации (401):** +- Проверь что `keys.json` одинаковый на клиенте и сервере +- Проверь время на обоих машинах (`date` на сервере, часы на ПК) + +**Nginx ошибка:** +```bash +nginx -t +systemctl restart nginx +``` diff --git a/client.py b/client.py new file mode 100644 index 0000000..a378611 --- /dev/null +++ b/client.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +""" +Клиент для DaSiWa API Server. +Запускается на ТВОЁМ ПК. Отправляет подписанные запросы на сервер. + +Использование: + python client.py --server http://:5000 --image photo.png --prompt "woman dancing" + python client.py --server http://:5000 --image start.png --last-image end.png --prompt "smooth transition" +""" + +import argparse +import base64 +import json +import os +import sys +import time + +import requests + +from hmac_auth import sign_request + +# ============================================================================ +# Конфигурация +# ============================================================================ + +KEYS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "keys.json") + + +def load_keys(): + if not os.path.exists(KEYS_FILE): + print(f"❌ Файл ключей не найден: {KEYS_FILE}") + print(" Запусти: python generate_keys.py") + sys.exit(1) + with open(KEYS_FILE, "r") as f: + return json.load(f) + + +def image_to_base64(path: str) -> str: + with open(path, "rb") as f: + return base64.b64encode(f.read()).decode() + + +def send_request(server_url: str, payload: dict, client_id: str, secret_key: str) -> dict: + """Отправляет подписанный запрос на сервер.""" + body = json.dumps(payload).encode("utf-8") + auth_headers = sign_request(body, secret_key, client_id) + + headers = { + "Content-Type": "application/json", + **auth_headers + } + + response = requests.post( + f"{server_url}/generate", + data=body, + headers=headers, + timeout=600 + ) + + return response.status_code, response.json() + + +def main(): + parser = argparse.ArgumentParser(description="DaSiWa API Client") + parser.add_argument("--server", required=True, help="Server URL, e.g. http://1.2.3.4:5000") + parser.add_argument("--image", required=True, help="Path to first frame image") + parser.add_argument("--last-image", default=None, help="Path to last frame image (FLF2V mode)") + parser.add_argument("--prompt", required=True, help="Text prompt") + parser.add_argument("--negative-prompt", default=None, help="Negative prompt") + parser.add_argument("--width", type=int, default=528) + parser.add_argument("--height", type=int, default=768) + parser.add_argument("--length", type=int, default=81, help="Frame count") + parser.add_argument("--steps", type=int, default=4) + parser.add_argument("--cfg", type=float, default=1.0) + parser.add_argument("--seed", type=int, default=-1) + parser.add_argument("--fps", type=int, default=16) + parser.add_argument("--output", "-o", default="output.mp4", help="Output video path") + args = parser.parse_args() + + keys = load_keys() + + # Формируем payload + payload = { + "prompt": args.prompt, + "image_base64": image_to_base64(args.image), + "width": args.width, + "height": args.height, + "length": args.length, + "steps": args.steps, + "cfg": args.cfg, + "seed": args.seed, + "fps": args.fps, + } + + if args.negative_prompt: + payload["negative_prompt"] = args.negative_prompt + + if args.last_image: + payload["last_image_base64"] = image_to_base64(args.last_image) + print(f"🎬 Режим: FLF2V (first + last frame)") + else: + print(f"🎬 Режим: I2V (image to video)") + + print(f"📐 {args.width}x{args.height}, {args.length} frames, {args.steps} steps") + print(f"📤 Отправляю запрос на {args.server}...") + + start = time.time() + status_code, result = send_request( + args.server, payload, keys["client_id"], keys["secret_key"] + ) + elapsed = time.time() - start + + if status_code != 200: + print(f"❌ Ошибка {status_code}: {result.get('error', 'Unknown')}") + if "detail" in result: + print(f" Детали: {result['detail']}") + sys.exit(1) + + if "video" in result: + video_bytes = base64.b64decode(result["video"]) + with open(args.output, "wb") as f: + f.write(video_bytes) + print(f"✅ Видео сохранено: {args.output} ({len(video_bytes) / 1024 / 1024:.1f} MB)") + print(f"⏱ Время: {elapsed:.1f}s (сервер: {result.get('elapsed', '?')}s)") + print(f"🌱 Seed: {result.get('seed', '?')}") + else: + print(f"❌ Ошибка: {result.get('error', 'No video in response')}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/dasiwa-api.service b/dasiwa-api.service new file mode 100644 index 0000000..8d1df31 --- /dev/null +++ b/dasiwa-api.service @@ -0,0 +1,18 @@ +[Unit] +Description=DaSiWa API Server +After=network.target + +[Service] +Type=simple +User=root +WorkingDirectory=/root/custom_comfyui +Environment=COMFY_HOST=127.0.0.1 +Environment=COMFY_PORT=8188 +Environment=API_PORT=8080 +Environment=COMFY_OUTPUT_DIR=/ComfyUI/output +ExecStart=/usr/bin/python3 /root/custom_comfyui/server.py +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target diff --git a/generate_keys.py b/generate_keys.py new file mode 100644 index 0000000..dec09be --- /dev/null +++ b/generate_keys.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +""" +Генерация пары ключей для HMAC авторизации. +Запусти один раз, сохрани ключи на клиенте и сервере. + +Использование: + python generate_keys.py +""" + +import secrets +import json +import os + + +def generate_keys(): + """Генерирует пару ключей: client_id (публичный) и secret_key (секретный).""" + client_id = f"client_{secrets.token_hex(8)}" + secret_key = secrets.token_hex(32) + + keys = { + "client_id": client_id, + "secret_key": secret_key + } + + print("=" * 60) + print("🔐 Сгенерированы ключи для HMAC авторизации") + print("=" * 60) + print(f"\n Client ID (публичный): {client_id}") + print(f" Secret Key (секретный): {secret_key}") + print() + + # Сохраняем в файл + keys_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "keys.json") + with open(keys_file, "w") as f: + json.dump(keys, f, indent=2) + + print(f"✅ Ключи сохранены в: {keys_file}") + print() + print("⚠️ ВАЖНО:") + print(" 1. Скопируй keys.json на СЕРВЕР в папку custom_comfyui/") + print(" 2. Скопируй keys.json на КЛИЕНТ (твой ПК)") + print(" 3. НЕ коммить keys.json в git!") + print("=" * 60) + + +if __name__ == "__main__": + generate_keys() diff --git a/hmac_auth.py b/hmac_auth.py new file mode 100644 index 0000000..9492253 --- /dev/null +++ b/hmac_auth.py @@ -0,0 +1,109 @@ +""" +HMAC авторизация для API запросов. +Используется и на сервере, и на клиенте. + +Принцип: +1. Клиент подписывает тело запроса секретным ключом + timestamp + nonce +2. Сервер проверяет подпись тем же ключом +3. Перехват бесполезен — подпись уникальна для каждого запроса +4. Replay-атака невозможна — timestamp + nonce проверяются +""" + +import hashlib +import hmac +import json +import time +import secrets + + +# Максимальное расхождение времени (секунды) между клиентом и сервером +MAX_TIMESTAMP_DRIFT = 300 # 5 минут + + +def sign_request(body: bytes, secret_key: str, client_id: str) -> dict: + """ + Подписывает запрос. Возвращает заголовки для отправки. + + Args: + body: тело запроса (bytes) + secret_key: секретный ключ (общий для клиента и сервера) + client_id: публичный ID клиента + + Returns: + dict с заголовками: X-Client-Id, X-Timestamp, X-Nonce, X-Signature + """ + timestamp = str(int(time.time())) + nonce = secrets.token_hex(16) + + # Строка для подписи: timestamp + nonce + body + message = f"{timestamp}.{nonce}.".encode() + body + signature = hmac.new( + secret_key.encode(), + message, + hashlib.sha256 + ).hexdigest() + + return { + "X-Client-Id": client_id, + "X-Timestamp": timestamp, + "X-Nonce": nonce, + "X-Signature": signature, + } + + +def verify_request(body: bytes, headers: dict, secret_key: str, client_id: str, + used_nonces: set = None) -> tuple[bool, str]: + """ + Проверяет подпись запроса. + + Args: + body: тело запроса (bytes) + headers: заголовки запроса + secret_key: секретный ключ + client_id: ожидаемый client_id + used_nonces: множество уже использованных nonce (для защиты от replay) + + Returns: + (is_valid, error_message) + """ + req_client_id = headers.get("X-Client-Id", "") + timestamp = headers.get("X-Timestamp", "") + nonce = headers.get("X-Nonce", "") + signature = headers.get("X-Signature", "") + + # Проверка client_id + if not hmac.compare_digest(req_client_id, client_id): + return False, "Invalid client ID" + + # Проверка timestamp + try: + req_time = int(timestamp) + except (ValueError, TypeError): + return False, "Invalid timestamp" + + now = int(time.time()) + if abs(now - req_time) > MAX_TIMESTAMP_DRIFT: + return False, f"Timestamp expired (drift: {abs(now - req_time)}s)" + + # Проверка nonce (защита от replay-атак) + if used_nonces is not None: + if nonce in used_nonces: + return False, "Nonce already used (replay attack?)" + used_nonces.add(nonce) + # Чистим старые nonce (старше MAX_TIMESTAMP_DRIFT) + # В реальном проде это делается через Redis TTL, тут — просто ограничиваем размер + if len(used_nonces) > 10000: + used_nonces.clear() + + # Проверка подписи + message = f"{timestamp}.{nonce}.".encode() + body + expected = hmac.new( + secret_key.encode(), + message, + hashlib.sha256 + ).hexdigest() + + if not hmac.compare_digest(signature, expected): + return False, "Invalid signature" + + return True, "OK" diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..5475a42 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,2 @@ +# ЭТОТ ФАЙЛ БОЛЬШЕ НЕ ИСПОЛЬЗУЕТСЯ — удали его +# API работает напрямую через Python на порту 8080 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1ca0a54 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +flask==3.1.0 +gunicorn==23.0.0 +websocket-client==1.8.0 +Pillow==11.1.0 +requests==2.32.3 diff --git a/server.py b/server.py new file mode 100644 index 0000000..b2e4d2b --- /dev/null +++ b/server.py @@ -0,0 +1,386 @@ +#!/usr/bin/env python3 +""" +DaSiWa I2V/FLF2V API Server для ComfyUI. +Работает рядом с ComfyUI на той же машине. + +Принимает HTTP запросы с HMAC авторизацией, +отправляет workflow в ComfyUI, возвращает видео. +""" + +import os +import sys +import json +import uuid +import time +import shutil +import base64 +import random +import logging +import binascii +import subprocess +import urllib.request +import urllib.parse +import websocket as ws_client + +from flask import Flask, request, jsonify +from hmac_auth import verify_request + +# ============================================================================ +# Конфигурация +# ============================================================================ + +COMFY_HOST = os.getenv("COMFY_HOST", "127.0.0.1") +COMFY_PORT = os.getenv("COMFY_PORT", "8188") +API_PORT = int(os.getenv("API_PORT", "8080")) + +WORKFLOW_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "workflow_api.json") +KEYS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "keys.json") + +COMFY_OUTPUT_DIR = os.getenv("COMFY_OUTPUT_DIR", "/ComfyUI/output") + +# ============================================================================ +# Инициализация +# ============================================================================ + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s" +) +logger = logging.getLogger(__name__) + +app = Flask(__name__) + +# Загрузка ключей +if not os.path.exists(KEYS_FILE): + logger.error(f"❌ Файл ключей не найден: {KEYS_FILE}") + logger.error(" Запусти: python generate_keys.py") + sys.exit(1) + +with open(KEYS_FILE, "r") as f: + keys = json.load(f) + CLIENT_ID = keys["client_id"] + SECRET_KEY = keys["secret_key"] + +logger.info(f"🔐 Ключи загружены. Client ID: {CLIENT_ID}") + +# Множество использованных nonce (защита от replay-атак) +used_nonces = set() + +# WebSocket client ID +ws_client_id = str(uuid.uuid4()) + + +# ============================================================================ +# Утилиты +# ============================================================================ + +def to_nearest_multiple_of_16(value): + """Округляет значение до ближайшего кратного 16.""" + try: + numeric_value = float(value) + except Exception: + raise ValueError(f"width/height value is not a number: {value}") + adjusted = int(round(numeric_value / 16.0) * 16) + return max(adjusted, 16) + + +def save_base64_to_file(base64_data, temp_dir, filename): + """Сохраняет base64 данные в файл.""" + decoded = base64.b64decode(base64_data) + os.makedirs(temp_dir, exist_ok=True) + file_path = os.path.abspath(os.path.join(temp_dir, filename)) + with open(file_path, "wb") as f: + f.write(decoded) + return file_path + + +def download_file(url, temp_dir, filename): + """Скачивает файл по URL.""" + os.makedirs(temp_dir, exist_ok=True) + file_path = os.path.abspath(os.path.join(temp_dir, filename)) + result = subprocess.run( + ["wget", "-O", file_path, "--no-verbose", url], + capture_output=True, text=True, timeout=120 + ) + if result.returncode != 0: + raise RuntimeError(f"Download failed: {result.stderr}") + return file_path + + +def process_image_input(job_input, prefix, temp_dir): + """ + Обрабатывает входные данные изображения. + prefix: "image" или "last_image" + Возвращает (file_path, True) или (None, False) + """ + path_key = f"{prefix}_path" + url_key = f"{prefix}_url" + b64_key = f"{prefix}_base64" + + if path_key in job_input and job_input[path_key]: + return job_input[path_key], True + elif url_key in job_input and job_input[url_key]: + return download_file(job_input[url_key], temp_dir, f"{prefix}.png"), True + elif b64_key in job_input and job_input[b64_key]: + return save_base64_to_file(job_input[b64_key], temp_dir, f"{prefix}.png"), True + + return None, False + + +def queue_prompt(prompt): + """Отправляет prompt в ComfyUI.""" + url = f"http://{COMFY_HOST}:{COMFY_PORT}/prompt" + data = json.dumps({"prompt": prompt, "client_id": ws_client_id}).encode("utf-8") + req = urllib.request.Request(url, data=data) + return json.loads(urllib.request.urlopen(req).read()) + + +def get_history(prompt_id): + """Получает историю выполнения prompt.""" + url = f"http://{COMFY_HOST}:{COMFY_PORT}/history/{prompt_id}" + with urllib.request.urlopen(url) as response: + return json.loads(response.read()) + + +def generate_video(prompt): + """Подключается к ComfyUI по WebSocket, запускает генерацию, ждёт результат.""" + ws_url = f"ws://{COMFY_HOST}:{COMFY_PORT}/ws?clientId={ws_client_id}" + + ws = ws_client.WebSocket() + ws.connect(ws_url) + logger.info("🔌 WebSocket подключён к ComfyUI") + + prompt_id = queue_prompt(prompt)["prompt_id"] + logger.info(f"📤 Prompt отправлен: {prompt_id}") + + # Ждём завершения + while True: + out = ws.recv() + if isinstance(out, str): + message = json.loads(out) + if message["type"] == "executing": + data = message["data"] + if data["node"] is None and data["prompt_id"] == prompt_id: + break + + ws.close() + logger.info("✅ Генерация завершена") + + # Извлекаем видео + history = get_history(prompt_id)[prompt_id] + for node_id in history["outputs"]: + node_output = history["outputs"][node_id] + if "gifs" in node_output: + for video in node_output["gifs"]: + video_path = video["fullpath"] + with open(video_path, "rb") as f: + video_b64 = base64.b64encode(f.read()).decode("utf-8") + # Очистка + try: + os.remove(video_path) + except OSError: + pass + return video_b64 + + return None + + +# ============================================================================ +# API Endpoints +# ============================================================================ + +@app.before_request +def check_hmac_auth(): + """Проверяет HMAC подпись для всех запросов кроме health check.""" + if request.path == "/health": + return None + + body = request.get_data() + headers = { + "X-Client-Id": request.headers.get("X-Client-Id", ""), + "X-Timestamp": request.headers.get("X-Timestamp", ""), + "X-Nonce": request.headers.get("X-Nonce", ""), + "X-Signature": request.headers.get("X-Signature", ""), + } + + is_valid, error = verify_request(body, headers, SECRET_KEY, CLIENT_ID, used_nonces) + if not is_valid: + logger.warning(f"🚫 Auth failed: {error} from {request.remote_addr}") + return jsonify({"error": "Unauthorized", "detail": error}), 401 + + +@app.route("/health", methods=["GET"]) +def health(): + """Health check — без авторизации.""" + try: + url = f"http://{COMFY_HOST}:{COMFY_PORT}/" + urllib.request.urlopen(url, timeout=5) + comfy_status = "ok" + except Exception: + comfy_status = "unavailable" + + return jsonify({ + "status": "ok", + "comfyui": comfy_status, + "timestamp": int(time.time()) + }) + + +@app.route("/generate", methods=["POST"]) +def generate(): + """Основной endpoint для генерации видео.""" + start_time = time.time() + + job_input = request.json or {} + + logger.info("=" * 60) + logger.info("🎬 Новый запрос на генерацию") + logger.info("=" * 60) + + # Логирование (без base64 данных) + log_input = {k: v for k, v in job_input.items() + if not k.endswith("_base64")} + logger.info(f"Параметры: {json.dumps(log_input, ensure_ascii=False)}") + + task_id = f"task_{uuid.uuid4().hex[:8]}" + temp_dir = os.path.join("/tmp", task_id) + + try: + # === Обработка изображений === + image_path, has_image = process_image_input(job_input, "image", temp_dir) + if not has_image: + return jsonify({"error": "No input image provided. Use image_base64, image_url, or image_path"}), 400 + + last_image_path, use_flf2v = process_image_input(job_input, "last_image", temp_dir) + + mode = "FLF2V" if use_flf2v else "I2V" + logger.info(f"🎬 Режим: {mode}") + + # === Загрузка workflow === + if not os.path.exists(WORKFLOW_FILE): + return jsonify({"error": f"Workflow file not found: {WORKFLOW_FILE}"}), 500 + + with open(WORKFLOW_FILE, "r") as f: + prompt = json.load(f) + + # === Параметры генерации === + width = to_nearest_multiple_of_16(job_input.get("width", 528)) + height = to_nearest_multiple_of_16(job_input.get("height", 768)) + length = job_input.get("length", 81) + steps = job_input.get("steps", 4) + cfg = job_input.get("cfg", 1.0) + seed = job_input.get("seed", -1) + fps = job_input.get("fps", 16) + sampler_name = job_input.get("sampler_name", "euler") + scheduler = job_input.get("scheduler", "linear_quadratic") + + if seed == -1: + seed = random.randint(0, 2**63 - 1) + + logger.info(f"📐 {width}x{height}, {length} frames, {steps} steps, CFG {cfg}, seed {seed}") + + # === Заполнение workflow === + + # Positive prompt + prompt["5"]["inputs"]["text"] = job_input.get("prompt", "") + + # Negative prompt + negative_prompt = job_input.get("negative_prompt", prompt["6"]["inputs"]["text"]) + prompt["6"]["inputs"]["text"] = negative_prompt + + # First frame image + prompt["7"]["inputs"]["image"] = image_path + + # FLF2V / I2V mode + if use_flf2v and last_image_path: + prompt["15"]["inputs"]["image"] = last_image_path + logger.info(f"🎬 FLF2V: last frame = {last_image_path}") + else: + prompt["8"]["class_type"] = "WanImageToVideo" + if "end_image" in prompt["8"]["inputs"]: + del prompt["8"]["inputs"]["end_image"] + if "15" in prompt: + del prompt["15"] + logger.info("🎬 I2V: single image mode") + + # Video dimensions + prompt["8"]["inputs"]["width"] = width + prompt["8"]["inputs"]["height"] = height + prompt["8"]["inputs"]["length"] = length + + # KSampler High + prompt["11"]["inputs"]["noise_seed"] = seed + prompt["11"]["inputs"]["steps"] = steps + prompt["11"]["inputs"]["cfg"] = cfg + prompt["11"]["inputs"]["sampler_name"] = sampler_name + prompt["11"]["inputs"]["scheduler"] = scheduler + prompt["11"]["inputs"]["end_at_step"] = steps // 2 + + # KSampler Low + prompt["12"]["inputs"]["noise_seed"] = seed + prompt["12"]["inputs"]["steps"] = steps + prompt["12"]["inputs"]["cfg"] = cfg + prompt["12"]["inputs"]["sampler_name"] = sampler_name + prompt["12"]["inputs"]["scheduler"] = scheduler + prompt["12"]["inputs"]["start_at_step"] = steps // 2 + + # Video output + prompt["14"]["inputs"]["frame_rate"] = fps + + # === Генерация === + video_b64 = generate_video(prompt) + + if not video_b64: + return jsonify({"error": "Video generation failed — no output"}), 500 + + elapsed = time.time() - start_time + logger.info(f"✅ Видео сгенерировано за {elapsed:.1f}s") + + return jsonify({ + "video": video_b64, + "seed": seed, + "mode": mode, + "elapsed": round(elapsed, 1) + }) + + except Exception as e: + logger.error(f"❌ Ошибка: {e}", exc_info=True) + return jsonify({"error": str(e)}), 500 + + finally: + # Очистка temp файлов + if os.path.exists(temp_dir): + shutil.rmtree(temp_dir, ignore_errors=True) + # Очистка output ComfyUI + try: + if os.path.exists(COMFY_OUTPUT_DIR): + for fname in os.listdir(COMFY_OUTPUT_DIR): + fpath = os.path.join(COMFY_OUTPUT_DIR, fname) + if os.path.isfile(fpath): + os.unlink(fpath) + elif os.path.isdir(fpath): + shutil.rmtree(fpath) + except Exception: + pass + + +# ============================================================================ +# Запуск +# ============================================================================ + +if __name__ == "__main__": + logger.info("=" * 60) + logger.info("🚀 DaSiWa API Server") + logger.info(f" ComfyUI: http://{COMFY_HOST}:{COMFY_PORT}") + logger.info(f" API Port: {API_PORT}") + logger.info(f" Workflow: {WORKFLOW_FILE}") + logger.info("=" * 60) + + # Проверяем подключение к ComfyUI + try: + urllib.request.urlopen(f"http://{COMFY_HOST}:{COMFY_PORT}/", timeout=5) + logger.info("✅ ComfyUI доступен") + except Exception: + logger.warning("⚠️ ComfyUI недоступен — запросы будут ждать") + + app.run(host="0.0.0.0", port=API_PORT, debug=False) diff --git a/setup.sh b/setup.sh new file mode 100644 index 0000000..5f544d2 --- /dev/null +++ b/setup.sh @@ -0,0 +1,181 @@ +#!/bin/bash +# ============================================================================ +# DaSiWa API Server — Автоматическая установка +# Запуск: chmod +x setup.sh && sudo ./setup.sh +# ============================================================================ + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +API_DIR="/root/custom_comfyui" +API_PORT=8080 + +echo "" +echo "============================================================" +echo "🚀 DaSiWa API Server — Установка" +echo "============================================================" +echo "" + +# ============================================================================ +# 1. Проверка root +# ============================================================================ +if [ "$EUID" -ne 0 ]; then + echo "❌ Запусти от root: sudo ./setup.sh" + exit 1 +fi + +# ============================================================================ +# 2. Обновление пакетов +# ============================================================================ +echo "📦 Обновление пакетов..." +apt-get update -qq + +# ============================================================================ +# 3. Python 3 + pip +# ============================================================================ +echo "🐍 Проверка Python..." +if command -v python3 &>/dev/null; then + PYTHON_VERSION=$(python3 --version 2>&1) + echo " ✅ $PYTHON_VERSION" +else + echo " 📥 Установка Python 3..." + apt-get install -y python3 python3-pip python3-venv +fi + +if ! command -v pip3 &>/dev/null; then + apt-get install -y python3-pip +fi + +# ============================================================================ +# 4. wget +# ============================================================================ +if ! command -v wget &>/dev/null; then + echo "📥 Установка wget..." + apt-get install -y wget +fi + +# ============================================================================ +# 5. Копирование файлов API +# ============================================================================ +echo "" +echo "📂 Настройка API директории: $API_DIR" + +if [ "$SCRIPT_DIR" != "$API_DIR" ]; then + mkdir -p "$API_DIR" + cp "$SCRIPT_DIR/server.py" "$API_DIR/" + cp "$SCRIPT_DIR/hmac_auth.py" "$API_DIR/" + cp "$SCRIPT_DIR/requirements.txt" "$API_DIR/" + cp "$SCRIPT_DIR/generate_keys.py" "$API_DIR/" + + [ -f "$SCRIPT_DIR/keys.json" ] && cp "$SCRIPT_DIR/keys.json" "$API_DIR/" && echo " ✅ keys.json скопирован" + [ -f "$SCRIPT_DIR/workflow_api.json" ] && cp "$SCRIPT_DIR/workflow_api.json" "$API_DIR/" && echo " ✅ workflow_api.json скопирован" + + echo " ✅ Файлы скопированы в $API_DIR" +else + echo " ✅ Уже в правильной директории" +fi + +# ============================================================================ +# 6. Установка Python зависимостей +# ============================================================================ +echo "" +echo "📦 Установка Python зависимостей..." +pip3 install -r "$API_DIR/requirements.txt" --quiet + +# ============================================================================ +# 7. Генерация ключей (если нет) +# ============================================================================ +if [ ! -f "$API_DIR/keys.json" ]; then + echo "" + echo "🔐 Генерация ключей..." + cd "$API_DIR" + python3 generate_keys.py + echo "" + echo "⚠️ ВАЖНО: Скопируй keys.json на свой ПК (клиент)!" + echo " scp root@:$API_DIR/keys.json ." +else + echo "🔐 keys.json найден — используем существующие ключи" +fi + +# ============================================================================ +# 8. Проверка workflow +# ============================================================================ +if [ ! -f "$API_DIR/workflow_api.json" ]; then + echo "" + echo "⚠️ workflow_api.json не найден!" + echo " Загрузи его: scp workflow_api.json root@:$API_DIR/" +fi + +# ============================================================================ +# 9. Настройка Firewall (UFW) +# ============================================================================ +echo "" +echo "🔥 Настройка Firewall..." +if ! command -v ufw &>/dev/null; then + apt-get install -y ufw +fi + +ufw allow 22/tcp # SSH +ufw allow ${API_PORT}/tcp # API +# НЕ открываем 8188 — ComfyUI только через localhost +ufw --force enable +echo " ✅ UFW: открыты порты 22 (SSH), ${API_PORT} (API)" +echo " 🔒 Порт 8188 (ComfyUI) закрыт снаружи" + +# ============================================================================ +# 10. Установка systemd сервиса (автозапуск) +# ============================================================================ +echo "" +echo "⚙️ Настройка systemd сервиса..." + +cat > /etc/systemd/system/dasiwa-api.service << EOF +[Unit] +Description=DaSiWa API Server +After=network.target + +[Service] +Type=simple +User=root +WorkingDirectory=$API_DIR +Environment=COMFY_HOST=127.0.0.1 +Environment=COMFY_PORT=8188 +Environment=API_PORT=${API_PORT} +Environment=COMFY_OUTPUT_DIR=/ComfyUI/output +ExecStart=/usr/bin/python3 $API_DIR/server.py +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target +EOF + +systemctl daemon-reload +systemctl enable dasiwa-api +systemctl start dasiwa-api +echo " ✅ Сервис dasiwa-api включён и запущен" +echo " ✅ Автозапуск при старте системы: ON" + +# ============================================================================ +# ГОТОВО +# ============================================================================ +echo "" +echo "============================================================" +echo "✅ УСТАНОВКА ЗАВЕРШЕНА" +echo "============================================================" +echo "" +echo "📋 Чеклист:" +echo " 1. Загрузи модели в /ComfyUI/models/ (если ещё нет)" +echo " 2. Загрузи workflow_api.json в $API_DIR/" +echo " 3. Скопируй keys.json на свой ПК" +echo "" +echo "🔧 Управление:" +echo " systemctl status dasiwa-api — статус" +echo " systemctl restart dasiwa-api — перезапуск" +echo " journalctl -u dasiwa-api -f — логи" +echo "" +echo "🧪 Проверка:" +echo " curl http://localhost:${API_PORT}/health" +echo "" +echo "🔐 API доступен на:" +echo " http://:${API_PORT}/generate" +echo "============================================================"