Files
BoosAPI/boosapi_comfy_nodes.py
boos 9cfe8427b5
CI / Detect Docs Changes (push) Has been cancelled
CI / Test Core (push) Has been cancelled
CI / Build Web (push) Has been cancelled
CI / Build Server (push) Has been cancelled
CI / Build Desktop (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Repo Drift Check (push) Has been cancelled
CI / Schema Check (SQLite) (push) Has been cancelled
CI / Schema Check (MySQL) (push) Has been cancelled
CI / Schema Check (Postgres) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Audit Production Dependencies (push) Has been cancelled
CI / Publish Docker Image (amd64) (push) Has been cancelled
CI / Publish Docker Image (arm64) (push) Has been cancelled
CI / Publish Docker Image (armv7) (push) Has been cancelled
CI / Publish Docker Manifest (push) Has been cancelled
CodeQL / Analyze (JavaScript/TypeScript) (javascript-typescript) (push) Has been cancelled
feat: rebrand Metapi to BoosAPI, add ComfyUI agent and user management
- Rename project from Metapi to BoosAPI across all UI and server strings
- Add ComfyUI conversational agent page (/comfyui-agent)
- Add user management system (register, login, API keys, admin)
- Update Dockerfile and database schema
- Add ComfyUI workflow nodes support
- Update shared contracts and platform configurations

Signed-off-by: Boos4721 <boos4721@icloud.com>
2026-05-15 20:20:58 +08:00

296 lines
10 KiB
Python

"""
BoosAPI Custom Nodes for ComfyUI
==================================
Call BoosAPI/metapi endpoints directly from ComfyUI workflows:
- Text generation (gpt-5.5)
- Image generation (gpt-image-2)
- Text-to-Speech
- Video generation (async)
Configuration (in ComfyUI settings or env vars):
BOOSAPI_BASE = https://api.boos.lat/v1
BOOSAPI_KEY = sk-boos4721
"""
import os
import json
import time
import requests
import torch
import numpy as np
from PIL import Image
from io import BytesIO
import folder_paths
# ── Config ──────────────────────────────────────────────────────────────────
BOOSAPI_BASE = os.getenv("BOOSAPI_BASE", "https://api.boos.lat/v1")
BOOSAPI_KEY = os.getenv("BOOSAPI_KEY", "sk-boos4721")
HEADERS = {
"Authorization": f"Bearer {BOOSAPI_KEY}",
"Content-Type": "application/json",
}
def _call_api(endpoint: str, payload: dict, timeout: int = 120) -> dict:
url = f"{BOOSAPI_BASE}/{endpoint.lstrip('/')}"
resp = requests.post(url, headers=HEADERS, json=payload, timeout=timeout)
if not resp.ok:
raise RuntimeError(f"BoosAPI {endpoint} failed HTTP {resp.status_code}: {resp.text}")
return resp.json()
# ── Shared helpers ──────────────────────────────────────────────────────────
def _pil_to_tensor(img: Image.Image) -> torch.Tensor:
"""PIL -> ComfyUI IMAGE (1, H, W, 3) float32 0-1."""
img = img.convert("RGB")
arr = np.array(img).astype(np.float32) / 255.0
return torch.from_numpy(arr).unsqueeze(0)
def _tensor_to_pil(tensor: torch.Tensor) -> Image.Image:
"""ComfyUI IMAGE -> PIL."""
arr = (tensor.squeeze(0).cpu().numpy() * 255).astype(np.uint8)
return Image.fromarray(arr)
# ── Node: Text Generation (gpt-5.5) ─────────────────────────────────────────
class BoosAPITextGen:
"""Call gpt-5.5 chat completion. Useful for prompt engineering within workflows."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"multiline": True, "default": ""}),
"model": ("STRING", {"default": "gpt-5.5"}),
"temperature": ("FLOAT", {"default": 0.7, "min": 0, "max": 2, "step": 0.1}),
"max_tokens": ("INT", {"default": 2048, "min": 1, "max": 32768}),
},
"optional": {
"system_prompt": ("STRING", {"multiline": True, "default": ""}),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("text",)
FUNCTION = "generate"
CATEGORY = "BoosAPI"
def generate(self, prompt, model, temperature, max_tokens, system_prompt=""):
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
messages.append({"role": "user", "content": prompt})
data = _call_api("chat/completions", {
"model": model,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens,
})
text = data["choices"][0]["message"]["content"]
return (text,)
# ── Node: Image Generation (gpt-image-2) ────────────────────────────────────
class BoosAPIImageGen:
"""Generate image via BoosAPI. Replaces CheckpointLoader+KSampler+VAE chain."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"multiline": True, "default": ""}),
"model": (["gpt-image-2", "dall-e-3"], {"default": "gpt-image-2"}),
"size": (["1024x1024", "1792x1024", "1024x1792"], {"default": "1024x1024"}),
},
"optional": {
"negative_prompt": ("STRING", {"multiline": True, "default": ""}),
"n": ("INT", {"default": 1, "min": 1, "max": 4}),
},
}
RETURN_TYPES = ("IMAGE", "STRING")
RETURN_NAMES = ("image", "image_url")
FUNCTION = "generate"
CATEGORY = "BoosAPI"
def generate(self, prompt, model, size, negative_prompt="", n=1):
payload = {
"model": model,
"prompt": prompt,
"n": n,
"size": size,
}
if negative_prompt:
payload["negative_prompt"] = negative_prompt
data = _call_api("images/generations", payload)
# Download first image
image_url = data["data"][0]["url"]
img_resp = requests.get(image_url, timeout=60)
img = Image.open(BytesIO(img_resp.content))
tensor = _pil_to_tensor(img)
return (tensor, image_url)
# ── Node: Text-to-Speech ────────────────────────────────────────────────────
class BoosAPITTS:
"""Generate speech audio from text via BoosAPI TTS."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": ("STRING", {"multiline": True, "default": ""}),
"voice": (["alloy", "echo", "fable", "onyx", "nova", "shimmer"], {"default": "alloy"}),
},
"optional": {
"speed": ("FLOAT", {"default": 1.0, "min": 0.25, "max": 4.0, "step": 0.25}),
},
}
RETURN_TYPES = ("STRING",) # file path to generated audio
RETURN_NAMES = ("audio_path",)
FUNCTION = "generate"
CATEGORY = "BoosAPI"
OUTPUT_NODE = True
def generate(self, text, voice, speed=1.0):
url = f"{BOOSAPI_BASE}/audio/speech"
resp = requests.post(
url, headers=HEADERS,
json={"model": "tts-1", "input": text, "voice": voice, "speed": speed},
timeout=120,
)
if not resp.ok:
raise RuntimeError(f"TTS failed HTTP {resp.status_code}: {resp.text}")
# Save to ComfyUI output directory
output_dir = folder_paths.get_output_directory()
os.makedirs(output_dir, exist_ok=True)
filename = f"boosapi_tts_{int(time.time())}.mp3"
filepath = os.path.join(output_dir, filename)
with open(filepath, "wb") as f:
f.write(resp.content)
print(f"[BoosAPI] TTS saved: {filepath}")
return (filepath,)
# ── Node: Video Generation (async) ─────────────────────────────────────────
class BoosAPIVideoGen:
"""Generate video via BoosAPI (async). Returns task ID for polling."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"multiline": True, "default": ""}),
"model": ("STRING", {"default": "cogvideo-5b"}),
"size": (["1024x1024", "1024x576", "576x1024", "1920x1080"], {"default": "1024x576"}),
},
"optional": {
"negative_prompt": ("STRING", {"multiline": True, "default": ""}),
"duration": ("INT", {"default": 5, "min": 2, "max": 30}),
"fps": ("INT", {"default": 16, "min": 8, "max": 30}),
},
}
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("task_id", "status_url")
FUNCTION = "generate"
CATEGORY = "BoosAPI"
def generate(self, prompt, model, size, negative_prompt="", duration=5, fps=16):
width, height = size.split("x")
payload = {
"model": model,
"prompt": prompt,
"width": int(width),
"height": int(height),
"duration": duration,
"fps": fps,
}
if negative_prompt:
payload["negative_prompt"] = negative_prompt
data = _call_api("videos", payload)
task_id = data.get("id", "")
status_url = f"{BOOSAPI_BASE}/videos/{task_id}"
return (task_id, status_url)
class BoosAPIVideoStatus:
"""Poll video generation status. Returns download URL when done."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"task_id": ("STRING", {"default": ""}),
},
"optional": {
"wait": ("BOOLEAN", {"default": True, "label_on": "Wait until done", "label_off": "Check once"}),
"poll_interval": ("INT", {"default": 5, "min": 1, "max": 60}),
"max_polls": ("INT", {"default": 60, "min": 1, "max": 600}),
},
}
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("status", "video_url")
FUNCTION = "poll"
CATEGORY = "BoosAPI"
def poll(self, task_id, wait=True, poll_interval=5, max_polls=60):
url = f"{BOOSAPI_BASE}/videos/{task_id}"
for attempt in range(max_polls):
resp = requests.get(url, headers=HEADERS, timeout=30)
if not resp.ok:
return ("error", f"HTTP {resp.status_code}")
data = resp.json()
status = data.get("status", "unknown")
if status == "completed":
video_url = data.get("video_url") or data.get("output", {}).get("video_url", "")
return ("completed", video_url)
elif status in ("failed", "error"):
error_msg = data.get("error", str(data))
return (f"failed: {error_msg}", "")
if not wait:
return (status, "")
time.sleep(poll_interval)
return ("timeout", "")
# ── Registration ────────────────────────────────────────────────────────────
NODE_CLASS_MAPPINGS = {
"BoosAPITextGen": BoosAPITextGen,
"BoosAPIImageGen": BoosAPIImageGen,
"BoosAPITTS": BoosAPITTS,
"BoosAPIVideoGen": BoosAPIVideoGen,
"BoosAPIVideoStatus": BoosAPIVideoStatus,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"BoosAPITextGen": "BoosAPI Text Gen (gpt-5.5)",
"BoosAPIImageGen": "BoosAPI Image Gen (gpt-image-2)",
"BoosAPITTS": "BoosAPI Text-to-Speech",
"BoosAPIVideoGen": "BoosAPI Video Gen (async)",
"BoosAPIVideoStatus": "BoosAPI Video Status",
}