This commit is contained in:
Av007
2026-04-16 12:50:18 +03:00
commit 7dacbd4501
3 changed files with 524 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
"""
credit_tracker — ComfyUI custom node package
Install: copy this folder to ComfyUI/custom_nodes/ and restart ComfyUI.
"""
from .nodes import CreditDisplay, CreditDisplayFromStrings
NODE_CLASS_MAPPINGS = {
"CreditDisplay": CreditDisplay,
"CreditDisplayFromStrings": CreditDisplayFromStrings,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"CreditDisplay": "Credit Display",
"CreditDisplayFromStrings": "Credit Display (from strings)",
}
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
+506
View File
@@ -0,0 +1,506 @@
"""
CreditDisplay — Pre-flight cost estimator for ComfyUI Partner Nodes.
Pricing source: https://docs.comfy.org/tutorials/partner-nodes/pricing
All costs are in **Comfy Credits**. Conversion: 211 credits = 1 USD.
ComfyUI already tracks *actual* spend per generation in Settings → Credits.
This node gives you an *estimate before queuing* based on known rates.
Install: copy this folder to ComfyUI/custom_nodes/ and restart ComfyUI.
"""
# Official pricing reference — single source of truth for the URL
PRICING_SOURCE_URL = "https://docs.comfy.org/tutorials/partner-nodes/pricing"
# Official conversion rate (as of Apr 2026)
CREDITS_PER_USD = 211.0
# ---------------------------------------------------------------------------
# Resolution → (width, height) lookup (used for Seedance 2.0 token formula)
# ---------------------------------------------------------------------------
RESOLUTION_DIMS: dict[str, dict[str, tuple[int, int]]] = {
"16:9": {
"360p": (640, 360),
"480p": (854, 480),
"720p": (1280, 720),
"1080p": (1920, 1080),
},
"9:16": {
"360p": (360, 640),
"480p": (480, 854),
"720p": (720, 1280),
"1080p": (1080, 1920),
},
"1:1": {
"360p": (360, 360),
"480p": (480, 480),
"720p": (720, 720),
"1080p": (1080, 1080),
},
}
# ---------------------------------------------------------------------------
# Pricing table
#
# billing_type:
# "per_run" — flat cost per generation (credits)
# "per_sec" — credits per second of output video
# "seedance2" — ByteDance Seedance 2.0/Fast: official pixel-temporal token formula
# tokens = (in_dur + out_dur) × h × w × 24fps ÷ 1024 (Per K tokens)
# "per_1m_tok" — ByteDance Seedance 1.x: same formula applied as estimation
# (Per 1M tokens; exact 1.x formula not published by ByteDance)
#
# Notes inline where non-obvious.
# ---------------------------------------------------------------------------
PRICING: dict = {
# ---- ByteDance Seedance ------------------------------------------------
# Source: PRICING_SOURCE_URL §ByteDance
# Seedance 2.0 token formula:
# tokens = (in_dur + out_dur) × out_h × out_w × 24fps ÷ 1024
# cost = tokens × credits_per_k_tokens / 1000
"Seedance 2.0 Fast (i2v/t2v)": {
"label": "Seedance 2.0 Fast — image/text→video",
"vendor": "ByteDance",
"model_id": "dreamina-seedance-2-0-fast",
"billing_type": "seedance2",
"credits_per_k_tokens": 1.182, # official: 1.182 / K tokens
},
"Seedance 2.0 Fast (v2v)": {
"label": "Seedance 2.0 Fast — video→video",
"vendor": "ByteDance",
"model_id": "dreamina-seedance-2-0-fast",
"billing_type": "seedance2",
"credits_per_k_tokens": 0.696, # official: 0.696 / K tokens
},
"Seedance 2.0 (i2v/t2v)": {
"label": "Seedance 2.0 — image/text→video",
"vendor": "ByteDance",
"model_id": "dreamina-seedance-2-0",
"billing_type": "seedance2",
"credits_per_k_tokens": 1.477,
},
"Seedance 2.0 (v2v)": {
"label": "Seedance 2.0 — video→video",
"vendor": "ByteDance",
"model_id": "dreamina-seedance-2-0",
"billing_type": "seedance2",
"credits_per_k_tokens": 0.907,
},
# Seedance 1.x — billed Per 1M tokens. Token formula not officially
# documented for 1.x; estimation uses the same spatial formula as 2.0.
"Seedance 1.0 Lite (i2v/t2v)": {
"label": "Seedance 1.0 Lite — i2v / t2v [model: seedance-1-0-lite-*-250428]",
"vendor": "ByteDance",
"billing_type": "per_1m_tok",
"credits_per_1m_tokens": 379.8, # 379.8 / 1M tokens
},
"Seedance 1.0 Pro (i2v/t2v)": {
"label": "Seedance 1.0 Pro — i2v / t2v [model: seedance-1-0-pro-250528]",
"vendor": "ByteDance",
"billing_type": "per_1m_tok",
"credits_per_1m_tokens": 527.5, # 527.5 / 1M tokens
},
"Seedance 1.0 Pro Fast (i2v/t2v)": {
"label": "Seedance 1.0 Pro Fast — i2v / t2v [model: seedance-1-0-pro-fast-251015]",
"vendor": "ByteDance",
"billing_type": "per_1m_tok",
"credits_per_1m_tokens": 211.0, # 211 / 1M tokens
},
"Seedream 4.5 (t2v)": {
"label": "Seedream 4.5 — t2v [model: seedream-4-5-251128]",
"vendor": "ByteDance",
"billing_type": "per_1m_tok",
"credits_per_1m_tokens": 211.0, # 211 / 1M tokens
},
"Seedance 1.5 Pro (no audio)": {
"label": "Seedance 1.5 Pro — no audio [model: seedance-1-5-pro-251215]",
"vendor": "ByteDance",
"billing_type": "per_1m_tok",
"credits_per_1m_tokens": 253.2, # 253.2 / 1M tokens
},
"Seedance 1.5 Pro (with audio)": {
"label": "Seedance 1.5 Pro — with audio [model: seedance-1-5-pro-251215]",
"vendor": "ByteDance",
"billing_type": "per_1m_tok",
"credits_per_1m_tokens": 506.4, # 506.4 / 1M tokens
},
# ---- Kling (Kuaishou) --------------------------------------------------
"Kling v2.1 Master Pro": {
"label": "Kling v2.1 Master (Pro)",
"vendor": "Kuaishou",
"billing_type": "per_run",
"credits_per_run": 295.4,
},
"Kling v2.1 Pro": {
"label": "Kling v2.1 (Pro)",
"vendor": "Kuaishou",
"billing_type": "per_run",
"credits_per_run": 103.39,
},
"Kling v2.1 Standard": {
"label": "Kling v2.1 (Standard)",
"vendor": "Kuaishou",
"billing_type": "per_run",
"credits_per_run": 59.08,
},
"Kling v1.6 Pro": {
"label": "Kling v1.6 (Pro)",
"vendor": "Kuaishou",
"billing_type": "per_run",
"credits_per_run": 103.39,
},
"Kling v1.6 Standard": {
"label": "Kling v1.6 (Standard)",
"vendor": "Kuaishou",
"billing_type": "per_run",
"credits_per_run": 59.08,
},
"Kling v2.6 Pro (no audio)": {
"label": "Kling v2.6 Pro (no audio)",
"vendor": "Kuaishou",
"billing_type": "per_sec",
"credits_per_sec": 14.77,
},
"Kling v2.6 Pro (with audio)": {
"label": "Kling v2.6 Pro (with audio)",
"vendor": "Kuaishou",
"billing_type": "per_sec",
"credits_per_sec": 29.54,
},
# ---- Google ------------------------------------------------------------
"Veo 2": {
"label": "Google Veo 2",
"vendor": "Google",
"billing_type": "per_run",
"credits_per_run": 105.5,
},
"Veo 3 Fast (no audio)": {
"label": "Google Veo 3 Fast (no audio)",
"vendor": "Google",
"billing_type": "per_run",
"credits_per_run": 168.8,
},
"Veo 3 Fast (with audio)": {
"label": "Google Veo 3 Fast (with audio)",
"vendor": "Google",
"billing_type": "per_run",
"credits_per_run": 253.2,
},
"Veo 3 (no audio)": {
"label": "Google Veo 3 (no audio)",
"vendor": "Google",
"billing_type": "per_run",
"credits_per_run": 337.6,
},
"Veo 3 (with audio)": {
"label": "Google Veo 3 (with audio)",
"vendor": "Google",
"billing_type": "per_run",
"credits_per_run": 675.2,
},
# ---- OpenAI Sora -------------------------------------------------------
"Sora 2 (720p)": {
"label": "OpenAI Sora 2 — 720p/1280p",
"vendor": "OpenAI",
"billing_type": "per_sec",
"credits_per_sec": 21.10,
},
"Sora 2 Pro (720p)": {
"label": "OpenAI Sora 2 Pro — 720p",
"vendor": "OpenAI",
"billing_type": "per_sec",
"credits_per_sec": 63.30,
},
"Sora 2 Pro (1080p)": {
"label": "OpenAI Sora 2 Pro — 1080p",
"vendor": "OpenAI",
"billing_type": "per_sec",
"credits_per_sec": 105.50,
},
# ---- Pika --------------------------------------------------------------
"Pika 2.2 i2v 5s 720p": {
"label": "Pika 2.2 — i2v 5s 720p",
"vendor": "Pika",
"billing_type": "per_run",
"credits_per_run": 42.2,
},
"Pika 2.2 i2v 5s 1080p": {
"label": "Pika 2.2 — i2v 5s 1080p",
"vendor": "Pika",
"billing_type": "per_run",
"credits_per_run": 94.95,
},
"Pika 2.2 i2v 10s 720p": {
"label": "Pika 2.2 — i2v 10s 720p",
"vendor": "Pika",
"billing_type": "per_run",
"credits_per_run": 126.6,
},
"Pika 2.2 i2v 10s 1080p": {
"label": "Pika 2.2 — i2v 10s 1080p",
"vendor": "Pika",
"billing_type": "per_run",
"credits_per_run": 211.0,
},
# ---- Minimax -----------------------------------------------------------
"Minimax Hailuo-02 6s 768P": {
"label": "Minimax Hailuo-02 — 6s 768P",
"vendor": "Minimax",
"billing_type": "per_run",
"credits_per_run": 59.08,
},
"Minimax Hailuo-02 6s 1080P": {
"label": "Minimax Hailuo-02 — 6s 1080P",
"vendor": "Minimax",
"billing_type": "per_run",
"credits_per_run": 103.39,
},
# ---- Luma --------------------------------------------------------------
"Luma Ray 2": {
"label": "Luma Ray 2",
"vendor": "Luma",
"billing_type": "per_sec",
"credits_per_sec": 1.93,
},
"Luma Ray Flash 2": {
"label": "Luma Ray Flash 2",
"vendor": "Luma",
"billing_type": "per_sec",
"credits_per_sec": 0.66,
},
# ---- Moonvalley --------------------------------------------------------
"Moonvalley i2v 5s": {
"label": "Moonvalley — i2v 5s",
"vendor": "Moonvalley",
"billing_type": "per_run",
"credits_per_run": 316.5,
},
"Moonvalley i2v 10s": {
"label": "Moonvalley — i2v 10s",
"vendor": "Moonvalley",
"billing_type": "per_run",
"credits_per_run": 633.0,
},
# ---- BFL Flux ----------------------------------------------------------
"Flux Dev": {
"label": "BFL Flux Dev",
"vendor": "BFL",
"billing_type": "per_run",
"credits_per_run": 5.28,
},
"Flux Pro 1.1": {
"label": "BFL Flux Pro 1.1",
"vendor": "BFL",
"billing_type": "per_run",
"credits_per_run": 8.44,
},
"Flux Pro 1.1 Ultra": {
"label": "BFL Flux Pro 1.1 Ultra",
"vendor": "BFL",
"billing_type": "per_run",
"credits_per_run": 12.66,
},
"Flux Kontext Pro": {
"label": "BFL Flux Kontext Pro",
"vendor": "BFL",
"billing_type": "per_run",
"credits_per_run": 8.44,
},
"Flux Kontext Max": {
"label": "BFL Flux Kontext Max",
"vendor": "BFL",
"billing_type": "per_run",
"credits_per_run": 16.88,
},
# ---- ElevenLabs --------------------------------------------------------
"ElevenLabs TTS": {
"label": "ElevenLabs Text-to-Speech",
"vendor": "ElevenLabs",
"billing_type": "per_run",
"credits_per_run": 50.64,
},
}
_MODEL_KEYS = list(PRICING.keys())
_ALL_RESOLUTIONS = ["360p", "480p", "720p", "1080p"]
_ALL_ASPECT_RATIOS = ["16:9", "9:16", "1:1"]
# ---------------------------------------------------------------------------
# Token / cost helpers
# ---------------------------------------------------------------------------
def _seedance2_tokens(
out_dur: float,
width: int,
height: int,
fps: int = 24,
in_dur: float = 0.0,
) -> float:
"""
Official Seedance 2.0 token formula.
Ref: https://docs.comfy.org/tutorials/partner-nodes/pricing §ByteDance
tokens = (in_dur + out_dur) × out_h × out_w × fps ÷ 1024
Returns token count (not K-tokens).
"""
return (in_dur + out_dur) * height * width * fps / 1024.0
def _calc_cost(
model_key: str,
resolution: str,
aspect_ratio: str,
duration_s: float,
runs: int,
) -> dict:
entry = PRICING.get(model_key)
if entry is None:
return {"error": f"Unknown model: {model_key}"}
bt = entry["billing_type"]
dims = RESOLUTION_DIMS.get(aspect_ratio, RESOLUTION_DIMS["16:9"])
w, h = dims.get(resolution, dims.get("480p", (854, 480)))
if bt == "per_run":
unit_credits = entry["credits_per_run"]
elif bt == "per_sec":
unit_credits = entry["credits_per_sec"] * duration_s
elif bt == "seedance2":
tokens = _seedance2_tokens(duration_s, w, h)
unit_credits = tokens * entry["credits_per_k_tokens"] / 1000.0
elif bt == "per_1m_tok":
# Use the Seedance 2.0 spatial formula as the best available estimate.
# ByteDance has not published the exact token formula for 1.x models,
# so results are approximate (~within 10-20% of actual charge).
tokens = _seedance2_tokens(duration_s, w, h)
unit_credits = tokens * entry["credits_per_1m_tokens"] / 1_000_000.0
else:
return {"error": f"Unknown billing_type: {bt}"}
total_credits = unit_credits * runs
total_usd = total_credits / CREDITS_PER_USD
return {
"model": entry["label"],
"vendor": entry["vendor"],
"billing_type": bt,
"resolution": resolution,
"aspect_ratio": aspect_ratio,
"dimensions": f"{w}×{h}",
"duration_s": duration_s,
"runs": runs,
"unit_credits": unit_credits,
"total_credits": total_credits,
"total_usd": total_usd,
}
def _format_output(b: dict) -> str:
if "error" in b:
return f"ERROR: {b['error']}"
lines = [
f"Model : {b['model']} ({b['vendor']})",
f"Settings : {b['resolution']} {b['aspect_ratio']} {b['dimensions']} {b['duration_s']}s ×{b['runs']} run(s)",
f"Per run : {b['unit_credits']:.2f} cr (${b['unit_credits'] / CREDITS_PER_USD:.4f})",
f"Total : {b['total_credits']:.2f} cr (${b['total_usd']:.4f})",
f"Rate : 211 cr = $1 USD | {PRICING_SOURCE_URL}",
]
return "\n".join(lines)
# ---------------------------------------------------------------------------
# ComfyUI node: CreditDisplay
# ---------------------------------------------------------------------------
class CreditDisplay:
"""
Estimate Comfy Credits spent for a Partner Node generation.
Wire cost_text → ShowText|pysssss to display inline on the canvas.
Actual charges are tracked in Settings → Credits after execution.
"""
CATEGORY = "utils/cost"
FUNCTION = "calculate"
RETURN_TYPES = ("STRING", "FLOAT", "FLOAT")
RETURN_NAMES = ("cost_text", "total_credits", "total_usd")
OUTPUT_NODE = True
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": (_MODEL_KEYS, {"default": "Seedance 2.0 Fast (i2v/t2v)"}),
"resolution": (_ALL_RESOLUTIONS, {"default": "480p"}),
"aspect_ratio": (_ALL_ASPECT_RATIOS, {"default": "16:9"}),
"duration_s": ("FLOAT", {"default": 5.0, "min": 1.0, "max": 120.0, "step": 0.5}),
"runs": ("INT", {"default": 1, "min": 1, "max": 100, "step": 1}),
},
"optional": {
# Wire any upstream value here to force recalc when params change.
"trigger": ("STRING", {"forceInput": True}),
},
}
def calculate(
self,
model: str,
resolution: str,
aspect_ratio: str,
duration_s: float,
runs: int,
trigger: str = "",
):
b = _calc_cost(model, resolution, aspect_ratio, duration_s, runs)
text = _format_output(b)
credits = float(b.get("total_credits", 0.0))
usd = float(b.get("total_usd", 0.0))
return {"ui": {"text": [text]}, "result": (text, credits, usd)}
# ---------------------------------------------------------------------------
# ComfyUI node: CreditDisplayFromStrings
# ---------------------------------------------------------------------------
class CreditDisplayFromStrings:
"""
Same as CreditDisplay but accepts model/resolution as STRING inputs
so you can pipe them from other nodes instead of picking from dropdowns.
"""
CATEGORY = "utils/cost"
FUNCTION = "calculate"
RETURN_TYPES = ("STRING", "FLOAT", "FLOAT")
RETURN_NAMES = ("cost_text", "total_credits", "total_usd")
OUTPUT_NODE = True
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": ("STRING", {"default": "Seedance 2.0 Fast (i2v/t2v)"}),
"resolution": ("STRING", {"default": "480p"}),
"aspect_ratio": ("STRING", {"default": "16:9"}),
"duration_s": ("FLOAT", {"default": 5.0, "min": 1.0, "max": 120.0, "step": 0.5}),
"runs": ("INT", {"default": 1, "min": 1, "max": 100}),
},
}
def calculate(
self,
model: str,
resolution: str,
aspect_ratio: str,
duration_s: float,
runs: int,
):
b = _calc_cost(model, resolution, aspect_ratio, duration_s, runs)
text = _format_output(b)
credits = float(b.get("total_credits", 0.0))
usd = float(b.get("total_usd", 0.0))
return {"ui": {"text": [text]}, "result": (text, credits, usd)}