commit 7dacbd45016f8a797b13066cd5c4b496953ed9be Author: Av007 Date: Thu Apr 16 12:50:18 2026 +0300 init diff --git a/custom_nodes/credit_tracker/__init__.py b/custom_nodes/credit_tracker/__init__.py new file mode 100644 index 0000000..fbceb27 --- /dev/null +++ b/custom_nodes/credit_tracker/__init__.py @@ -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"] diff --git a/custom_nodes/credit_tracker/__pycache__/nodes.cpython-313.pyc b/custom_nodes/credit_tracker/__pycache__/nodes.cpython-313.pyc new file mode 100644 index 0000000..05368f7 Binary files /dev/null and b/custom_nodes/credit_tracker/__pycache__/nodes.cpython-313.pyc differ diff --git a/custom_nodes/credit_tracker/nodes.py b/custom_nodes/credit_tracker/nodes.py new file mode 100644 index 0000000..ecb4dc9 --- /dev/null +++ b/custom_nodes/credit_tracker/nodes.py @@ -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)}