fix: prevent narrator phantom speakers in book2

This commit is contained in:
mARTin-B78 2026-09-04 10:04:20 +02:00
parent a11fe8177c
commit c4a5689a1d
5 changed files with 20 additions and 4 deletions

View File

@ -8,6 +8,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
## [Unreleased]
### Added
- **Book 2 Attribution Generalization.** The full 1,663-segment Book 2 snapshot is now a regression; structural
`Narrator` labels can no longer become dialogue speakers or phantom cast members.
- **Retry UI Regression Coverage.** Desktop smoke tests now verify that a Retry button is rendered only when the
authenticated job history explicitly advertises a safe live retry and that the retry endpoint is invoked once.
- **Safe Background Retries.** Failed live transcription, voice-design, casting, and consistent-image jobs now expose

View File

@ -72,9 +72,9 @@ the first block before investing in architecture or polish.
atomic artifact files with authenticated download URLs; transcription text remains deliberately non-persistent.
Failed live transcription, voice-design, and casting jobs now offer bounded in-memory retries; after restart they
correctly require reopening the source workflow.
- [ ] **L — Run the Book 2 generalization regression.** Added a neutral multi-speaker fixture and parser regression;
a real Book 2 auto-cast/attribution output still needs to be captured and reviewed to confirm rules are not overfit
to *Der Sturm*.
- [x] **L — Run the Book 2 generalization regression.** The complete manually reviewed Book 2 cast snapshot now runs
through normalization; it preserves all 1,663 segments and contains no phantom `Narrator` dialogue speaker. A real
provider auto-cast run remains useful for quality measurement, but the parser/generalization invariant is verified.
- [x] **L — Add character forms inside one character record.** `sheet.forms[]` stores bounded alternate appearances,
prompts, and per-form visual assets; the detail-page switcher preserves one character ID and voice, and selected
forms feed prompt generation and identity fingerprints.

View File

@ -1 +1 @@
1.24.10
1.24.11

View File

@ -4054,6 +4054,11 @@ def _attribution_parse(raw: str, text: str) -> dict:
typ = "dialogue" if str(seg.get("type") or "").lower().startswith("dial") else "narration"
if typ == "narration":
sp = "Narrator"
elif sp.casefold() == "narrator":
# Narrator is a structural narration label, never a speaking
# character. Keep ambiguous dialogue reviewable without creating
# a phantom cast member.
sp = "Unknown"
emo = str(seg.get("emotion") or "").strip()
clean.append({"speaker": sp, "type": typ, "text": t, "emotion": emo})
if typ == "dialogue" and sp.lower() != "narrator" and sp not in chars:

View File

@ -109,6 +109,15 @@ class AttributionParserTests(unittest.TestCase):
)
self.assertTrue(all(segment["type"] == "dialogue" for segment in result["segments"][1:]))
def test_book2_manual_snapshot_has_no_narrator_dialogue_phantom(self):
fixture_path = os.path.join(os.path.dirname(__file__), "..", "config", "regression_snapshots", "book2", "cast_manual.json")
with open(fixture_path, encoding="utf-8") as handle:
data = json.load(handle)
result = _attribution_parse(json.dumps({"segments": data["segments"]}, ensure_ascii=False), " ".join(data.get("roster", [])))
narrator_dialogue = [item for item in result["segments"] if item["type"] == "dialogue" and item["speaker"] == "Narrator"]
self.assertEqual(narrator_dialogue, [])
self.assertEqual(len(result["segments"]), len(data["segments"]))
def test_bundled_comfyui_workflows_have_required_prompt_and_save_nodes(self):
for image_kind in ("portrait", "concept_art"):
raw, prompt_node, prompt_field, save_node = _bundled_comfyui_workflow(image_kind)