test: lock visual identity prompts

This commit is contained in:
mARTin-B78 2026-09-04 15:30:39 +02:00
parent 8181c491c0
commit b6f4a9518b
7 changed files with 71 additions and 15 deletions

View File

@ -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

20
TODO.md
View File

@ -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

View File

@ -1 +1 @@
1.24.98
1.24.99

View File

@ -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.

View File

@ -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:

View File

@ -10,7 +10,7 @@
<meta name="format-detection" content="telephone=no">
<meta name="color-scheme" content="light dark">
<meta name="theme-color" content="#2563EB">
<meta name="app-version" content="1.24.98">
<meta name="app-version" content="1.24.99">
<link rel="manifest" href="/manifest.webmanifest">
<link rel="icon" href="/static/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/static/icon.svg">
@ -27,12 +27,12 @@
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
<link rel="stylesheet" href="/static/style.css?v=1.24.98">
<link rel="stylesheet" href="/static/css/resource-diagnostics.css?v=1.24.98">
<link rel="stylesheet" href="/static/css/background-jobs.css?v=1.24.98">
<link rel="stylesheet" href="/static/css/voice-design.css?v=1.24.98">
<link rel="stylesheet" href="/static/css/rehearser.css?v=1.24.98">
<link rel="stylesheet" href="/static/css/audiobook.css?v=1.24.98">
<link rel="stylesheet" href="/static/style.css?v=1.24.99">
<link rel="stylesheet" href="/static/css/resource-diagnostics.css?v=1.24.99">
<link rel="stylesheet" href="/static/css/background-jobs.css?v=1.24.99">
<link rel="stylesheet" href="/static/css/voice-design.css?v=1.24.99">
<link rel="stylesheet" href="/static/css/rehearser.css?v=1.24.99">
<link rel="stylesheet" href="/static/css/audiobook.css?v=1.24.99">
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
@ -397,7 +397,7 @@ window.toggleNavTree = function(treeId, chevronId) {
</script>
<!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
<script src="/static/loader.js?v=1.24.98"></script>
<script src="/static/loader.js?v=1.24.99"></script>
</body>
</html>

View File

@ -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"