From a280ddacde03b6d234d07cb3a374d66419549471 Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Sat, 19 Sep 2026 22:31:17 +0200 Subject: [PATCH] fix(characters): do not flag a character reusing its own picture across books Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 1 + routes/characters.py | 5 ++++- tests/test_character_identity.py | 7 +++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e3dd87..098063d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi - **Verify / recast for span-mode casts** (`audiobookVerifySpans`): the app's Recast unknown, 2nd Quality Run and auto-repair entry points now run span-natively for a span cast instead of the classic passes that re-emit text. **Verify is advisory**: one extra differently-windowed pass (~10 min per novel) that never changes a speaker — agreeing lines are marked `agreed`, disagreeing lines keep the first answer, are marked `disputed` and record the other pass's answer as `altSpeaker`; the confidence score reflects both. **Recast unknown** re-asks only the Unknown lines with reasoning and fuller context (`attribute_only` on `/api/attribute-spans`) and leaves any line the model cannot place Unknown. Segments now carry a `spanId`. ### Fixed +- **Visual-pair review no longer flags the same character reusing its picture in a later book** (`asset_reused_across_characters` now fires only when *different names* share one image). On the real library this removed 6 of 14 false-alarm groups (Darrag, Himgi, Lysandra, Marcian, Nyrilla, Narrator); the remaining ones are aliases or mix-ups worth a look (e.g. Alrik/Ritter/Zerwas share one image). - Span cast and verify now send the model **warm-up request** first (as the classic engine does), so a cold-booting model server no longer times out the first passage into tag guessing. - The cast view's **2.4 Verify all characters** now reaches the advisory second pass for span casts (it previously passed only `repairNarration`, which would have run the Unknown-only recast); its tooltip describes what it does for a span cast. diff --git a/routes/characters.py b/routes/characters.py index 4b17910..35fc26e 100644 --- a/routes/characters.py +++ b/routes/characters.py @@ -176,9 +176,12 @@ async def characters_visual_pair_review(): if form_id: report["form_id"], report["form_name"] = form_id, form_name reports.append(report) + names_by_id = {r.get("id"): re.sub(r"\s+", " ", str(r.get("name") or "")).strip().lower() for r in records} for owners in digest_owners.values(): character_ids = {owner.rsplit(":", 1)[0] for owner in owners} - if len(character_ids) > 1: + # The same character carried into a later book legitimately keeps its picture; only + # different names sharing one image (a copy/paste or mix-up) is worth reviewing. + if len({names_by_id.get(cid) for cid in character_ids}) > 1: for owner in owners: char_id = owner.rsplit(":", 1)[0] report = next((item for item in reports if item["id"] == char_id and "form_id" not in item), None) diff --git a/tests/test_character_identity.py b/tests/test_character_identity.py index ce20312..9938d10 100644 --- a/tests/test_character_identity.py +++ b/tests/test_character_identity.py @@ -153,6 +153,13 @@ class VisualPairReviewTests(unittest.TestCase): "forms": [{"id": "f", "name": "F", "physical": "x"}]}} self.assertEqual(self.review([rec])["issues"], []) + def test_the_same_character_reusing_its_picture_in_another_book_is_not_flagged(self): + s = {"physical": "a"} + recs = [{"id": f"b{i}::darrag", "book": f"Book {i}", "name": "Darrag", "image": _img(9), + "sheet": {**s, **_pair_meta(s), "concept_art_image": _img(20 + i)}} for i in range(2)] + recs[1]["name"] = " darrag " + self.assertEqual(self.review(recs)["issues"], []) + def test_the_same_asset_on_two_characters_is_flagged(self): s = {"physical": "a"} recs = [{"id": f"c{i}", "name": f"N{i}", "image": _img(9),