diff --git a/CHANGELOG.md b/CHANGELOG.md index b636656..6802704 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi ## [Unreleased] ### Changed +- **Visual identity regression coverage.** Portrait and concept-sheet prompt tests now verify shared identity anchors; + completed pairs centralize prompt-version, identity-fingerprint, and asset-digest metadata. - **Frontend namespace migration completed.** The remaining utility, library, cast-compatibility, i18n, and settings/log surfaces now keep implementation state private while preserving explicit legacy compatibility exports. - **Conversation/log namespace migration.** Settings log state and About rendering helpers now use a private scope with diff --git a/TODO.md b/TODO.md index 549f9e2..f878266 100644 --- a/TODO.md +++ b/TODO.md @@ -261,6 +261,26 @@ architecture change: the larger Audiobook/Rehearser/style split. ## P1 — Speaker attribution: measured state and next steps (paused 2026-09-02) +### Ordered follow-up queue + +These follow-ups are ordered by dependency and expected benefit. Estimates assume one developer familiar with this +codebase; benefit is relative user/reliability impact. + +- [x] **P1 / M — High benefit — Identity-anchor regression tests.** Add automated coverage proving avatar and concept-sheet + requests share canonical identity anchors and persist the prompt version used for each generated asset. +- [ ] **P1 / S–M — Medium benefit — Preserve dialogue-only merging.** Keep the dialogue-only merge for recast/verify + outputs where adjacent pairs still occur, with regression coverage for the non-dialogue boundaries. +- [ ] **P1 / L–XL — Very high benefit — Rework segmentation from Run 3.** Inspect the earlier dialogue split/fusion + stages and correct the dominant unmatched-segment class using the measured Run 3 artifacts. +- [ ] **P1 / M — High benefit — Re-run Book 1.** Re-measure the hand-corrected Book 1 reference after segmentation + changes; do not treat a short or incomplete run as a valid result. +- [ ] **P1 / L–XL — Very high benefit — Run Book 2 generalization.** Execute the backed-up Book 2 reference regression + to measure whether segmentation and attribution rules generalize beyond Book 1. +- [ ] **P1 / L — Very high benefit — Unify visual identity.** Ensure portrait and concept-sheet generation use one + canonical identity description, stored prompt version, and stale-asset review path. +- [ ] **P2 / M–L — Medium–high benefit — Enrich character-form UX.** Add richer inline form editing and per-form + bulk-generation controls while preserving one character ID and one voice. + Three full regression runs of *Der Sturm* against the hand-corrected reference. Everything below is measured, not estimated. Artefacts live in `config/regression_snapshots/der_sturm/` (`*_manual.json` are the human diff --git a/VERSION b/VERSION index 3da8cae..9e35561 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.24.98 +1.24.99 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 8e0bee1..6c711e7 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -87,6 +87,7 @@ Done incrementally as global-scope ordered files (no ES-module change needed) - ✅ Conversation settings/log state and About helpers use a private scope with compatibility exports and a namespace API. - ✅ All remaining application frontend modules are either privately scoped or intentionally stateless bootstrap/cache IIFEs; legacy globals are explicit compatibility exports and new integrations use `window.TTVC` namespaces. +- ✅ Visual pair generation centralizes prompt-version, identity-fingerprint, and asset-digest metadata with regression coverage for shared anchors. - ✅ `static/css/resource-diagnostics.css` — isolated Performance/resource diagnostic panel styles. - ✅ `static/css/background-jobs.css` — isolated background-job history and retry-panel styles. - ✅ `static/css/voice-design.css` — isolated Voice Design form, samples, and preview-match styles. diff --git a/routes/conversation.py b/routes/conversation.py index 903d495..bcf9a4e 100644 --- a/routes/conversation.py +++ b/routes/conversation.py @@ -159,6 +159,17 @@ def _character_identity_fingerprint(sheet: dict) -> str: return format(value, "x") +def _consistent_image_result_metadata(sheet: dict, result: dict | None) -> dict: + """Attach the identity contract to every completed visual pair.""" + payload = dict(result or {}) + payload["prompt_version"] = _CHARACTER_VISUAL_PROMPT_VERSION + payload["identity_consistent"] = True + payload["identity_fingerprint"] = _character_identity_fingerprint(sheet) + payload["profile_asset_digest"] = _data_image_digest(payload.get("profile")) + payload["reference_asset_digest"] = _data_image_digest(payload.get("reference")) + return payload + + def _character_form_sheet(sheet: dict) -> dict: """Overlay the selected appearance form without changing the character ID or voice.""" if not isinstance(sheet, dict): @@ -3478,12 +3489,7 @@ async def character_generate_consistent_images(request: Request): generation_key, progress=lambda elapsed: (_cij_check_cancelled(job_id), _cij_set(job_id)), ) - result = dict(result or {}) - result["prompt_version"] = _CHARACTER_VISUAL_PROMPT_VERSION - result["identity_consistent"] = True - result["identity_fingerprint"] = _character_identity_fingerprint(sheet) - result["profile_asset_digest"] = _data_image_digest(result.get("profile")) - result["reference_asset_digest"] = _data_image_digest(result.get("reference")) + result = _consistent_image_result_metadata(sheet, result) result = _persist_consistent_image_artifacts(job_id, result) _cij_set(job_id, stage="done", detail="Done", done=True, result=result) except HTTPException as exc: diff --git a/static/index.html b/static/index.html index 5b1017a..372f26b 100644 --- a/static/index.html +++ b/static/index.html @@ -10,7 +10,7 @@ - + @@ -27,12 +27,12 @@ - - - - - - + + + + + + @@ -397,7 +397,7 @@ window.toggleNavTree = function(treeId, chevronId) { - + diff --git a/tests/test_conversation.py b/tests/test_conversation.py index 36a08d2..b50e8d9 100644 --- a/tests/test_conversation.py +++ b/tests/test_conversation.py @@ -13,6 +13,10 @@ from routes.conversation import ( _attribution_parse, _character_form_sheet, _character_identity_fingerprint, + _build_character_portrait_prompt, + _build_character_sheet_prompt, + _consistent_image_result_metadata, + _CHARACTER_VISUAL_PROMPT_VERSION, _cij_check_cancelled, _cij_new, _consistent_image_cancel_events, @@ -30,6 +34,29 @@ from routes.conversation import ( class AttributionParserTests(unittest.TestCase): + def test_visual_prompts_share_canonical_identity_anchors(self): + sheet = { + "gender": "female", "age_estimate": "late thirties", "race_species": "human", + "physical": "olive skin, angular face, shoulder-length copper hair, green eyes", + "clothing": "faded blue wool coat", "profession": "cartographer", "marks": "crescent scar on left cheek", + } + portrait = _build_character_portrait_prompt("Mara Venn", "The Survey", sheet, {"era": "early Renaissance"}) + reference = _build_character_sheet_prompt("Mara Venn", "The Survey", sheet, {"era": "early Renaissance"}) + for anchor in ("female", "late thirties", "human", "olive skin", "copper hair", "green eyes", "blue wool coat", "cartographer"): + self.assertIn(anchor, portrait) + self.assertIn(anchor, reference) + + def test_visual_pair_metadata_persists_shared_prompt_version_and_identity(self): + sheet = {"name": "Mara Venn", "gender": "female", "physical": "copper hair and green eyes"} + profile = "data:image/png;base64,AA==" + reference = "data:image/png;base64,AQ==" + result = _consistent_image_result_metadata(sheet, {"profile": profile, "reference": reference}) + self.assertEqual(result["prompt_version"], _CHARACTER_VISUAL_PROMPT_VERSION) + self.assertEqual(result["identity_fingerprint"], _character_identity_fingerprint(sheet)) + self.assertTrue(result["identity_consistent"]) + self.assertTrue(result["profile_asset_digest"]) + self.assertTrue(result["reference_asset_digest"]) + def test_consistent_image_persistence_keeps_live_payload_and_adds_artifact_refs(self): from core.database import _BACKGROUND_ARTIFACT_DIR job_id = "live-image-artifact-test"