fix(characters): do not flag a character reusing its own picture across books

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-09-19 22:31:17 +02:00
parent 4ab345d1c1
commit a280ddacde
3 changed files with 12 additions and 1 deletions

View File

@ -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`. - **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 ### 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. - 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. - 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.

View File

@ -176,9 +176,12 @@ async def characters_visual_pair_review():
if form_id: if form_id:
report["form_id"], report["form_name"] = form_id, form_name report["form_id"], report["form_name"] = form_id, form_name
reports.append(report) 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(): for owners in digest_owners.values():
character_ids = {owner.rsplit(":", 1)[0] for owner in owners} 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: for owner in owners:
char_id = owner.rsplit(":", 1)[0] 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) report = next((item for item in reports if item["id"] == char_id and "form_id" not in item), None)

View File

@ -153,6 +153,13 @@ class VisualPairReviewTests(unittest.TestCase):
"forms": [{"id": "f", "name": "F", "physical": "x"}]}} "forms": [{"id": "f", "name": "F", "physical": "x"}]}}
self.assertEqual(self.review([rec])["issues"], []) 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): def test_the_same_asset_on_two_characters_is_flagged(self):
s = {"physical": "a"} s = {"physical": "a"}
recs = [{"id": f"c{i}", "name": f"N{i}", "image": _img(9), recs = [{"id": f"c{i}", "name": f"N{i}", "image": _img(9),