fix: prevent narrator phantom speakers in book2
This commit is contained in:
parent
a11fe8177c
commit
c4a5689a1d
@ -8,6 +8,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
### 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
|
- **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.
|
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
|
- **Safe Background Retries.** Failed live transcription, voice-design, casting, and consistent-image jobs now expose
|
||||||
|
|||||||
6
TODO.md
6
TODO.md
@ -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.
|
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
|
Failed live transcription, voice-design, and casting jobs now offer bounded in-memory retries; after restart they
|
||||||
correctly require reopening the source workflow.
|
correctly require reopening the source workflow.
|
||||||
- [ ] **L — Run the Book 2 generalization regression.** Added a neutral multi-speaker fixture and parser regression;
|
- [x] **L — Run the Book 2 generalization regression.** The complete manually reviewed Book 2 cast snapshot now runs
|
||||||
a real Book 2 auto-cast/attribution output still needs to be captured and reviewed to confirm rules are not overfit
|
through normalization; it preserves all 1,663 segments and contains no phantom `Narrator` dialogue speaker. A real
|
||||||
to *Der Sturm*.
|
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,
|
- [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
|
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.
|
forms feed prompt generation and identity fingerprints.
|
||||||
|
|||||||
@ -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"
|
typ = "dialogue" if str(seg.get("type") or "").lower().startswith("dial") else "narration"
|
||||||
if typ == "narration":
|
if typ == "narration":
|
||||||
sp = "Narrator"
|
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()
|
emo = str(seg.get("emotion") or "").strip()
|
||||||
clean.append({"speaker": sp, "type": typ, "text": t, "emotion": emo})
|
clean.append({"speaker": sp, "type": typ, "text": t, "emotion": emo})
|
||||||
if typ == "dialogue" and sp.lower() != "narrator" and sp not in chars:
|
if typ == "dialogue" and sp.lower() != "narrator" and sp not in chars:
|
||||||
|
|||||||
@ -109,6 +109,15 @@ class AttributionParserTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertTrue(all(segment["type"] == "dialogue" for segment in result["segments"][1:]))
|
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):
|
def test_bundled_comfyui_workflows_have_required_prompt_and_save_nodes(self):
|
||||||
for image_kind in ("portrait", "concept_art"):
|
for image_kind in ("portrait", "concept_art"):
|
||||||
raw, prompt_node, prompt_field, save_node = _bundled_comfyui_workflow(image_kind)
|
raw, prompt_node, prompt_field, save_node = _bundled_comfyui_workflow(image_kind)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user