fix(review): never queue an ambiguous file with nothing to choose
The maintainer's live queue held 39 items whose candidates all read
"unresolved" ("No positive / No negative"). An ambiguous result is now
reviewable only when at least two candidates are resolved with non-blank
positive text. With none the file could not be traced; with exactly one,
the untraced branches may still have made the file, so that text is not
certain either. Both are reported unresolved, never linked and never
queued (also the A1111 two-delimiter case, which has no candidates).
A rescan removes the pending items of such files whose own evidence
offers no choice, with the same safe delete as the linked settle
(pending, revision 0, no decision row). A limited item (over 16 candidates) that holds at least two
candidates is kept. The job scope gains review_untraced and the log
"= N images left Needs review · nothing to choose".
This commit is contained in:
+30
-10
@@ -44,6 +44,7 @@ try:
|
||||
parse_page_json,
|
||||
)
|
||||
from ...utils.prompt_pair_resolver import resolve_prompt_pair
|
||||
from ...utils.review_capture import reviewable
|
||||
except ImportError:
|
||||
from database.operations import PromptDatabase
|
||||
from database.pair_ingestion import ingest_resolved_pair
|
||||
@@ -54,6 +55,7 @@ except ImportError:
|
||||
parse_page_json,
|
||||
)
|
||||
from utils.prompt_pair_resolver import resolve_prompt_pair
|
||||
from utils.review_capture import reviewable
|
||||
|
||||
|
||||
try:
|
||||
@@ -214,8 +216,9 @@ def _time_left(done, total, batches, elapsed):
|
||||
def new_review_counts():
|
||||
"""The scan's Needs review tally: ``added`` new items, ``queued`` files
|
||||
in the queue after this scan (new or already there), ``settled`` items
|
||||
removed because their file is linked now, and ``errors``."""
|
||||
return {"added": 0, "queued": 0, "settled": 0, "errors": 0}
|
||||
removed because their file is linked now, ``untraced`` items removed
|
||||
because they offer nothing to choose, and ``errors``."""
|
||||
return {"added": 0, "queued": 0, "settled": 0, "untraced": 0, "errors": 0}
|
||||
|
||||
|
||||
def _review_scope(review):
|
||||
@@ -229,6 +232,7 @@ def _review_scope(review):
|
||||
"review_items": review["queued"],
|
||||
"review_added": review["added"],
|
||||
"review_settled": review["settled"],
|
||||
"review_untraced": review["untraced"],
|
||||
"review_errors": review["errors"],
|
||||
}
|
||||
|
||||
@@ -1612,7 +1616,9 @@ class AdminRoutesMixin:
|
||||
whose final outcome is ambiguous and unlinked (the resolver found it
|
||||
ambiguous and the fallback did not link it) is recorded for Needs
|
||||
review (best effort, counted in ``review`` when given); one a saved
|
||||
row already links is reported ``unchanged`` and never queued.
|
||||
row already links is reported ``unchanged`` and never queued. An
|
||||
ambiguous file with nothing to choose is reported ``unresolved`` and
|
||||
its pending items without a choice leave the queue.
|
||||
"""
|
||||
counts = dict.fromkeys(SCAN_COUNT_KEYS, 0)
|
||||
|
||||
@@ -1624,10 +1630,14 @@ class AdminRoutesMixin:
|
||||
if self._scan_job and self._scan_job.cancelled:
|
||||
break
|
||||
counts["processed"] += 1
|
||||
ambiguous = []
|
||||
ambiguous, untraced = [], []
|
||||
try:
|
||||
outcome = self._ingest_scanned_file(
|
||||
media_file, metadata, detailed=True, ambiguous=ambiguous
|
||||
media_file,
|
||||
metadata,
|
||||
detailed=True,
|
||||
ambiguous=ambiguous,
|
||||
untraced=untraced,
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error processing {media_file.name}: {e}")
|
||||
@@ -1639,6 +1649,8 @@ class AdminRoutesMixin:
|
||||
outcome = "unchanged" # already linked: not a review case
|
||||
else:
|
||||
self._record_scanned_review(media_file, roots, review)
|
||||
elif untraced and outcome == "unresolved":
|
||||
self._settle_untraced_reviews(untraced, roots, review)
|
||||
if outcome is None:
|
||||
note(media_file, None)
|
||||
continue
|
||||
@@ -1673,7 +1685,7 @@ class AdminRoutesMixin:
|
||||
return positive.strip() or None
|
||||
|
||||
def _ingest_scanned_file(
|
||||
self, media_file, metadata, detailed=False, ambiguous=None
|
||||
self, media_file, metadata, detailed=False, ambiguous=None, untraced=None
|
||||
):
|
||||
"""Enrich exact pairs; retain legacy positive-only import for absent pairs.
|
||||
|
||||
@@ -1683,7 +1695,10 @@ class AdminRoutesMixin:
|
||||
the pair counts. When the resolver finds the file ambiguous (whether
|
||||
or not the browser parser then links it) and ``ambiguous`` is a list,
|
||||
``media_file`` is appended to it; only a file that then returns
|
||||
``"ambiguous"`` (nothing linked it) is a Needs review case.
|
||||
``"ambiguous"`` (nothing linked it) is a Needs review case. An
|
||||
ambiguous result with nothing to choose (``reviewable`` is False: fewer
|
||||
than two candidates carry a prompt) that nothing links returns
|
||||
``"unresolved"`` instead, and ``media_file`` goes to ``untraced``.
|
||||
"""
|
||||
if not metadata:
|
||||
return None
|
||||
@@ -1699,7 +1714,12 @@ class AdminRoutesMixin:
|
||||
return None
|
||||
_with_file_size(canonical, media_file)
|
||||
resolution = resolve_prompt_pair(canonical)
|
||||
if resolution["state"] == "ambiguous" and ambiguous is not None:
|
||||
# Ambiguous without a choice: unresolved unless something links it.
|
||||
unchoosable = resolution["state"] == "ambiguous" and not reviewable(resolution)
|
||||
if unchoosable:
|
||||
if untraced is not None:
|
||||
untraced.append(media_file)
|
||||
elif resolution["state"] == "ambiguous" and ambiguous is not None:
|
||||
ambiguous.append(media_file)
|
||||
if resolution["state"] == "resolved":
|
||||
result = ingest_resolved_pair(
|
||||
@@ -1720,9 +1740,9 @@ class AdminRoutesMixin:
|
||||
# Graphs without a recognized sampler keep the legacy extractor below.
|
||||
prompt_text = self._graph_prompt_text(metadata)
|
||||
if not prompt_text:
|
||||
return resolution["state"]
|
||||
return "unresolved" if unchoosable else resolution["state"]
|
||||
elif resolution["state"] == "ambiguous":
|
||||
return "ambiguous"
|
||||
return "unresolved" if unchoosable else "ambiguous"
|
||||
elif resolution["state"] == "unresolved" and not isinstance(
|
||||
canonical.get("parameters"), str
|
||||
):
|
||||
|
||||
@@ -5,13 +5,18 @@ final outcome is ambiguous and unlinked. The strict resolver must find it
|
||||
``ambiguous``, the scan's legacy fallback (the browser parser,
|
||||
``_graph_prompt_text``) must not link it, and no saved row may link it
|
||||
already. A file the fallback links, an exact pair the ingester stored and an
|
||||
already linked file are never queued.
|
||||
already linked file are never queued. Neither is an ambiguous file with
|
||||
nothing to choose: fewer than two of its candidates carry a prompt
|
||||
(``utils.review_capture.reviewable``); the scan reports it unresolved ("could
|
||||
not be traced").
|
||||
|
||||
The queue heals itself: a pending item whose file is now linked (by the
|
||||
fallback, a later resolution or the monitor) is removed by the next scan
|
||||
that lists the file or by the monitor's next pass over it. Only an
|
||||
undecided item goes (``pending``, revision 0, no decision row); a decision
|
||||
is never deleted or changed (``settle_linked_review_items``).
|
||||
that lists the file or by the monitor's next pass over it, and so is a
|
||||
pending item without a choice whose file is still ambiguous without one
|
||||
(``settle_untraced_review_items``). Only an undecided item goes
|
||||
(``pending``, revision 0, no decision row); a decision is never deleted or
|
||||
changed (``settle_linked_review_items``).
|
||||
|
||||
The scan records through ``_record_scanned_review`` (best effort: a failure
|
||||
is logged and counted); the monitor calls ``record_review_file`` from its
|
||||
@@ -42,6 +47,9 @@ try:
|
||||
except ImportError:
|
||||
from utils.review_capture import record_ambiguous_file
|
||||
|
||||
# The scan log's reason for items that leave the queue with nothing to choose.
|
||||
NOTHING_TO_CHOOSE = "nothing to choose"
|
||||
|
||||
|
||||
class ReviewRecordingMixin:
|
||||
"""Record the scan's and the monitor's ambiguous files for Needs review."""
|
||||
@@ -49,7 +57,7 @@ class ReviewRecordingMixin:
|
||||
def _record_scanned_review(self, media_file, roots, review=None):
|
||||
"""Record one ambiguous, unlinked scanned file (logged and counted)."""
|
||||
try:
|
||||
outcome = self._record_review_sync(media_file, roots)
|
||||
outcome = self._record_review_sync(media_file, roots, review)
|
||||
except Exception:
|
||||
self.logger.warning(
|
||||
f"{media_file.name} could not be added to Needs review",
|
||||
@@ -65,15 +73,22 @@ class ReviewRecordingMixin:
|
||||
if outcome in QUEUED_OUTCOMES:
|
||||
review["queued"] += 1
|
||||
|
||||
def _record_review_sync(self, media_file, roots):
|
||||
"""``record_ambiguous_file`` for a file under one of ``roots`` (blocking)."""
|
||||
def _record_review_sync(self, media_file, roots, review=None):
|
||||
"""``record_ambiguous_file`` for a file under one of ``roots`` (blocking).
|
||||
|
||||
An ``untraced`` file (its captured bytes offer nothing to choose) also
|
||||
loses its pending items that offer nothing either.
|
||||
"""
|
||||
located = self._review_location(media_file, roots)
|
||||
if located is None:
|
||||
return "skipped"
|
||||
root, index, relative, _spelled = located
|
||||
return record_ambiguous_file(
|
||||
outcome = record_ambiguous_file(
|
||||
self.db, self._capture_store(), Path(root), index, relative
|
||||
)
|
||||
if outcome == "untraced":
|
||||
self._settle_untraced_reviews([media_file], roots, review)
|
||||
return outcome
|
||||
|
||||
@staticmethod
|
||||
def _review_location(media_file, roots, real_roots=None):
|
||||
@@ -129,6 +144,33 @@ class ReviewRecordingMixin:
|
||||
review["settled"] += settled
|
||||
return settled
|
||||
|
||||
def _settle_untraced_reviews(self, media_files, roots, review=None):
|
||||
"""Remove the pending items without a choice of untraced ``media_files``.
|
||||
|
||||
``media_files`` are files found ambiguous with fewer than two
|
||||
candidates that carry a prompt. Blocking; best effort like the linked
|
||||
settle (a failure is logged and counted in ``review["errors"]``).
|
||||
Returns how many items left.
|
||||
"""
|
||||
locators = []
|
||||
for media_file in media_files:
|
||||
located = self._review_location(media_file, roots)
|
||||
if located is not None:
|
||||
locators.append(os.path.normcase(located[3]))
|
||||
try:
|
||||
removed = self.db.settle_untraced_review_items(locators) if locators else 0
|
||||
except Exception:
|
||||
self.logger.warning(
|
||||
"Needs review items without a choice could not be removed",
|
||||
exc_info=True,
|
||||
)
|
||||
if review is not None:
|
||||
review["errors"] += 1
|
||||
return 0
|
||||
if review is not None:
|
||||
review["untraced"] += removed
|
||||
return removed
|
||||
|
||||
def _fallback_links(self, metadata):
|
||||
"""True when the scan's legacy fallback would link these records."""
|
||||
return bool(metadata.get("prompt")) and bool(self._graph_prompt_text(metadata))
|
||||
@@ -140,7 +182,9 @@ class ReviewRecordingMixin:
|
||||
current root is ``skipped``. A linked file is never queued: its
|
||||
pending items are removed and the answer is ``linked``. An unlinked
|
||||
file the scan's fallback would link is ``skipped`` (the next scan
|
||||
links it); any other file is recorded when it is ambiguous. While a
|
||||
links it); any other file is recorded when it is ambiguous with a
|
||||
choice, and is ``untraced`` (its pending items without a choice
|
||||
removed) when it is ambiguous without one. While a
|
||||
job runs that a ``records`` job may not run beside, nothing is written
|
||||
and the answer is ``deferred``. Errors propagate: the monitor logs
|
||||
them and keeps its link.
|
||||
@@ -183,6 +227,12 @@ class ReviewRecordingMixin:
|
||||
f"{SCAN_LOG_PREFIXES['known']} "
|
||||
f"{_plural(review['settled'], 'image')} left Needs review · now linked"
|
||||
)
|
||||
if review["untraced"]:
|
||||
job.log_line(
|
||||
f"{SCAN_LOG_PREFIXES['known']} "
|
||||
f"{_plural(review['untraced'], 'image')} left Needs review · "
|
||||
f"{NOTHING_TO_CHOOSE}"
|
||||
)
|
||||
if review["errors"]:
|
||||
job.log_line(
|
||||
f"{SCAN_LOG_PREFIXES['errors']} "
|
||||
|
||||
Reference in New Issue
Block a user