Browse Source

Create comfy ui loader

master
e-maks 4 days ago
commit
a5b7f0eff7
  1. 4
      .gitignore
  2. 208
      README.md
  3. 132
      client.py
  4. 18
      dasiwa-api.service
  5. 47
      generate_keys.py
  6. 109
      hmac_auth.py
  7. 2
      nginx.conf
  8. 5
      requirements.txt
  9. 386
      server.py
  10. 181
      setup.sh

4
.gitignore vendored

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
keys.json
workflow_api.json
__pycache__/
*.pyc

208
README.md

@ -0,0 +1,208 @@ @@ -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@<ip_сервера>"
# Загрузить всю папку
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@<ip_сервера>
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@<ip_сервера>:/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://<ip_сервера>:8080 \
--image photo.png \
--prompt "woman dancing gracefully" \
--output video.mp4
# FLF2V — из двух кадров
python client.py \
--server http://<ip_сервера>: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
```

132
client.py

@ -0,0 +1,132 @@ @@ -0,0 +1,132 @@
#!/usr/bin/env python3
"""
Клиент для DaSiWa API Server.
Запускается на ТВОЁМ ПК. Отправляет подписанные запросы на сервер.
Использование:
python client.py --server http://<ip>:5000 --image photo.png --prompt "woman dancing"
python client.py --server http://<ip>: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()

18
dasiwa-api.service

@ -0,0 +1,18 @@ @@ -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

47
generate_keys.py

@ -0,0 +1,47 @@ @@ -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()

109
hmac_auth.py

@ -0,0 +1,109 @@ @@ -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"

2
nginx.conf

@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
# ЭТОТ ФАЙЛ БОЛЬШЕ НЕ ИСПОЛЬЗУЕТСЯ удали его
# API работает напрямую через Python на порту 8080

5
requirements.txt

@ -0,0 +1,5 @@ @@ -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

386
server.py

@ -0,0 +1,386 @@ @@ -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)

181
setup.sh

@ -0,0 +1,181 @@ @@ -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@<ip_сервера>:$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@<ip>:$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://<ip_сервера>:${API_PORT}/generate"
echo "============================================================"
Loading…
Cancel
Save