diff --git a/TESTING.md b/TESTING.md index 2e3b7184..fbf4c325 100644 --- a/TESTING.md +++ b/TESTING.md @@ -1639,6 +1639,9 @@ targets of the longest legal path (1024 code points, even escaped as `\uXXXX`) f unchanged, `-` skipped and `!` error. - The progress line is the scan's: "Processing file N of M · about X min left", the estimate (D56's `_time_left`) shown once two files are done, each file one batch. + An ended job never keeps it: its status line becomes " + · N files processed" (or the storage-restored sentence), + pinned by `test_an_ended_job_shows_no_time_left` (#379 live check). - Download report saves `prompt-manager-image-tags-.json`. - The start toasts are "Tagging started." / "Auto-tagging started." (D85); the end toasts follow the design's pattern with the job's counts, as the scan's: " diff --git a/py/api/image_tag_jobs.py b/py/api/image_tag_jobs.py index 4b670f32..09aeb874 100644 --- a/py/api/image_tag_jobs.py +++ b/py/api/image_tag_jobs.py @@ -669,11 +669,15 @@ class ImageTagJobsMixin: job.finish("failed") return if not await self._tag_each_file(run): + job.update(status_line=STORAGE_RESTORED) job.log_line(f"{verb} stopped · {STORAGE_RESTORED}") job.finish("failed") return state = "cancelled" if job.cancelled else "finished" - job.log_line(f"{verb} {state} · {_plural(job.processed, 'file')} processed") + # An ended job never keeps the loop's "about X min left" (#379). + ended = f"{verb} {state} · {_plural(job.processed, 'file')} processed" + job.update(status_line=ended) + job.log_line(ended) job.publish({"type": state, "progress": job.progress}) job.finish(state) finally: diff --git a/py/tagging_defaults.py b/py/tagging_defaults.py index b9c35f42..575a65af 100644 --- a/py/tagging_defaults.py +++ b/py/tagging_defaults.py @@ -14,14 +14,14 @@ MODEL_TYPES = WD14_MODEL_TYPES | LLM_MODEL_TYPES MAX_INSTRUCTION_CHARS = 4000 MAX_INSTRUCTION_BYTES = 16 * 1024 # Tag output images' review estimate (#379): seconds per image for the WD14 -# engines, by where ONNX Runtime runs them. The processor figure is an upper -# bound from the maintainer's cancelled CPU run (19 min / 697 images, backup, -# model load and skips included). The graphics card figure is provisional -# until PR 1's manual check measures Auto-tag selected on 100 images with CUDA; -# it is corrected there when the measurement is off by more than a quarter. -# The language-model engines have no figure. +# engines, by where ONNX Runtime runs them. The graphics card figure was +# measured in PR 1's live check (RTX 4090, CUDA: Auto-tag selected on 100 +# images in 16.7 s with the model load, 0.167 s/image). The processor figure +# is an upper bound from the maintainer's cancelled CPU run (19 min / 697 +# images, backup, model load and skips included), kept as the fallback. The +# language-model engines have no figure. WD14_SECONDS_PER_IMAGE = 1.6 -WD14_GPU_SECONDS_PER_IMAGE = 0.25 +WD14_GPU_SECONDS_PER_IMAGE = 0.17 def wd14_seconds_per_image(device: str) -> float: diff --git a/tests/test_image_tag_jobs.py b/tests/test_image_tag_jobs.py index 04d0dde5..25943b81 100644 --- a/tests/test_image_tag_jobs.py +++ b/tests/test_image_tag_jobs.py @@ -690,6 +690,32 @@ class TestFixRound(ImageTagJobTestCase): ], ) + async def test_an_ended_job_shows_no_time_left(self): + """#379 live check: a finished or cancelled job never says "min left".""" + for index in range(3): + self.png(f"f{index}.png", color=(index, 7, 7)) + clock = iter([0.0, 0.0, 120.0, 240.0]) + with patch( + "py.api.image_tag_jobs._monotonic", side_effect=lambda: next(clock, 240.0) + ): + job = await self.run_job(self.auto_body([f"f{i}.png" for i in range(3)])) + self.assertEqual(job.state, "finished") + self.assertEqual(job.status_line, "Auto-tag finished · 3 files processed") + + self.png("g0.png", color=(1, 1, 1)) + self.png("g1.png", color=(2, 2, 2)) + entered, proceed = threading.Event(), threading.Event() + self.tagger.block = (entered, proceed) + job = await self.start_ok(self.auto_body(["g0.png", "g1.png"])) + await self.wait_for(entered) + await self.client.post(f"/prompt_manager/jobs/{job.id}/cancel") + self.tagger.block = None + proceed.set() + await asyncio.wait_for(asyncio.shield(job.task), WAIT) + self.assertEqual(job.state, "cancelled") + self.assertEqual(job.status_line, "Auto-tag cancelled · 1 file processed") + self.assertNotIn("left", job.status_line) + def maximal_target(self, number): """A distinct target whose path is 1024 non-ASCII code points long.""" head = f"{number:03d}/" + ("界" * 200 + "/") * 5