diff --git a/core/media.py b/core/media.py index 0cb5b349..71f71940 100644 --- a/core/media.py +++ b/core/media.py @@ -117,7 +117,7 @@ def listing() -> list[dict]: if fresh: with _LOCK: _save_index(items) - out = [it for it in items if (config.MEDIA_DIR / it.get("filename", "")).is_file()] + out = [it for it in items if not it.get("scratch") and (config.MEDIA_DIR / it.get("filename", "")).is_file()] # scratch files are not the user's: never listed out.sort(key=lambda i: i.get("added", 0), reverse=True) return out @@ -166,6 +166,28 @@ def save_upload(orig_name: str, data: bytes, limit=...) -> dict: return entry +def save_scratch(orig_name: str, data: bytes, folder) -> dict: + """A picture the app made for one run (a carried last frame): written into `folder` (ComfyUI's temp folder), answerable by id + like any media but never listed in the bin. It goes when the folder is cleared; entries whose file is gone are dropped here.""" + if not data: + raise ValueError("that file is empty") + ext = Path(orig_name or "").suffix.lower() + if ext not in _IMAGE_EXT: + raise ValueError(f"{ext or 'that file type'} is not a picture") + mid = uuid.uuid4().hex[:12] + target = Path(folder) / (mid + ext) + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(data) + # `filename` is the absolute path: MEDIA_DIR / is that path, so path_for, delete and the thumbnails need no second case. + entry = {"id": mid, "name": (Path(orig_name or "").name or target.name)[:MAX_NAME], "filename": str(target), "kind": "image", + "size": len(data), "added": time.time(), "scratch": True} + with _LOCK: + items = [it for it in _load_index() if not (it.get("scratch") and not Path(it.get("filename", "")).is_file())] + items.append(entry) + _save_index(items) + return entry + + def path_for(mid: str) -> Path | None: it = get(mid) if not it: diff --git a/modules/output/render/serving.py b/modules/output/render/serving.py index d882031d..88b6aa6e 100644 --- a/modules/output/render/serving.py +++ b/modules/output/render/serving.py @@ -256,7 +256,7 @@ def last_frame(project, scene_id: str, render: dict | None = None, window: dict try: brightness, peak = files.last_frame(src, out, clip["in"], clip["dur"], clip["fps"], first, at_fraction) with open(out, "rb") as fh: - entry = media.save_upload(f"last_frame_{projects.safe_part(scene_id)[:12]}.png", fh.read()) + entry = media.save_scratch(f"last_frame_{projects.safe_part(scene_id)[:12]}.png", fh.read(), os.path.dirname(out)) # a working file in ComfyUI's temp folder, not an upload in the bin finally: try: os.remove(out) diff --git a/modules/output/render/tests/test_serving.py b/modules/output/render/tests/test_serving.py index 8c3d5c20..7121f651 100644 --- a/modules/output/render/tests/test_serving.py +++ b/modules/output/render/tests/test_serving.py @@ -232,13 +232,14 @@ def _post(port, path, body): @needs_ffmpeg -def test_last_frame_route_saves_a_picture_to_the_bin_once_and_flags_a_fade_to_black(comfy, server): +def test_last_frame_route_saves_a_picture_to_the_temp_folder_once_and_flags_a_fade_to_black(comfy, server): from core import media moov_at_end(comfy.output / "a.mp4", 4.0) proj = _project(comfy, frames=50, frames_mode="timeline") serving._frames.clear() status, a = _post(server, f"{BASE}/projects/{proj.id}/last-frame", {"scene_id": "s1"}) assert status == 200 and a["dark"] is False and media.path_for(a["media_id"]) + assert media.path_for(a["media_id"]).parent == comfy.temp and a["media_id"] not in [m["id"] for m in media.listing()] # a working file in temp, never in the user's bin assert _post(server, f"{BASE}/projects/{proj.id}/last-frame", {"scene_id": "s1"})[1]["media_id"] == a["media_id"] # same window: same file subprocess.run(["ffmpeg", "-y", "-f", "lavfi", "-i", "color=c=black:s=160x120:d=2:r=25", "-c:v", "libx264", "-pix_fmt", "yuv420p", str(comfy.output / "black.mp4")], check=True, capture_output=True) dark = _project(comfy, frames=50, frames_mode="timeline")