diff --git a/py/api/autotag_routes.py b/py/api/autotag_routes.py index f02c683b..bcd46f1d 100644 --- a/py/api/autotag_routes.py +++ b/py/api/autotag_routes.py @@ -44,6 +44,8 @@ BATCH_JOB_BODY_MAX = 32 * 1024 SKIPPED_PREVIEW_BODY_MAX = 64 * 1024 SKIPPED_PREVIEW_LEASE = "Skipped tags preview" SKIPPED_PREVIEW_SAMPLE = 12 +# WD14 label lists the preview found missing or unreadable (logged once each). +_MISSING_LABEL_LISTS = set() REVIEW_REQUIRED = ( "Replacing tags needs a review of each image. Use Preview tagging review, " "or choose Skip or Merge for a batch run." @@ -760,8 +762,11 @@ class AutotagRoutesMixin: try: names = wd14_label_names(path) except (OSError, ValueError) as exc: - self.logger.debug(f"No WD14 label list for {model_type}: {exc}") + if path not in _MISSING_LABEL_LISTS: # logged once per list + _MISSING_LABEL_LISTS.add(path) + self.logger.debug(f"No WD14 label list for {model_type}: {exc}") continue + _MISSING_LABEL_LISTS.discard(path) lists.append(MODELS[model_type]["name"]) for name in names: known.setdefault(name.casefold(), (1, name)) diff --git a/py/tagging_defaults.py b/py/tagging_defaults.py index e76d4aca..b1181372 100644 --- a/py/tagging_defaults.py +++ b/py/tagging_defaults.py @@ -204,8 +204,8 @@ def read_wd14_label_rows(path) -> List[Dict[str, str]]: def wd14_label_names(path) -> Tuple[str, ...]: """The names a WD14 label list can suggest (categories 0 and 4), cached - by ``(mtime_ns, size)``. OSError and ValueError propagate (the caller - skips a missing or unreadable list).""" + by ``(mtime_ns, size)``. OSError (missing) and ValueError (unreadable) + propagate; the caller skips that list.""" key = os.fspath(path) stat = os.stat(key) stamp = (stat.st_mtime_ns, stat.st_size) @@ -213,9 +213,13 @@ def wd14_label_names(path) -> Tuple[str, ...]: cached = _label_cache.get(key) if cached is not None and cached[0] == stamp: return cached[1] + try: + rows = read_wd14_label_rows(key) + except csv.Error as exc: # a damaged list (a NUL byte, an oversized field) + raise ValueError(f"unreadable label list: {exc}") from None names = tuple( name - for row in read_wd14_label_rows(key) + for row in rows if row.get("category", "0") in WD14_SUGGESTED_CATEGORIES and (name := (row.get("name") or "").strip()) ) diff --git a/tests/test_skipped_tags.py b/tests/test_skipped_tags.py index 9db3c68e..dbb4a9b1 100644 --- a/tests/test_skipped_tags.py +++ b/tests/test_skipped_tags.py @@ -5,6 +5,7 @@ reader and the filter in ``AutoTagService.generate_tag_suggestions``. No model is loaded: ``_scored_tags`` is replaced by a fixed list. """ +import csv import os import tempfile import unicodedata @@ -190,6 +191,15 @@ class TestWd14Labels(unittest.TestCase): self.assertEqual(wd14_label_names(self.path), ("blurry", "sky")) self.assertEqual(reader.call_count, 2) + def test_a_damaged_list_is_a_value_error_for_the_caller_to_skip(self): + # A field over the csv module's size limit raises csv.Error. + big = "x" * (csv.field_size_limit() + 1) + self.path.write_text( + f"tag_id,name,category,count\n1,{big},0,1\n", encoding="utf-8" + ) + with self.assertRaises(ValueError): + wd14_label_names(self.path) + def test_a_missing_list_raises_for_the_caller_to_skip(self): with self.assertRaises(OSError): wd14_label_names(self.path) diff --git a/web/js/workspace/system-skipped-tags.mjs b/web/js/workspace/system-skipped-tags.mjs index 3fb0e989..8d42d98a 100644 --- a/web/js/workspace/system-skipped-tags.mjs +++ b/web/js/workspace/system-skipped-tags.mjs @@ -107,7 +107,6 @@ export function skippedTagsBlock({ api, scope, drafts, delay = PREVIEW_DELAY_MS input.id = 'tag-skipped-input'; input.autocomplete = 'off'; input.placeholder = SKIPPED_COPY.placeholder; - input.maxLength = MAX_ENTRY_CHARS + 10; input.setAttribute('aria-describedby', 'tag-skipped-problem'); const add = createButton(SKIPPED_COPY.add); add.id = 'tag-skipped-add'; @@ -116,6 +115,7 @@ export function skippedTagsBlock({ api, scope, drafts, delay = PREVIEW_DELAY_MS problem.id = 'tag-skipped-problem'; problem.setAttribute('role', 'alert'); const chips = el('div', undefined, 'tags skip-chips'); + chips.setAttribute('role', 'group'); chips.setAttribute('aria-label', SKIPPED_COPY.chips); const preview = el('p', '', 'small skip-preview'); preview.setAttribute('role', 'status');