Files
2026-07-01 21:05:03 +08:00

849 lines
33 KiB
Python

from __future__ import annotations
import json
from pathlib import Path
import os
import traceback
try:
from .tgbridge.config import load_config, ConfigError
from .tgbridge.formatter import build_caption, build_detail_messages, build_json_metadata
from .tgbridge.metadata import build_metadata, resolve_send_type, select_files
from .tgbridge.reference_image import save_reference_image
from .tgbridge.telegram_bot import TelegramBotClient
except ImportError:
from tgbridge.config import load_config, ConfigError
from tgbridge.formatter import build_caption, build_detail_messages, build_json_metadata
from tgbridge.metadata import build_metadata, resolve_send_type, select_files
from tgbridge.reference_image import save_reference_image
from tgbridge.telegram_bot import TelegramBotClient
class TelegramWorkflowReport:
@classmethod
def INPUT_TYPES(cls):
string = ("STRING", {"default": ""})
multiline = ("STRING", {"default": "", "multiline": True})
return {
"required": {
"enabled": ("BOOLEAN", {"default": True}),
"upload_mode": (["final_only", "all_files"], {"default": "final_only"}),
"send_as": (["auto", "video", "photo", "document"], {"default": "auto"}),
"message_mode": (
["caption_only", "caption_and_reply", "caption_reply_and_json"],
{"default": "caption_and_reply"},
),
"failure_mode": (["warn", "raise"], {"default": "warn"}),
},
"optional": {
"filenames": ("VHS_FILENAMES",),
"title": string,
"positive_prompt": multiline,
"negative_prompt": multiline,
"extra_notes": multiline,
"base_model": string,
"base_model_obj": ("WANVIDEOMODEL",),
"high_model": string,
"high_model_obj": ("WANVIDEOMODEL",),
"low_model": string,
"low_model_obj": ("WANVIDEOMODEL",),
"vae": string,
"lora_1_name": string,
"lora_1_strength": ("FLOAT", {"default": 0.0, "step": 0.01}),
"lora_2_name": string,
"lora_2_strength": ("FLOAT", {"default": 0.0, "step": 0.01}),
"lora_3_name": string,
"lora_3_strength": ("FLOAT", {"default": 0.0, "step": 0.01}),
"lora_4_name": string,
"lora_4_strength": ("FLOAT", {"default": 0.0, "step": 0.01}),
"lora_5_name": string,
"lora_5_strength": ("FLOAT", {"default": 0.0, "step": 0.01}),
"high_loras_obj": ("WANVIDLORA",),
"high_loras": multiline,
"low_loras_obj": ("WANVIDLORA",),
"low_loras": multiline,
"extra_loras": multiline,
"seed": ("INT", {"default": -1, "min": -1}),
"steps": ("INT", {"default": 0, "min": 0}),
"cfg": ("FLOAT", {"default": 0.0, "min": 0.0, "step": 0.1}),
"sampler": string,
"scheduler": string,
"width": ("INT", {"default": 0, "min": 0}),
"height": ("INT", {"default": 0, "min": 0}),
"frames": ("INT", {"default": 0, "min": 0}),
"fps": ("FLOAT", {"default": 0.0, "min": 0.0, "step": 0.01}),
"reference_label": string,
"reference_image": ("IMAGE",),
"send_reference_image": ("BOOLEAN", {"default": False}),
},
}
RETURN_TYPES = ("VHS_FILENAMES", "STRING",)
RETURN_NAMES = ("filenames", "message_preview",)
FUNCTION = "upload"
CATEGORY = "Telegram"
OUTPUT_NODE = True
def _extract_model_name(self, model_obj):
"""Extract model name from WANVIDEOMODEL object."""
try:
model_name = model_obj.model["model_name"]
except (KeyError, AttributeError, TypeError):
return None
if isinstance(model_name, str) and model_name:
return os.path.basename(model_name)
return None
def _format_lora_list(self, lora_list):
"""Format WANVIDLORA list as string (one per line)."""
if lora_list is None:
return None
lora_entries = []
if isinstance(lora_list, list):
for lora in lora_list:
if isinstance(lora, dict):
name = lora.get("name", "unknown")
strength = lora.get("strength", 0.0)
if isinstance(strength, list):
strength_str = str(strength)
else:
strength_str = f"{strength:.2f}"
lora_entries.append(f"{name}({strength_str})")
return "\n".join(lora_entries) if lora_entries else None
def _process_model_inputs(self, kwargs):
"""Process WANVIDEOMODEL and WANVIDLORA inputs to extract strings."""
result = dict(kwargs)
# Extract model names from WANVIDEOMODEL objects (obj inputs take priority)
if "base_model_obj" in kwargs and kwargs["base_model_obj"] is not None:
name = self._extract_model_name(kwargs["base_model_obj"])
if name and not result.get("base_model"):
result["base_model"] = name
if "high_model_obj" in kwargs and kwargs["high_model_obj"] is not None:
name = self._extract_model_name(kwargs["high_model_obj"])
if name and not result.get("high_model"):
result["high_model"] = name
if "low_model_obj" in kwargs and kwargs["low_model_obj"] is not None:
name = self._extract_model_name(kwargs["low_model_obj"])
if name and not result.get("low_model"):
result["low_model"] = name
# Extract lora lists from WANVIDLORA objects (obj inputs take priority)
if "high_loras_obj" in kwargs and kwargs["high_loras_obj"] is not None:
formatted = self._format_lora_list(kwargs["high_loras_obj"])
if formatted and not result.get("high_loras"):
result["high_loras"] = formatted
if "low_loras_obj" in kwargs and kwargs["low_loras_obj"] is not None:
formatted = self._format_lora_list(kwargs["low_loras_obj"])
if formatted and not result.get("low_loras"):
result["low_loras"] = formatted
return result
def upload(
self,
filenames=None,
enabled=True,
upload_mode="final_only",
send_as="auto",
message_mode="caption_and_reply",
failure_mode="warn",
**kwargs,
):
# Process WANVIDEOMODEL and WANVIDLORA inputs to extract strings
processed_kwargs = self._process_model_inputs(kwargs)
# Try to load config, but use defaults for testing if not configured
try:
config = load_config()
parse_mode = config.parse_mode
except ConfigError:
# Config not set up - use defaults for testing/preview
parse_mode = "HTML"
print("[ComfyUI-tgBridge] Config not found - running in test mode (preview only)")
# Build preview message regardless of enabled state or filenames
if filenames is not None:
_, output_files = filenames
files, _ = select_files(output_files, upload_mode)
else:
files = []
metadata = build_metadata(processed_kwargs, files)
caption = build_caption(metadata, parse_mode)
detail_messages = list(build_detail_messages(metadata, parse_mode))
# Build preview text
preview_lines = ["=== Telegram Message Preview ==="]
preview_lines.append(f"\n📎 Caption:\n{caption}")
if detail_messages:
preview_lines.append("\n📝 Follow-up Messages:")
for i, msg in enumerate(detail_messages, 1):
preview_lines.append(f" {i}. {msg}")
preview_lines.append(f"\n📁 Files to upload: {[str(f) for f in files]}")
preview_lines.append(f"📤 Send as: {send_as}")
preview_lines.append(f"💬 Message mode: {message_mode}")
message_preview = "\n".join(preview_lines)
if not enabled or filenames is None:
return (filenames, message_preview)
try:
self._upload(
filenames=filenames,
upload_mode=upload_mode,
send_as=send_as,
message_mode=message_mode,
**processed_kwargs,
)
except ConfigError as exc:
# Config not set up - informative message for user
if failure_mode == "raise":
raise
print(f"[ComfyUI-tgBridge] Telegram not configured: {exc}")
print("[ComfyUI-tgBridge] Set COMFY_TG_BOT_TOKEN and COMFY_TG_CHAT_ID environment variables, or create config/telegram.json")
except Exception as exc:
if failure_mode == "raise":
raise
print(f"[ComfyUI-tgBridge] Telegram upload skipped: {exc}")
traceback.print_exc()
return (filenames, message_preview)
def _upload(self, filenames, upload_mode, send_as, message_mode, **kwargs):
_save_output, output_files = filenames
files, missing = select_files(output_files, upload_mode)
for missing_path in missing:
print(f"[ComfyUI-tgBridge] Missing output file skipped: {missing_path}")
if not files:
print("[ComfyUI-tgBridge] No files to send - skipping Telegram upload (test mode)")
return
config = load_config()
client = TelegramBotClient(config)
metadata = build_metadata(kwargs, files)
caption = build_caption(metadata, config.parse_mode)
first_message_id = None
for index, path in enumerate(files):
media_caption = caption if index == 0 else ""
send_type = resolve_send_type(path, send_as)
sent = client.send_media(path, send_type, media_caption)
if first_message_id is None:
first_message_id = sent.message_id
if message_mode in {"caption_and_reply", "caption_reply_and_json"}:
for message in build_detail_messages(metadata, config.parse_mode):
client.send_message(message, reply_to_message_id=first_message_id)
reference_path = None
try:
if kwargs.get("send_reference_image") and kwargs.get("reference_image") is not None:
reference_path = save_reference_image(kwargs["reference_image"])
caption_text = kwargs.get("reference_label") or "Reference image"
client.send_photo(reference_path, caption=caption_text, reply_to_message_id=first_message_id)
finally:
if reference_path:
Path(reference_path).unlink(missing_ok=True)
if message_mode == "caption_reply_and_json":
content = build_json_metadata(metadata)
client.send_document_bytes(
content,
filename="comfyui_generation_metadata.json",
caption="Generation metadata JSON",
reply_to_message_id=first_message_id,
)
class WanVideoModelToString:
"""Helper node to extract model name from WANVIDEOMODEL for use in string inputs."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": ("WANVIDEOMODEL",),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("model_name",)
FUNCTION = "extract"
CATEGORY = "Telegram"
DESCRIPTION = "Extracts the model name from a WANVIDEOMODEL object as a string."
def extract(self, model):
# The model name is stored in model.model["model_name"] via __getitem__
try:
model_name = model.model["model_name"]
except (KeyError, AttributeError, TypeError):
model_name = "unknown"
# If it's a full path, get just the basename
if isinstance(model_name, str) and model_name != "unknown":
model_name = os.path.basename(model_name)
return (model_name,)
class WanVideoLoraListToString:
"""Helper node to extract lora names and strengths from WANVIDLORA for use in string inputs."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {},
"optional": {
"high_loras": ("WANVIDLORA",),
"low_loras": ("WANVIDLORA",),
"format": (["list", "comma_separated", "numbered"], {"default": "list"}),
},
}
RETURN_TYPES = ("STRING", "STRING",)
RETURN_NAMES = ("high_loras", "low_loras",)
FUNCTION = "extract"
CATEGORY = "Telegram"
DESCRIPTION = "Extracts lora names and strengths from WANVIDLORA lists as formatted strings."
def extract(self, high_loras=None, low_loras=None, format="comma_separated"):
def format_lora_list(lora_list, fmt):
if lora_list is None:
return ""
lora_entries = []
if isinstance(lora_list, list):
for lora in lora_list:
if isinstance(lora, dict):
name = lora.get("name", "unknown")
strength = lora.get("strength", 0.0)
if isinstance(strength, list):
strength_str = str(strength)
else:
strength_str = f"{strength:.2f}"
lora_entries.append(f"{name}({strength_str})")
if fmt == "list":
return "\n".join(lora_entries) if lora_entries else ""
elif fmt == "numbered":
return "\n".join(f"{i+1}. {entry}" for i, entry in enumerate(lora_entries)) if lora_entries else ""
else: # comma_separated
return ", ".join(lora_entries) if lora_entries else ""
return (
format_lora_list(high_loras, format),
format_lora_list(low_loras, format),
)
class TelegramAutoReport:
"""Simplified Telegram report that reads workflow metadata from saved files."""
@classmethod
def INPUT_TYPES(cls):
string = ("STRING", {"default": ""})
multiline = ("STRING", {"default": "", "multiline": True})
return {
"required": {
"filenames": ("VHS_FILENAMES",),
"enabled": ("BOOLEAN", {"default": True}),
},
"optional": {
"title": string,
"extra_notes": multiline,
"send_as": (["auto", "video", "photo", "document"], {"default": "auto"}),
"message_mode": (
["caption_only", "caption_and_reply", "caption_reply_and_json"],
{"default": "caption_and_reply"},
),
"failure_mode": (["warn", "raise"], {"default": "warn"}),
},
}
RETURN_TYPES = ("VHS_FILENAMES", "STRING",)
RETURN_NAMES = ("filenames", "message_preview",)
FUNCTION = "upload"
CATEGORY = "Telegram"
OUTPUT_NODE = True
def _metadata_candidates(self, output_files):
candidates = []
seen = set()
def add(path):
candidate = Path(path)
key = str(candidate)
if key not in seen and candidate.exists():
seen.add(key)
candidates.append(candidate)
for raw_path in output_files:
path = Path(raw_path)
add(path)
add(path.with_suffix(".json"))
add(path.with_suffix(".png"))
add(path.with_suffix(".webp"))
return candidates
def _read_metadata_payload(self, metadata_path):
path = Path(metadata_path)
try:
if path.suffix.lower() == ".json":
return json.loads(path.read_text(encoding="utf-8"))
if path.suffix.lower() in {".png", ".webp"}:
return self._read_image_metadata(path)
except Exception as exc:
print(f"[ComfyUI-tgBridge] Failed to read metadata from {path}: {exc}")
return None
def _read_image_metadata(self, image_path):
try:
from PIL import Image
except ImportError:
print("[ComfyUI-tgBridge] Pillow is required to read embedded image metadata")
return None
with Image.open(image_path) as image:
info = dict(image.info)
payload = {}
for key in ("prompt", "workflow"):
value = info.get(key)
if value is not None:
payload[key] = self._parse_json_value(value)
return payload or None
def _extract_from_metadata_files(self, output_files):
for path in self._metadata_candidates(output_files):
payload = self._read_metadata_payload(path)
if not isinstance(payload, dict):
continue
extracted = self._extract_from_payload(payload)
if extracted:
print(f"[ComfyUI-tgBridge] Extracted metadata from: {path.name}")
return extracted
return {}
def _extract_from_payload(self, data):
extracted = {}
workflow = self._parse_json_value(data.get("workflow"))
prompt = self._parse_json_value(data.get("prompt"))
if isinstance(workflow, dict):
title = workflow.get("title") or workflow.get("name")
if title:
extracted["title"] = title
if isinstance(prompt, dict):
prompt_values = self._extract_from_prompt_graph(prompt)
extracted.update({key: value for key, value in prompt_values.items() if value not in ("", None, ())})
if isinstance(workflow, dict) and workflow.get("nodes"):
workflow_values = self._extract_from_workflow_nodes(workflow["nodes"])
for key, value in workflow_values.items():
if key not in extracted and value not in ("", None, ()):
extracted[key] = value
return extracted
def _extract_from_prompt_graph(self, prompt):
nodes = {
str(node_id): node
for node_id, node in prompt.items()
if isinstance(node, dict)
}
extracted = {}
sampler = self._find_sampler_node(nodes.values())
if sampler:
inputs = sampler.get("inputs", {})
self._copy_first(extracted, inputs, "seed", ("seed", "noise_seed"))
self._copy_first(extracted, inputs, "steps", ("steps",))
self._copy_first(extracted, inputs, "cfg", ("cfg", "cfg_scale"))
self._copy_first(extracted, inputs, "sampler", ("sampler_name", "sampler"))
self._copy_first(extracted, inputs, "scheduler", ("scheduler",))
positive = self._resolve_prompt_text(nodes, inputs.get("positive"))
negative = self._resolve_prompt_text(nodes, inputs.get("negative"))
if positive:
extracted["positive_prompt"] = positive
if negative:
extracted["negative_prompt"] = negative
if not extracted.get("positive_prompt"):
positive, negative = self._fallback_prompt_text(nodes.values())
if positive:
extracted["positive_prompt"] = positive
if negative:
extracted["negative_prompt"] = negative
self._extract_models_and_loras_from_prompt(nodes.values(), extracted)
self._extract_video_settings(nodes.values(), extracted)
return extracted
def _extract_from_workflow_nodes(self, nodes):
extracted = {}
positive = []
negative = []
for node in nodes:
if not isinstance(node, dict):
continue
node_type = str(node.get("type") or node.get("class_type") or "")
title = str(node.get("title") or "")
values = node.get("widgets_values")
text_value = self._workflow_text_value(values)
label = f"{node_type} {title}".lower()
if text_value and "negative" in label:
negative.append(text_value)
elif text_value and ("positive" in label or "prompt" in label or "text" in label):
positive.append(text_value)
self._extract_model_from_inputs(node_type, self._workflow_inputs(node), extracted)
if positive:
extracted["positive_prompt"] = "\n".join(positive)
if negative:
extracted["negative_prompt"] = "\n".join(negative)
return extracted
def _find_sampler_node(self, nodes):
for node in nodes:
class_type = str(node.get("class_type", ""))
if "sampler" in class_type.lower():
return node
return None
def _resolve_prompt_text(self, nodes, link):
node_id = self._linked_node_id(link)
if node_id is None or node_id not in nodes:
return ""
return self._collect_text_from_node(nodes, node_id, set())
def _collect_text_from_node(self, nodes, node_id, visited):
if node_id in visited:
return ""
visited.add(node_id)
node = nodes.get(node_id, {})
inputs = node.get("inputs", {})
text = inputs.get("text")
if isinstance(text, str):
return text
texts = []
for value in inputs.values():
linked_id = self._linked_node_id(value)
if linked_id is not None:
linked_text = self._collect_text_from_node(nodes, linked_id, visited)
if linked_text:
texts.append(linked_text)
return "\n".join(texts)
def _fallback_prompt_text(self, nodes):
positive = []
negative = []
unlabeled = []
for node in nodes:
class_type = str(node.get("class_type", ""))
inputs = node.get("inputs", {})
text = inputs.get("text")
if not isinstance(text, str) or not text.strip():
continue
label = class_type.lower()
if "negative" in label:
negative.append(text)
elif "positive" in label:
positive.append(text)
else:
unlabeled.append(text)
if not positive and unlabeled:
positive.append(unlabeled[0])
negative.extend(unlabeled[1:2])
return "\n".join(positive), "\n".join(negative)
def _extract_models_and_loras_from_prompt(self, nodes, extracted):
for node in nodes:
class_type = str(node.get("class_type", ""))
inputs = node.get("inputs", {})
self._extract_model_from_inputs(class_type, inputs, extracted)
self._extract_loras_from_inputs(class_type, inputs, extracted)
def _extract_model_from_inputs(self, class_type, inputs, extracted):
lowered = class_type.lower()
if "model" not in lowered and "loader" not in lowered and "checkpoint" not in lowered:
return
model_name = self._first_string(
inputs,
(
"model_name",
"ckpt_name",
"unet_name",
"checkpoint",
"checkpoint_name",
"vae_name",
),
)
if not model_name:
return
model_name = os.path.basename(model_name)
if "vae" in lowered or "vae_name" in inputs:
extracted.setdefault("vae", model_name)
elif "high" in lowered:
extracted.setdefault("high_model", model_name)
elif "low" in lowered:
extracted.setdefault("low_model", model_name)
else:
extracted.setdefault("base_model", model_name)
def _extract_loras_from_inputs(self, class_type, inputs, extracted):
if "lora" not in class_type.lower() and not any("lora" in str(key).lower() for key in inputs):
return
entries = []
for key, value in inputs.items():
key_text = str(key).lower()
if "lora" in key_text and isinstance(value, str) and value.lower() != "none":
strength = self._lora_strength(inputs)
entries.append(self._format_lora(value, strength))
elif "lora" in key_text and isinstance(value, list):
entries.extend(self._format_lora_list(value))
if not entries:
return
target = "high_loras" if "high" in class_type.lower() else "low_loras" if "low" in class_type.lower() else "extra_loras"
existing = extracted.get(target, "")
extracted[target] = "\n".join(line for line in (existing, "\n".join(entries)) if line)
def _extract_video_settings(self, nodes, extracted):
for node in nodes:
inputs = node.get("inputs", {})
self._copy_first(extracted, inputs, "width", ("width", "empty_latent_width"))
self._copy_first(extracted, inputs, "height", ("height", "empty_latent_height"))
self._copy_first(extracted, inputs, "frames", ("frames", "num_frames", "frame_count", "video_length", "length"))
self._copy_first(extracted, inputs, "fps", ("fps", "frame_rate"))
def _workflow_inputs(self, node):
inputs = {}
raw_inputs = node.get("inputs")
if isinstance(raw_inputs, dict):
return raw_inputs
if isinstance(raw_inputs, list):
for item in raw_inputs:
if isinstance(item, dict) and "name" in item and "value" in item:
inputs[item["name"]] = item["value"]
return inputs
def _workflow_text_value(self, values):
if isinstance(values, str):
return values
if isinstance(values, list):
for value in values:
if isinstance(value, str) and "\n" in value:
return value
for value in values:
if isinstance(value, str) and value.strip():
return value
if isinstance(values, dict):
value = values.get("text")
if isinstance(value, str):
return value
return ""
def _format_lora_list(self, values):
entries = []
for value in values:
if isinstance(value, dict):
name = value.get("name") or value.get("lora_name")
if name and str(name).lower() != "none":
entries.append(self._format_lora(str(name), value.get("strength")))
elif isinstance(value, str) and value.lower() != "none":
entries.append(value)
return entries
def _format_lora(self, name, strength):
if strength in (None, ""):
return os.path.basename(name)
try:
return f"{os.path.basename(name)}: {float(strength):g}"
except (TypeError, ValueError):
return f"{os.path.basename(name)}: {strength}"
def _lora_strength(self, inputs):
for key in ("strength_model", "strength_clip", "strength", "lora_strength"):
value = inputs.get(key)
if value not in (None, ""):
return value
return None
def _first_string(self, inputs, keys):
for key in keys:
value = inputs.get(key)
if isinstance(value, str) and value and value.lower() != "none":
return value
return ""
def _copy_first(self, extracted, inputs, target, keys):
if target in extracted:
return
for key in keys:
value = inputs.get(key)
if value not in (None, "") and not isinstance(value, (list, dict)):
extracted[target] = value
return
def _linked_node_id(self, value):
if isinstance(value, (list, tuple)) and value:
return str(value[0])
return None
def _parse_json_value(self, value):
if isinstance(value, str):
try:
return json.loads(value)
except json.JSONDecodeError:
return value
return value
def upload(
self,
filenames,
enabled=True,
title="",
extra_notes="",
send_as="auto",
message_mode="caption_and_reply",
failure_mode="warn",
):
if filenames is None:
return (filenames, "")
# Try to load config
config = None
config_error = None
try:
config = load_config()
parse_mode = config.parse_mode
except ConfigError as exc:
parse_mode = "HTML"
config_error = exc
print("[ComfyUI-tgBridge] Config not found - running in test mode (preview only)")
# Get output files
_, output_files = filenames
files, _ = select_files(output_files, "final_only")
if not files:
return (filenames, "[Telegram Auto] No files to process")
extracted = self._extract_from_metadata_files(output_files)
metadata_values = {
"title": title or extracted.get("title", ""),
"positive_prompt": extracted.get("positive_prompt", ""),
"negative_prompt": extracted.get("negative_prompt", ""),
"extra_notes": extra_notes,
"base_model": extracted.get("base_model", ""),
"high_model": extracted.get("high_model", ""),
"low_model": extracted.get("low_model", ""),
"vae": extracted.get("vae", ""),
"high_loras": extracted.get("high_loras", ""),
"low_loras": extracted.get("low_loras", ""),
"extra_loras": extracted.get("extra_loras", ""),
"seed": extracted.get("seed", -1),
"steps": extracted.get("steps", 0),
"cfg": extracted.get("cfg", 0),
"sampler": extracted.get("sampler", ""),
"scheduler": extracted.get("scheduler", ""),
"width": extracted.get("width", 0),
"height": extracted.get("height", 0),
"frames": extracted.get("frames", 0),
"fps": extracted.get("fps", 0),
}
metadata = build_metadata(metadata_values, files)
# Build caption and messages
caption = build_caption(metadata, parse_mode)
detail_messages = list(build_detail_messages(metadata, parse_mode))
# Build preview
preview_lines = ["=== Telegram Auto Report Preview ==="]
preview_lines.append(f"\n📎 Caption:\n{caption}")
if detail_messages:
preview_lines.append("\n📝 Follow-up Messages:")
for i, msg in enumerate(detail_messages, 1):
preview_lines.append(f" {i}. {msg}")
preview_lines.append(f"\n📁 Files to upload: {[str(f) for f in files]}")
message_preview = "\n".join(preview_lines)
if not enabled:
return (filenames, message_preview)
if config_error is not None:
if failure_mode == "raise":
raise config_error
print(f"[ComfyUI-tgBridge] Telegram not configured: {config_error}")
return (filenames, message_preview)
try:
client = TelegramBotClient(config)
# Send files
first_message_id = None
for index, path in enumerate(files):
media_caption = caption if index == 0 else ""
send_type = resolve_send_type(path, send_as)
sent = client.send_media(path, send_type, media_caption)
if first_message_id is None:
first_message_id = sent.message_id
# Send follow-up messages
if message_mode in {"caption_and_reply", "caption_reply_and_json"}:
for message in detail_messages:
client.send_message(message, reply_to_message_id=first_message_id)
# Send JSON metadata if requested
if message_mode == "caption_reply_and_json":
content = build_json_metadata(metadata)
client.send_document_bytes(
content,
filename="comfyui_generation_metadata.json",
caption="Generation metadata JSON",
reply_to_message_id=first_message_id,
)
except ConfigError as exc:
if failure_mode == "raise":
raise
print(f"[ComfyUI-tgBridge] Telegram not configured: {exc}")
except Exception as exc:
if failure_mode == "raise":
raise
print(f"[ComfyUI-tgBridge] Upload skipped: {exc}")
traceback.print_exc()
return (filenames, message_preview)
NODE_CLASS_MAPPINGS = {
"TelegramWorkflowReport": TelegramWorkflowReport,
"TelegramAutoReport": TelegramAutoReport,
"WanVideoModelToString": WanVideoModelToString,
"WanVideoLoraListToString": WanVideoLoraListToString,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"TelegramWorkflowReport": "Telegram Workflow Report",
"TelegramAutoReport": "Telegram Auto Workflow Report",
"WanVideoModelToString": "WanVideo Model → String",
"WanVideoLoraListToString": "WanVideo Lora List → String",
}