Files
MeteorAndyandClaude 6f2aa13575 Align with official API nodes: VIDEO/AUDIO output + real vendor formats
Rewrite each node to faithfully mirror the official comfy_api_nodes/
contract (apis/*.py + Comfy-Org/embedded-docs). The ComfyOrg proxy
forwards the body verbatim, so the official request/response models
ARE the real vendor API.

- Video nodes now return VIDEO (InputImpl.VideoFromFile), not file paths
- Audio nodes now return AUDIO ({waveform, sample_rate}), not file paths
- Request/response bodies match official pydantic models field-for-field
- Model ids, options, defaults, ranges match official + embedded docs
- Every node exposes api_key + base_url (overridable for relays/proxies)
- utils.py: add ComfyUI-native helpers — video_from_bytes,
  download_video_output, audio_from_bytes, download_audio_output,
  poll_until, validate_prompt, tensor_to_data_uri
- Rewrite nodes_minimax.py (submit -> poll -> retrieve -> download VIDEO)
- nodes_extra.py: base_url on all three, poll_until, validate_prompt
- Clean __init__.py (drop duplicate grok import/keys); 37 nodes total
- README: output-type alignment, node list, relay examples

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-15 20:07:04 +08:00

169 lines
6.9 KiB
Python

"""Reve AI image generation nodes (bring-your-own-key).
Mirrors the official ComfyUI ``ReveImageCreateNode`` but calls the real Reve
vendor API directly instead of going through the ComfyOrg proxy. The official
node posts to ``/proxy/reve/v1/image/create``; the proxy forwards the body
verbatim to ``https://api.reve.com/v1/image/create``, so the official request
model (``ReveImageCreateRequest``) and endpoint path ARE the real vendor API.
Request body (field-for-field with ``comfy_api_nodes/apis/reve.py``):
prompt: str
aspect_ratio: str | None
version: str (the model id, e.g. "reve-create@20250915")
test_time_scaling: int (1..15)
postprocessing: list[{process, upscale_factor?}] | None
The official node sends ``Accept: image/webp`` and reads the response as raw
binary image bytes (``bytesio_to_image_tensor``). We do the same and fall back
to parsing a base64 ``image`` field if a relay returns JSON instead.
"""
from .utils import bytes_to_tensor, download_bytes, http_request, run_async, validate_prompt
# Real Reve vendor host (overridable for relays). The official proxy path is
# "/proxy/reve/v1/image/create"; stripping "/proxy/reve" yields the real path.
REVE_API_BASE = "https://api.reve.com"
# Model versions and their selectable aspect ratios, taken verbatim from the
# official ReveImageCreateNode schema.
REVE_CREATE_MODELS = ["reve-create@20250915"]
REVE_CREATE_ASPECT_RATIOS = ["3:2", "16:9", "9:16", "2:3", "4:3", "3:4", "1:1"]
class BYOKey_Reve_Image:
"""Generate images from text descriptions using Reve. Output: IMAGE.
Faithful BYOKey port of the official ``ReveImageCreateNode``.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"api_key": ("STRING", {"default": "", "tooltip": "Reve API key (papi.xxxxx)."}),
"base_url": ("STRING", {"default": REVE_API_BASE, "tooltip": "Reve API host. Override for relays."}),
"prompt": (
"STRING",
{
"multiline": True,
"default": "",
"tooltip": "Text description of the desired image. Maximum 2560 characters.",
},
),
"model": (
REVE_CREATE_MODELS,
{"default": REVE_CREATE_MODELS[0], "tooltip": "Model version to use for generation."},
),
"aspect_ratio": (
REVE_CREATE_ASPECT_RATIOS,
{"default": REVE_CREATE_ASPECT_RATIOS[0], "tooltip": "Aspect ratio of the output image."},
),
},
"optional": {
"test_time_scaling": (
"INT",
{
"default": 1,
"min": 1,
"max": 5,
"step": 1,
"tooltip": "Higher values produce better images but cost more credits.",
},
),
"upscale": (
["disabled", "enabled"],
{"default": "disabled", "tooltip": "Upscale the generated image. May add additional cost."},
),
"upscale_factor": (
"INT",
{"default": 2, "min": 2, "max": 4, "step": 1, "tooltip": "Upscale factor (2x, 3x, or 4x)."},
),
"remove_background": (
"BOOLEAN",
{"default": False, "tooltip": "Remove the background from the generated image. May add additional cost."},
),
"seed": (
"INT",
{
"default": 0,
"min": 0,
"max": 2147483647,
"control_after_generate": True,
"tooltip": "Seed controls whether the node should re-run; "
"results are non-deterministic regardless of seed.",
},
),
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "execute"
CATEGORY = "api/byokey/image"
DESCRIPTION = "Generate images from text descriptions using Reve."
@staticmethod
def _build_postprocessing(upscale: str, upscale_factor: int, remove_background: bool):
"""Match official ``_build_postprocessing``: list of ops, or None."""
ops = []
if upscale == "enabled":
ops.append({"process": "upscale", "upscale_factor": int(upscale_factor)})
if remove_background:
ops.append({"process": "remove_background"})
return ops or None
def execute(
self,
api_key,
base_url,
prompt,
model=REVE_CREATE_MODELS[0],
aspect_ratio=REVE_CREATE_ASPECT_RATIOS[0],
test_time_scaling=1,
upscale="disabled",
upscale_factor=2,
remove_background=False,
seed=0,
):
# Official node: validate_string(prompt, min_length=1, max_length=2560)
validate_prompt(prompt, min_length=1)
if len(prompt) > 2560:
raise ValueError("Prompt must be at most 2560 characters.")
host = (base_url or REVE_API_BASE).rstrip("/")
url = f"{host}/v1/image/create"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"Accept": "image/webp",
}
# Request body field-for-field with ReveImageCreateRequest.
body = {
"prompt": prompt,
"aspect_ratio": aspect_ratio,
"version": model,
"test_time_scaling": int(test_time_scaling),
}
postprocessing = self._build_postprocessing(upscale, upscale_factor, remove_background)
if postprocessing is not None:
body["postprocessing"] = postprocessing
resp = run_async(http_request("POST", url, headers, json_data=body))
# The vendor returns raw image bytes when Accept: image/webp is set;
# utils.http_request surfaces non-JSON bodies as {"_raw": <bytes>}.
if isinstance(resp, dict) and "_raw" in resp:
return (bytes_to_tensor(resp["_raw"], mode="RGB"),)
# Fallback: a relay returned JSON (matching ReveImageResponse).
if isinstance(resp, dict):
if resp.get("content_violation"):
raise RuntimeError("The generated image was flagged for content policy violation.")
image_b64 = resp.get("image")
if image_b64:
# download_bytes decodes data: URIs; prefix bare base64 to reuse it.
data = run_async(download_bytes(f"data:image/webp;base64,{image_b64}"))
return (bytes_to_tensor(data, mode="RGB"),)
raise RuntimeError(f"Unexpected Reve response (no image bytes): {resp}")