Create comfy ui loader
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
keys.json
|
||||||
|
workflow_api.json
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -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()
|
||||||
@@ -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
|
||||||
@@ -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
@@ -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"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# ЭТОТ ФАЙЛ БОЛЬШЕ НЕ ИСПОЛЬЗУЕТСЯ — удали его
|
||||||
|
# API работает напрямую через Python на порту 8080
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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 "============================================================"
|
||||||
Reference in New Issue
Block a user