diff --git a/app/ui/features/training/data.js b/app/ui/features/training/data.js index 0becfd74..6e4240fc 100644 --- a/app/ui/features/training/data.js +++ b/app/ui/features/training/data.js @@ -94,12 +94,12 @@ export function dataStep({ app, session, go, ui }) { function showRated() { const all = shownRated(); for (const k of [...ratedPicked]) if (!all.some((r) => keyOf(r) === k)) ratedPicked.delete(k); - ratedGallery.setItems(all.map((r) => ({ id: keyOf(r), label: r.media.filename, thumb: stillOf({ src: { kind: "output", ...r.media }, media_kind: r.media.filename.match(/\.(mp4|webm|mov|mkv|avi)$/i) ? "video" : "image" }), badge: [r.label, clipWords(ui.clips[keyOf(r)])].filter(Boolean).join(" · ") }))); + ratedGallery.setItems(all.map((r) => ({ id: keyOf(r), label: r.media.filename, thumb: stillOf({ src: { kind: "output", ...r.media }, media_kind: r.media.filename.match(/\.(mp4|webm|mov|mkv|avi)$/i) ? "video" : "image" }), badge: [r.label, r.doubtful ? "old rating?" : "", clipWords(ui.clips[keyOf(r)])].filter(Boolean).join(" · ") }))); ui.needClips(all.filter((r) => /\.(mp4|webm|mov|mkv|avi)$/i.test(r.media.filename)).map((r) => ({ kind: "output", ...r.media })), () => showRated()); ratedGallery.setValue([...ratedPicked]); const n = all.length, p = ratedPicked.size; - const other = ui.rated.different || 0; - const left = other ? ` ${other} left out: the file with that name is another video than the one rated (names repeat between ComfyUI instances).` : ""; + const other = (ui.rated.results || []).filter((r) => r.doubtful).length; + const left = other ? ` ${other} marked "old rating?": the rating may belong to another video that had this name on another ComfyUI instance. Its caption is still the prompt of the file you see.` : ""; ratedNote.setText((n ? `${n} rated result${n > 1 ? "s" : ""} found in your projects. ${p ? `${p} picked: only those are added.` : "None picked: all are added."}` : "None found yet. Rate results in your projects and they appear here.") + left); addRated.node.disabled = !n || !open; } diff --git a/app/ui/features/training/training.test.js b/app/ui/features/training/training.test.js index e62cca6b..daa285db 100644 --- a/app/ui/features/training/training.test.js +++ b/app/ui/features/training/training.test.js @@ -491,15 +491,15 @@ test("Results: a run shows the time per step and the time spent in all, restarts off(); }); -test("Data step: results left out because the file is another video are counted, not silent", async () => { +test("Data step: results whose rating may be from another video are marked, not hidden", async () => { const f = fake(); - f.api.trainRated = async () => ({ results: [{ label: "liked", media: { type: "output", subfolder: "", filename: "x.mp4" }, caption: "a red fox" }], counts: { liked: 1 }, different: 3 }); + f.api.trainRated = async () => ({ results: [{ label: "liked", media: { type: "output", subfolder: "", filename: "x.mp4" }, caption: "a red fox", doubtful: true }, { label: "liked", media: { type: "output", subfolder: "", filename: "y.mp4" }, caption: "a cat" }], counts: { liked: 2 }, doubtful: 1 }); const { button, off } = await open(f); button.click(); await wait(60); const rated = [...document.querySelectorAll("button")].find((b) => /rated|results/i.test(b.textContent) && /Add|Use|Open/i.test(b.textContent)); if (rated) rated.click(); await wait(60); - assert.ok(text().includes("3 left out: the file with that name is another video"), "the count is shown"); + assert.ok(text().includes('1 marked "old rating?"'), "the count is shown, and nothing is hidden"); off(); }); diff --git a/modules/training/lora/__init__.py b/modules/training/lora/__init__.py index b14e96c0..fa49911d 100644 --- a/modules/training/lora/__init__.py +++ b/modules/training/lora/__init__.py @@ -146,19 +146,17 @@ def routes(table, base, web): roots = train.roots() here = [(r, store.resolve({"kind": "output", **r["media"]}, roots, train.bin_path)) for r in found] here = [(r, path) for r, path in here if path] # a rating is not a video: only results still on this machine - mine = [(r, path) for r, path in here if rated.is_this_video(r, path)] # ...and the same video: a project carried to another instance meets other files under the same names - different = len(here) - len(mine) found = [] - for r, path in mine: - made_from = rated.embedded_prompt(path) # the file's own prompt beats what the project kept for it - if made_from: - r = {**r, "prompt_text": made_from, "caption": rated.prompt_caption(made_from, doc_of.get(r["project"]), parts, lambda t: shortcuts.expand(t, seed=1), shortcuts.resolve_variables)} - found.append(r) + for r, path in here: + doubtful = not rated.is_this_video(r, path) # the file may be another video that took the name of one rated on another instance: shown, but marked + made_from = rated.embedded_prompt(path) # only the file's own prompt: what the project kept under this name may belong to another video + r = {**r, "prompt_text": made_from, "caption": rated.prompt_caption(made_from, doc_of.get(r["project"]), parts, lambda t: shortcuts.expand(t, seed=1), shortcuts.resolve_variables)} + found.append({**r, "doubtful": doubtful}) want = req.rel_url.query.get("label") if want: found = [r for r in found if r["label"] == want] counts = {label: sum(1 for r in found if r["label"] == label) for label in rated.LABEL_TITLES} - return web.json_response({"results": found, "counts": counts, "different": different}) + return web.json_response({"results": found, "counts": counts, "doubtful": sum(1 for r in found if r["doubtful"])}) @table.post(base + "/clips") async def _clips(req): diff --git a/modules/training/lora/tests/test_plan_and_routes.py b/modules/training/lora/tests/test_plan_and_routes.py index 3672be2d..b3391528 100644 --- a/modules/training/lora/tests/test_plan_and_routes.py +++ b/modules/training/lora/tests/test_plan_and_routes.py @@ -211,7 +211,7 @@ def test_a_file_made_from_another_prompt_is_not_the_one_that_was_rated(tmp_path) assert rated.same_video(caption, tmp_path / "gone.png"), "unreadable: not judged" -def test_the_rated_list_leaves_out_files_that_are_other_videos_and_says_how_many(tmp_path, monkeypatch): +def test_the_rated_list_keeps_every_file_but_marks_those_that_may_be_other_videos(tmp_path, monkeypatch): from aiohttp import web from aiohttp.test_utils import TestClient, TestServer from core import config, projects @@ -238,7 +238,7 @@ def test_the_rated_list_leaves_out_files_that_are_other_videos_and_says_how_many async with TestClient(TestServer(app)) as c: return await (await c.get("/m/rated")).json() got = asyncio.run(go()) - assert [r["media"]["filename"] for r in got["results"]] == ["same.png"] and got["different"] == 1 and got["counts"]["liked"] == 1 + assert [(r["media"]["filename"], r["doubtful"]) for r in got["results"]] == [("same.png", False), ("clash.png", True)] and got["doubtful"] == 1 and got["counts"]["liked"] == 2 def test_a_file_added_without_words_gets_the_prompt_it_was_made_from(tmp_path, monkeypatch): @@ -274,7 +274,7 @@ def test_a_file_added_without_words_gets_the_prompt_it_was_made_from(tmp_path, m assert "typed by hand" in [i["caption"] for i in items] and "" in [i["caption"] for i in items], "no prompt in the file: left empty; a caption already there: kept" -def test_a_rated_result_made_in_this_session_is_captioned_from_its_own_file_and_an_older_rating_on_a_reused_name_is_left_out(tmp_path, monkeypatch): +def test_a_rated_result_is_captioned_from_its_own_file_and_an_older_rating_on_a_reused_name_is_marked(tmp_path, monkeypatch): import os import time from aiohttp import web @@ -286,9 +286,12 @@ def test_a_rated_result_made_in_this_session_is_captioned_from_its_own_file_and_ _png_with_prompt(tmp_path / "now.png", ["a red fox runs through the snow"], "FunPackEnhancePrompt") _png_with_prompt(tmp_path / "reused.png", ["a city street at night"], "FunPackEnhancePrompt") os.utime(tmp_path / "now.png", ((now - 60_000) / 1000,) * 2) - doc = {"scenes": [{"id": "a", "rating": "10", "rated": {"text": "something the project kept that is wrong", "at": now}}, + _png_with_prompt(tmp_path / "bare.png", [], "FunPackEnhancePrompt") + os.utime(tmp_path / "bare.png", ((now - 60_000) / 1000,) * 2) + doc = {"scenes": [{"id": "c", "rating": "10", "rated": {"text": "the project's words", "at": now}}, {"id": "a", "rating": "10", "rated": {"text": "something the project kept that is wrong", "at": now}}, {"id": "b", "rating": "10", "rated": {"text": "a red fox runs through the snow", "at": now - 86_400_000}}], - "scene_renders": {"a": {"media": {"type": "output", "subfolder": "", "filename": "now.png"}}, + "scene_renders": {"c": {"media": {"type": "output", "subfolder": "", "filename": "bare.png"}}, + "a": {"media": {"type": "output", "subfolder": "", "filename": "now.png"}}, "b": {"media": {"type": "output", "subfolder": "", "filename": "reused.png"}}}} class Project: @@ -306,5 +309,7 @@ def test_a_rated_result_made_in_this_session_is_captioned_from_its_own_file_and_ async with TestClient(TestServer(app)) as c: return await (await c.get("/m/rated")).json() got = asyncio.run(go()) - assert [(r["media"]["filename"], r["caption"]) for r in got["results"]] == [("now.png", "a red fox runs through the snow")] - assert got["different"] == 1 + assert {r["media"]["filename"]: r["caption"] for r in got["results"]}["bare.png"] == "", "a file with no prompt of its own gets none: the project's words are never used" + got["results"] = [r for r in got["results"] if r["media"]["filename"] != "bare.png"] + assert [(r["media"]["filename"], r["caption"], r["doubtful"]) for r in got["results"]] == [("now.png", "a red fox runs through the snow", False), ("reused.png", "a city street at night", True)] + assert got["doubtful"] == 1