Files
Ramiro Montes De Oca 482c7eb182 Add Render Time output dependency and media metadata
RenderTime is now an output node with an optional source input and Prefix field for naming generated files.

Also tracks saved image/video outputs, embeds workflow/timing metadata into managed PNG/MP4 artifacts, and exposes media previews in the node UI.
2026-05-09 20:12:05 -07:00

137 lines
4.8 KiB
Python

# ComfyUI Render Time — config_manager.py
# Author: Ramiro Montes De Oca
# GitHub: https://github.com/videofeedback/comfyui-render-time
#
# Reads and writes plugin settings from/to config.json in the plugin directory.
# Author/contact identity is stored separately in author.txt (blank on fresh install).
import json
from pathlib import Path
from typing import Any
PLUGIN_DIR = Path(__file__).parent
CONFIG_FILE = PLUGIN_DIR / "config.json"
AUTHOR_FILE = PLUGIN_DIR / "author.txt"
# Default configuration — all output types enabled, all default to ComfyUI output/.
# workflow_author / workflow_contact are intentionally NOT here — they live in author.txt
# so a fresh install always starts with blank identity fields.
DEFAULT_CONFIG: dict = {
"embed_json": {
"enabled": True,
"location": "default", # "default" | "custom"
"custom_path": "",
},
"txt_report": {
"enabled": True,
"location": "default",
"custom_path": "",
},
"isolated_json": {
"enabled": True,
"location": "default",
"custom_path": "",
},
"workflow_png": {
"enabled": True,
"location": "default",
"custom_path": "",
},
"workflow_mp4": {
"location": "default",
"custom_path": "",
},
"output_naming": {
"title_mode": "default", # "default" | "custom"
"custom_title": "",
"extra_mode": "default_t_ymd_hms_w", # "default_t_ymd_hms_w" | "t_w" | "t" | "w" | "custom" | "none"
"custom_extra": "",
},
# Embed workflow + render-time metadata into MP4 files written by Save Video
"video_metadata_enabled": True,
# Audio notification when a render + report completes
"notify_on_complete": True,
}
def _deep_merge(base: dict, override: dict) -> dict:
"""Merge override into base recursively; base fills in missing keys."""
result = dict(base)
for key, val in override.items():
if key in result and isinstance(result[key], dict) and isinstance(val, dict):
result[key] = _deep_merge(result[key], val)
else:
result[key] = val
return result
def get_author_info() -> dict:
"""
Read workflow_author / workflow_contact from author.txt.
Returns empty strings for both fields if the file does not exist.
This is intentional — a fresh install should never inherit someone else's name.
"""
if not AUTHOR_FILE.exists():
return {"workflow_author": "", "workflow_contact": ""}
try:
lines = AUTHOR_FILE.read_text(encoding="utf-8").splitlines()
author = lines[0].strip() if len(lines) > 0 else ""
contact = lines[1].strip() if len(lines) > 1 else ""
return {"workflow_author": author, "workflow_contact": contact}
except Exception:
return {"workflow_author": "", "workflow_contact": ""}
def save_author_info(author: str, contact: str) -> None:
"""
Persist workflow_author / workflow_contact to author.txt.
Called ONLY when the user explicitly saves from the Settings panel or
Properties panel — never called automatically during report generation.
"""
AUTHOR_FILE.write_text(f"{author}\n{contact}\n", encoding="utf-8")
def get_config() -> dict:
"""
Load config from disk, filling in any missing keys with defaults.
Author/contact are overlaid from author.txt (blank on fresh install).
"""
if not CONFIG_FILE.exists():
cfg = dict(DEFAULT_CONFIG)
else:
try:
with open(CONFIG_FILE, encoding="utf-8") as f:
on_disk = json.load(f)
# Strip any stale author fields that may have been saved by older versions
on_disk.pop("workflow_author", None)
on_disk.pop("workflow_contact", None)
cfg = _deep_merge(DEFAULT_CONFIG, on_disk)
except Exception:
cfg = dict(DEFAULT_CONFIG)
# Always overlay from author.txt so the frontend gets the right values
cfg.update(get_author_info())
return cfg
def save_config(cfg: dict) -> None:
"""
Persist config to disk. Author/contact fields are stripped — they belong
in author.txt, not config.json. Unknown keys are preserved as-is.
"""
cfg_to_save = {k: v for k, v in cfg.items()
if k not in ("workflow_author", "workflow_contact")}
merged = _deep_merge(DEFAULT_CONFIG, cfg_to_save)
CONFIG_FILE.write_text(json.dumps(merged, indent=2, ensure_ascii=False), encoding="utf-8")
def get_default_descriptions() -> dict:
"""Return human-readable default path descriptions for the UI."""
return {
"embed_json": "Default: ComfyUI output/",
"txt_report": "Default: ComfyUI output/",
"isolated_json": "Default: ComfyUI output/",
"workflow_png": "Default: ComfyUI output/",
"workflow_mp4": "Default: ComfyUI output/",
}