* feat(inspector): use recorded run history when the graph alone can't name the prompt (#386) When a file's own graph leaves the prompt open, both inspect routes ask recorded run history about the file (one run reported it and it was written during that run). The save node that run reported it from anchors the walk like a file-name match, so dataset branches that share one saver prefix show their own prompt, labelled "Recorded run". Still open at one sampler whose PromptManager text was recorded at execution (a language model's output), the recorded text shows as "Execution recorded". Results the file resolves alone never change; a failed lookup judges the file alone. * fix(inspector): recorded texts follow history's saver; conflicts anchor nothing (#386) Review follow-ups: the texts PromptManager recorded are looked up at the sampler history's saver leads to, never at a sampler the unanchored walk named; a file name naming a different saver than history anchors nothing; outputs and texts are read in one transaction; "Recorded run" ranks below every workflow-inferred source; unexpected lookup errors log at error level. * fix(history): look up recorded runs by history's own spelling of the file (#386) The history route inspects a case-normalized target on Windows, while history matches file names exactly, so the occurrence's reference names the file.
164 lines
5.2 KiB
Python
164 lines
5.2 KiB
Python
"""One bounded current-file inspection worker shared by physical identities."""
|
|
|
|
import os
|
|
import stat
|
|
from pathlib import Path
|
|
from typing import Any, Callable
|
|
|
|
try:
|
|
from ...utils.prompt_inspector import (
|
|
MetadataReadError,
|
|
inspect_prompt_evidence,
|
|
read_inspection_file,
|
|
unreadable_evidence,
|
|
)
|
|
from ...utils.prompt_pair_resolver import (
|
|
INSPECTION_PROFILE,
|
|
PREVIEW_INSPECTION_PROFILE,
|
|
)
|
|
from ...utils.video_metadata import is_video_path
|
|
except ImportError:
|
|
from utils.prompt_inspector import (
|
|
MetadataReadError,
|
|
inspect_prompt_evidence,
|
|
read_inspection_file,
|
|
unreadable_evidence,
|
|
)
|
|
from utils.prompt_pair_resolver import (
|
|
INSPECTION_PROFILE,
|
|
PREVIEW_INSPECTION_PROFILE,
|
|
)
|
|
from utils.video_metadata import is_video_path
|
|
|
|
|
|
# Only these file types are served or inspected within an authorized directory.
|
|
IMAGE_EXTENSIONS = frozenset(
|
|
{
|
|
".png",
|
|
".jpg",
|
|
".jpeg",
|
|
".webp",
|
|
".gif",
|
|
".bmp",
|
|
".tiff",
|
|
".tif",
|
|
".mp4",
|
|
".webm",
|
|
".mov",
|
|
}
|
|
)
|
|
VIDEO_EXTENSIONS = frozenset({".mp4", ".webm", ".avi", ".mov", ".mkv", ".m4v", ".wmv"})
|
|
SERVABLE_EXTENSIONS = IMAGE_EXTENSIONS | VIDEO_EXTENSIONS
|
|
|
|
|
|
class CurrentFileChanged(Exception):
|
|
"""File facts are invalid or changed across the bounded metadata observation."""
|
|
|
|
|
|
def _current_file_facts(path: Path) -> os.stat_result:
|
|
if path.suffix.lower() not in SERVABLE_EXTENSIONS:
|
|
raise CurrentFileChanged
|
|
try:
|
|
observed = path.stat()
|
|
except (FileNotFoundError, NotADirectoryError, PermissionError):
|
|
raise CurrentFileChanged from None
|
|
if (
|
|
not stat.S_ISREG(observed.st_mode)
|
|
or not observed.st_mode & 0o444
|
|
or not os.access(path, os.R_OK)
|
|
):
|
|
raise CurrentFileChanged
|
|
return observed
|
|
|
|
|
|
def _in_host_temp(path: Path) -> bool:
|
|
"""Whether a file lies in ComfyUI's temp folder, where previews are written.
|
|
|
|
An unknown temp folder reads as no: the file is then inspected as saved
|
|
output.
|
|
"""
|
|
try:
|
|
import folder_paths
|
|
|
|
temp = folder_paths.get_temp_directory()
|
|
except (ImportError, AttributeError, OSError):
|
|
return False
|
|
if type(temp) is not str or not temp:
|
|
return False
|
|
try:
|
|
folder = os.path.normcase(os.path.realpath(temp))
|
|
target = os.path.normcase(os.path.realpath(path))
|
|
return os.path.commonpath([folder, target]) == folder
|
|
except (OSError, ValueError):
|
|
return False
|
|
|
|
|
|
def inspect_current_file(
|
|
path: Path,
|
|
*,
|
|
source_label: str,
|
|
saved: dict | None,
|
|
library: dict,
|
|
image_tags: Callable[[int, int], dict] | None = None,
|
|
recorded: Callable[[Path], Any] | None = None,
|
|
) -> dict:
|
|
"""Current-file evidence read between two equal stats of *path*.
|
|
|
|
*image_tags*, when given, is called once with that stable
|
|
``(st_size, st_mtime_ns)`` and its facet is returned as ``image_tags``
|
|
(spec amendment S1: tags of a byte record verified at exactly that stat).
|
|
*recorded*, when given, is asked within the same stat window for what
|
|
recorded run history knows about *path* (``RecordedOutput`` or None: the
|
|
save node that wrote it and the texts PromptManager recorded, #386).
|
|
"""
|
|
before = _current_file_facts(path)
|
|
try:
|
|
inspection = read_inspection_file(path)
|
|
except MetadataReadError:
|
|
inspection = None
|
|
run = recorded(path) if recorded is not None and inspection is not None else None
|
|
after = _current_file_facts(path)
|
|
fields = ("st_dev", "st_ino", "st_mode", "st_size", "st_mtime_ns", "st_ctime_ns")
|
|
if any(getattr(before, key) != getattr(after, key) for key in fields):
|
|
raise CurrentFileChanged
|
|
facets = {}
|
|
if image_tags is not None:
|
|
facets["image_tags"] = image_tags(before.st_size, before.st_mtime_ns)
|
|
if inspection is None:
|
|
evidence = unreadable_evidence()
|
|
else:
|
|
role_saved = (
|
|
None if saved and saved.get("association_status") == "ambiguous" else saved
|
|
)
|
|
# A preview may be the output of a stage that reaches only previews.
|
|
profile = (
|
|
PREVIEW_INSPECTION_PROFILE if _in_host_temp(path) else INSPECTION_PROFILE
|
|
)
|
|
# The file name anchors on the saver that wrote it (#381); recorded run
|
|
# history names that saver when the file alone cannot (#386).
|
|
evidence = inspect_prompt_evidence(
|
|
inspection.metadata,
|
|
role_saved,
|
|
profile=profile,
|
|
file_name=path.name,
|
|
recorded_saver_id=run.saver_id if run is not None else None,
|
|
recorded_texts=run.texts if run is not None else None,
|
|
)
|
|
return {
|
|
"file": {
|
|
"observation": "current-file",
|
|
"source_label": source_label,
|
|
"media_type": "video" if is_video_path(path) else "image",
|
|
"extension": path.suffix.lower(),
|
|
"size": before.st_size,
|
|
"modified_time": before.st_mtime,
|
|
"format": None,
|
|
"width": None,
|
|
"height": None,
|
|
},
|
|
**evidence,
|
|
"inspection": inspection,
|
|
"library": library,
|
|
**facets,
|
|
}
|