From 3f4dfb70a42a514cb8a982cabb37704f13fe363f Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Tue, 29 Sep 2026 21:20:56 +0200 Subject: [PATCH] fix(characters): harder gender/period locks in scene prompts, no unchecked sheet overwrites a validated one, weapon wording only for armed characters, poll survives 5xx blips Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 2 ++ routes/conversation.py | 40 +++++++++++++++++++++++++++++---- static/js/library-characters.js | 11 ++++++++- 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a46691..da6893d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi ## [Unreleased] ### Fixed +- **Character scene images.** The scene prompt now states the character's gender and the book's pre-industrial period as hard constraints and asks for the focal person's face, upper body and hands in the near foreground. A sheet whose image checker is unavailable is no longer saved over a previously validated image: generation stops with a clear error instead. The weapon-integrity wording ("one hilt and one blade") is only added for characters who actually carry a weapon (it was planted into every scene). +- The character-generation status poll rides out a transient 502/503/504 (proxy or app restart) instead of reporting a false "lost job". - **Voice benchmark failures now say why.** Designed voices are benchmarked through the Voice Design engine; when that service is down the row showed a bare "-" and the raw "Max retries exceeded" text. The error now names the service and the container to start, the Factor and WPM columns show `ERR` beside the time column, and a batch stops after three identical "unreachable" failures instead of retrying every remaining voice. ### Added - `scripts/bench_ocr.py`: engine-agnostic OCR benchmark that scores an engine's transcription of rendered pages against a native-text PDF's own text layer (word F1, sequence similarity, seconds per page). First results (tesseract, Unlimited-OCR, docling, PaddleOCR) in `architecture/evidence/ocr-bench-2026-09-29/`: transcription accuracy is equal within noise (F1 0.865-0.881), tesseract is 4-20x faster; DeepSeek-OCR and Qwen3-VL-30B were not run (no memory headroom). diff --git a/routes/conversation.py b/routes/conversation.py index 775ec1b..24b5435 100644 --- a/routes/conversation.py +++ b/routes/conversation.py @@ -2771,13 +2771,35 @@ def _build_compact_character_render_prompts(name: str, book: str, sheet: dict, back_view = ( "Unused: the rear view is generated in the controlled turnaround triptych." ) + scene_gender = _visual_prompt_value(sheet.get("gender")).strip().lower() + scene_gender_lock = ( + "FOCAL GENDER LOCK: the single prominent foreground character is unmistakably female, with clearly feminine facial features and build; " + if scene_gender in {"female", "weiblich"} else + "FOCAL GENDER LOCK: the single prominent foreground character is unmistakably male, with clearly masculine facial features and build; " + if scene_gender in {"male", "männlich", "maennlich"} else "" + ) + scene_period_lock = ( + "HISTORICAL SCENE LOCK: this is a scene from the book's pre-industrial early-Renaissance fantasy world, " + "with period-authentic wool, linen, leather, forged metal, timber, stone, earth, and candle or daylight illumination; " + "all visible people wear the established period costume and the setting contains only the book's historical-fantasy world. " + ) + # The weapon-integrity sentence belongs only to characters who carry one; for everyone else it + # planted a hilt and blade into the scene. + scene_weapon_integrity = ( + "Every held or worn weapon is one single complete object with exactly one hilt and one blade or head, " + "correctly proportioned and physically attached, never a duplicated or fused shape. " + ) if _character_has_established_combat_gear(sheet) else "" scene_prompt = ( ("HEAD CONTINUITY: Every visible person's scalp, crown, hairline, ears, and natural hairstyle are clearly visible and unobstructed. " if bareheaded else "") + "CREATE ONE HORIZONTAL PHOTOREALISTIC SOURCE-GROUNDED STORY MOMENT WITH NATURAL ACTION. " f"EXACT CHARACTER: {sheet_subject}" + (f"; exactly as in the portrait: {portrait_look}" if portrait_look else "") + ". " f"BOOK-SOURCE EVIDENCE: {scene_evidence}. " - f"FOCAL PERSON: {_character_identity_phrase(name, sheet)}, exactly as described above, the single clear subject in the foreground. " - "Show the character naturally acting, speaking, working, commanding, using magic, or fighting exactly as the evidence supports. The named character appears exactly once as the focal actor, with exactly one head and exactly two hands, each holding at most one item, and the same species, skin, and facial features as the turnaround sheet — never rendered as an unmarked ordinary human when a non-human species is established. Preserve the stated face, age, species, build, hair, costume, materials, and period. Every held or worn weapon is one single complete object with exactly one hilt and one blade or head, correctly proportioned and physically attached, never a duplicated or fused shape. Use only verified equipment needed by this moment. When the evidence requires supporting figures, give them clearly different faces and simpler clothing while keeping them secondary. " + + scene_period_lock + + scene_gender_lock + + f"FOCAL PERSON: {_character_identity_phrase(name, sheet)}, exactly as described above, the single clear subject in the foreground. Place the focal person's fully visible head, face, upper body, and both hands prominently in the near foreground so the character identity is immediately readable; the location and any supporting figures remain secondary background context. " + "Show the character naturally acting, speaking, working, commanding, using magic, or fighting exactly as the evidence supports. The named character appears exactly once as the focal actor, with exactly one head and exactly two hands, each holding at most one item, and the same species, skin, and facial features as the turnaround sheet — never rendered as an unmarked ordinary human when a non-human species is established. Preserve the stated face, age, species, build, hair, costume, materials, and period. " + + scene_weapon_integrity + + "Use only verified equipment needed by this moment. When the evidence requires supporting figures, give them clearly different faces and simpler clothing while keeping them secondary. " f"{period} {sheet_style} Use a believable location from the book, restrained cinematic staging, and complete uncropped head and hands. Keep all surfaces unlettered and free of graphic overlays. Output 768x512." ) prop_period = ( @@ -4065,8 +4087,18 @@ async def character_generate_consistent_images(request: Request): _cij_set(job_id, stage="render", detail=f"Checking the character sheet (attempt {attempt + 1})…") sheet_verdicts = _check_sheet_panels(panels, sheet, settings_now) if any(checker_unavailable(v) for v in sheet_verdicts.values()): - best_sheet, verified = (0, candidate, sheet_verdicts), False - break + # An unchecked sheet must never replace a previously validated character + # image. The renderer can drift into modern scenery or unrelated people even + # when the prompt contains the correct period and identity constraints; the + # checker-unavailable state is therefore a hard stop, not an implicit pass. + unavailable = next( + v for v in sheet_verdicts.values() if checker_unavailable(v) + ) + raise RuntimeError( + "Character sheet validation is unavailable; the generated sheet was not saved. " + "Please restore the image checker and try again. " + + "; ".join(str(f) for f in (unavailable.get("failures") or [])[:2]) + ) score = failure_count(sheet_verdicts) if best_sheet is None or score < best_sheet[0]: best_sheet = (score, candidate, sheet_verdicts) diff --git a/static/js/library-characters.js b/static/js/library-characters.js index ec30b7e..766fb14 100644 --- a/static/js/library-characters.js +++ b/static/js/library-characters.js @@ -3755,7 +3755,16 @@ async function _cijPollJob(jobId, panel) { let status; try { const r = await fetch('/api/character-generate-consistent-images/status/' + jobId); - if (!r.ok) throw new Error('Lost track of the generation job (status ' + r.status + ')'); + if (!r.ok) { + // A reverse proxy or app restart can briefly return a gateway error while + // the worker is still alive. Do not turn one transient 502/503/504 into + // a false permanent "lost job" error. + if ([502, 503, 504].indexOf(r.status) >= 0) { + await new Promise(function (res) { setTimeout(res, 3000); }); + continue; + } + throw new Error('Lost track of the generation job (status ' + r.status + ')'); + } status = await r.json(); } catch (e) { panel.done(false);