From 7dacbd45016f8a797b13066cd5c4b496953ed9be Mon Sep 17 00:00:00 2001 From: Av007 Date: Thu, 16 Apr 2026 12:50:18 +0300 Subject: [PATCH] init --- custom_nodes/credit_tracker/__init__.py | 18 + .../__pycache__/nodes.cpython-313.pyc | Bin 0 -> 12007 bytes custom_nodes/credit_tracker/nodes.py | 506 ++++++++++++++++++ 3 files changed, 524 insertions(+) create mode 100644 custom_nodes/credit_tracker/__init__.py create mode 100644 custom_nodes/credit_tracker/__pycache__/nodes.cpython-313.pyc create mode 100644 custom_nodes/credit_tracker/nodes.py 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 0000000000000000000000000000000000000000..05368f75cbea8561b039b88bed188142fffbdaa0 GIT binary patch literal 12007 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 E4|N#8Z~y=R literal 0 HcmV?d00001 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)}