fix(tagging): legacy pages tag each image, not its prompt (#379, PR 2) (#394)

* fix(tagging): Tag output images tags each image, not its prompt (#379, PR 1)

The native "Tag output images" job now runs the image-tags loop over every
still image the output folders list, linked or not: each image gets its own
model tags (at most "Suggestions per image") in its image tag set, and its
bytes are marked auto-tagged in a new media_image_autotags table. No prompt
tag or prompt text is written.

- database/autotag_schema.py: one forward-only WITHOUT ROWID table, optional
  in old backups, refused without the annotation group.
- add_image_tags/replace_image_tags take mark_autotagged; autotag_state and
  autotagged_locators read the marks through the one current-record rule.
- "Skip already tagged" skips images whose current bytes are marked, counted
  in a stat-only pre-pass; hand tags do not count.
- The review counts images and estimates the time (WD14 on CPU or CUDA).
- System > Auto-tagging review rows and copy, D134 and the Auto-tagging
  contract follow.

Part of #379

* fix(tagging): end the batch job after its cleanup and report pre-pass progress (#379)

The job read as ended while it still held the model gate, so a quick
restart could be refused with model-busy. Pre-pass chunks now publish
progress, and a cancel between chunks keeps exact counts.

Part of #379

* fix(tagging): measured GPU rate, and an ended image-tags job shows no time left (#379)

The live check measured WD14 on CUDA at 0.167 s per image (100 images in
16.7 s with the model load), so the review estimate uses 0.17 s. A finished
or cancelled Auto-tag/Add tags job kept "Processing file N of N · about X
min left"; it now ends with "<verb> <state> · N files processed".

Part of #379

* fix(tagging): legacy pages tag each image, not its prompt (#379, PR 2)

The admin and gallery batch starts the shared Tag output images job
("Tag output images (legacy page)") and relays its progress; closing the
window cancels it. Review asks per image with Skip first and applies by
image and sha256; apply refuses a prompt_id body. The Tag image dialog
marks an image auto-tagged only after Generate suggestions on that
capture. A second legacy batch (also one that wins the start race)
answers JSON 409, and both pages now show that sentence.

* test(tagging): gallery review units apply by image; record each progress change

The gallery review unit tests still pinned the removed prompt_id apply.
The legacy batch spec read the progress text once per observer callback,
so a phase frame and the first image frame landing in one task lost the
phase (WebKit); it now records every mutation record.
This commit is contained in:
Vito
2026-10-27 22:40:57 +00:00
committed by GitHub
parent 2f8b7cb306
commit d11b354ed6
21 changed files with 1622 additions and 1022 deletions
+20
View File
@@ -296,6 +296,7 @@ class AnnotationRoutesMixin:
self._captures_closed = True
unsubscribe, self._captures_unsubscribe = self._captures_unsubscribe, None
store, self._captures = self._captures, None
self._suggested_tokens = set()
if unsubscribe is not None:
unsubscribe()
if store is not None:
@@ -515,6 +516,7 @@ class AnnotationRoutesMixin:
store = self._captures
if store is not None and _TOKEN.fullmatch(token) is not None:
store.release(token)
self._suggested_tokens.discard(token)
return web.json_response(
{"success": True}, headers={"Cache-Control": "no-store"}
)
@@ -564,6 +566,9 @@ class AnnotationRoutesMixin:
return _coded("capture-expired", _OTHER_FILE)
if facts.sha256 != capture.sha256:
return _coded("file-changed")
# A save after Generate suggestions ran on this capture marks the
# image auto-tagged (#379), so a Skip batch leaves the review alone.
suggested = token in self._suggested_tokens
try:
receipt = await self._run_in_executor(
self.db.replace_image_tags,
@@ -574,6 +579,7 @@ class AnnotationRoutesMixin:
expected_epoch=expected_epoch,
expected_tag_revision=revision,
tags=tags,
mark_autotagged=suggested,
)
except AnnotationCommandError as exc:
current = exc.current or {}
@@ -590,10 +596,23 @@ class AnnotationRoutesMixin:
except Exception:
self.logger.exception("Image tag save failed")
return _error(500, "save-failed", "The tags could not be saved.")
# Dropped only after a saved or replayed receipt: a retry after a
# tag-conflict still marks.
self._suggested_tokens.discard(token)
return web.json_response(
{"success": True, **receipt}, headers={"Cache-Control": "no-store"}
)
def _remember_suggested(self, store, token: str) -> None:
"""Note that Generate ran on *token*; forget tokens whose capture is
gone. ``expires_in`` does not reset a capture's idle clock."""
self._suggested_tokens = {
kept
for kept in self._suggested_tokens
if store.expires_in(kept) is not None
}
self._suggested_tokens.add(token)
# -- suggest ---------------------------------------------------------------
async def suggest_image_tags(self, request: web.Request) -> web.Response:
@@ -675,6 +694,7 @@ class AnnotationRoutesMixin:
finally:
if not started:
store.release_use(token)
self._remember_suggested(store, token)
return web.json_response(
{
"success": True,