fix(characters): accessory pictures show only props, no body parts
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f5b5993e4d
commit
904d6a432c
@ -11,6 +11,7 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
||||
- **Filter the page grid by status.** Each legend entry is now a toggle button with the number of pages in that status; choose one or several to show only pages that have any of them. It combines with *Show only pages that need me*, the summary says how many pages are shown, *Show all pages* clears everything, a status no page has is greyed out, and the buttons announce their on/off state and work from the keyboard.
|
||||
|
||||
### Fixed
|
||||
- **The item pictures on the character concept sheet show only objects, never hands, feet or people.** The lower-right tiles are now asked for as "a high-quality concept art prop sheet featuring isolated character accessories" on a plain, neutral studio background, still life, strictly inanimate objects, with no people, hands, faces or body parts; the item name (German names included) and the book's world and style stay in the prompt. A body-part list was also added to the item tile's negative prompt, which like the rest only acts if the cfg is raised above 1.0. Existing sheets need to be regenerated.
|
||||
- **Driving mode and the playlist highlight the sentence that is actually being spoken.** The player guessed where each sentence sits in the MP3 from its text length, so the highlighted line and the picture drifted from the voice (worst after seeking). The renderer already records the exact time of every sentence (`audio/<id>.timings.json`); a new endpoint `GET /api/magazine/{doc}/articles/{id}/timings` returns them (`{available, sentences: [{text, block, block_id, start, end}]}`, seconds, from the same sentence planner the EPUB uses) and the player uses them, falling back to the old estimate when an MP3 has no usable record (older audio, interview audio, text edited since). The transcript is still fetched with captions, but the MP3 is built without them, so a caption now gets no time slot (the estimate no longer spends audio time on captions either). The highlight is derived only from the audio's current time against the timeline, so the slider, skip buttons, tapping a sentence, lock-screen seek and resume all stay in sync. Covered by a backend test (`ArticleTimingsRouteTests`) and a Playwright test with seeking.
|
||||
- **Magazine player buttons stay round on a phone.** In the full-screen player at 375 px wide (iPhone SE) the five transport buttons were 52+76+110+76+52 px plus gaps, wider than the screen, so they shrank unevenly: the big play button became a tall oval and the 15-second skip buttons were squeezed. The buttons now have a fixed 1:1 size that scales down on narrow screens (play 64-84 px, others 44-56 px) and never shrink or stretch, and the player no longer clips its top or bottom when the content is taller than the screen.
|
||||
- **No gaps on the character concept sheet.** After the divider lines were removed, light cream gutters remained between the three poses and above the lower pictures, and the left pose's cape and the right pose's sword were clipped at them. The composer cut the generated triptych into thirds and pasted each into an inset panel (8 px per side), and the lower pictures had 12-18 px padding. The poses are now pasted as one whole image with no cut and no inset, and the lower pictures sit flush under it. The generation prompt never asked for gaps, so it is unchanged. Sheets already stored have the gaps baked in and need to be regenerated.
|
||||
|
||||
@ -80,7 +80,7 @@ reference**, or open a character and regenerate its Konzeptbild. Pick *Cinematic
|
||||
**Expected result:** the lower pictures show the same person as the poses (face, hair,
|
||||
armour or clothing, colours), in the medieval high-fantasy world of the book (stone,
|
||||
timber, tents, torch or daylight, other people in medieval dress), and the item lies on
|
||||
a table in that world, made in the style and colours of its owner's costume. They contain
|
||||
a plain neutral background as an isolated prop, with no hands, feet or other body parts, made in the style and colours of its owner's costume. They contain
|
||||
no writing, captions or modern things. The face in the story moment is taken from the
|
||||
profile picture.
|
||||
|
||||
|
||||
@ -2810,6 +2810,14 @@ _LOWER_PANEL_NEGATIVE = (
|
||||
"modern building, street, car, power line, lamp post, suburban garden, plastic"
|
||||
)
|
||||
|
||||
# Added to the item tile's negative prompt (node 24) on top of _LOWER_PANEL_NEGATIVE. Body parts
|
||||
# only: garments such as boots or gloves can be the item itself. Like the rest, it only acts when
|
||||
# the cfg is raised above 1.0; the positive wording of the prompt carries the constraint.
|
||||
_SIGNATURE_ITEM_PANEL_NEGATIVE = (
|
||||
"people, human, person, hand, hands, fingers, arm, arms, leg, legs, feet, foot, "
|
||||
"face, head, torso, body, skin, mannequin, model, worn by a person, being held"
|
||||
)
|
||||
|
||||
_LOWER_PANEL_WORLD = "the medieval high-fantasy world of The Dark Eye (Aventuria), roughly 900-1300 AD"
|
||||
|
||||
|
||||
@ -2864,9 +2872,10 @@ def _build_story_moment_prompt(name: str, sheet: dict, book_profile: dict, visua
|
||||
|
||||
def _build_signature_item_prompt(name: str, sheet: dict, book_profile: dict, visual_style: str,
|
||||
item: str, costume: str) -> str:
|
||||
"""A lower-right tile: the character's own item, at home in the book's world."""
|
||||
"""A lower-right tile: the character's own item, isolated on a plain studio background,
|
||||
with no people or body parts (a worn item is shown as an object on its own)."""
|
||||
sheet = _character_form_sheet(sheet)
|
||||
description = _prop_render_description(item).replace("on the sweep", "on the table")
|
||||
description = _prop_render_description(item).replace("on the sweep", "on a plain surface")
|
||||
role = _primary_visual_role(sheet)
|
||||
owner = f"{name}, {role}" if name and role else (name or role or "its owner")
|
||||
costume = costume.strip().rstrip(" .…")
|
||||
@ -2874,11 +2883,13 @@ def _build_signature_item_prompt(name: str, sheet: dict, book_profile: dict, vis
|
||||
costume = costume[:200].rsplit(" ", 1)[0].rstrip(" ,;:")
|
||||
match = f" ({costume})" if costume else ""
|
||||
return " ".join((
|
||||
f"{_lower_panel_medium(visual_style)}: a still life of a single object in {_lower_panel_world(book_profile)}.",
|
||||
f"The object: {description}. It is the personal, well-used possession of {owner}, made with the same "
|
||||
f"A high-quality concept art prop sheet featuring isolated character accessories: {description}. It is the personal, well-used possession of {owner}, made with the same "
|
||||
f"craftsmanship, materials and colours as the owner's costume{match}.",
|
||||
"It lies whole and fully visible in the centre of the frame on a rough oak table in a medieval room, "
|
||||
"the object alone on the table, with a softly blurred background of stone, timber and candle light.",
|
||||
"The items are cleanly laid out and arranged against a plain, neutral studio background. "
|
||||
"Still life, detailed textures, professional prop design. Strictly inanimate objects only.",
|
||||
"Absolutely no people, no hands, no faces, and no body parts of any kind. "
|
||||
"The object alone is visible, fully complete and shown from the best angle in the centre of the frame.",
|
||||
f"{_lower_panel_medium(visual_style)}, made for {_lower_panel_world(book_profile)}.",
|
||||
_LOWER_PANEL_CRAFT,
|
||||
))
|
||||
|
||||
@ -3754,10 +3765,11 @@ def _comfyui_run_consistent_images(comfyui_url: str, workflow_json: str,
|
||||
graph["66"].setdefault("inputs", {})["text"] = front_prompt
|
||||
graph["70"].setdefault("inputs", {})["text"] = scene_prompt
|
||||
# Scene (71) and prop (24) negatives: see _LOWER_PANEL_NEGATIVE for why this alone is not enough.
|
||||
for negative_node in ("71", "24"):
|
||||
for negative_node, extra in (("71", ""), ("24", _SIGNATURE_ITEM_PANEL_NEGATIVE)):
|
||||
if negative_node in graph:
|
||||
inputs = graph[negative_node].setdefault("inputs", {})
|
||||
inputs["text"] = ", ".join(filter(None, (str(inputs.get("text") or "").strip(" ,"), _LOWER_PANEL_NEGATIVE)))
|
||||
inputs["text"] = ", ".join(filter(None, (str(inputs.get("text") or "").strip(" ,"),
|
||||
_LOWER_PANEL_NEGATIVE, extra)))
|
||||
# Disconnect the failed image-edit camera branches. Structural camera and
|
||||
# action changes are handled by native T2I branches below.
|
||||
for old_output in ("front_save", "side_save", "back_save", "scene_save"):
|
||||
|
||||
@ -272,6 +272,25 @@ class TurnaroundIdentityTests(unittest.TestCase):
|
||||
for person_word in ("scalp", "expression", "CLOTHING LOCK", "hairstyle"):
|
||||
self.assertNotIn(person_word, prompt)
|
||||
|
||||
def test_accessory_prompts_are_isolated_props_without_body_parts(self):
|
||||
*_, props = self.prompts(clothing="Rüstung", inventory="Armbrust, Schlüssel, Lederhandschuhe")
|
||||
self.assertTrue(props)
|
||||
for prompt in props:
|
||||
self.assertIn("concept art prop sheet featuring isolated character accessories:", prompt)
|
||||
self.assertIn("plain, neutral studio background", prompt)
|
||||
self.assertIn("Strictly inanimate objects only", prompt)
|
||||
self.assertIn("no people, no hands, no faces, and no body parts of any kind", prompt)
|
||||
self.assertNotIn("oak table", prompt)
|
||||
joined = " ".join(props).lower()
|
||||
self.assertIn("armbrust", joined)
|
||||
self.assertIn("schlüssel", joined)
|
||||
|
||||
def test_item_negative_prompt_reaches_the_item_node_only(self):
|
||||
from routes.conversation import _SIGNATURE_ITEM_PANEL_NEGATIVE, _LOWER_PANEL_NEGATIVE
|
||||
self.assertIn("hands", _SIGNATURE_ITEM_PANEL_NEGATIVE)
|
||||
self.assertNotIn("boots", _SIGNATURE_ITEM_PANEL_NEGATIVE)
|
||||
self.assertNotIn(_SIGNATURE_ITEM_PANEL_NEGATIVE, _LOWER_PANEL_NEGATIVE)
|
||||
|
||||
def test_the_book_period_is_high_fantasy_medieval_not_renaissance(self):
|
||||
portrait = self.prompts(clothing="Wollrock")[0]
|
||||
self.assertNotIn("Renaissance", portrait)
|
||||
@ -386,7 +405,8 @@ class LowerPanelPromptTests(unittest.TestCase):
|
||||
"""The encoder draws what it reads, even after "no": nothing to avoid is named positively."""
|
||||
_p, _t, _s, _b, scene, props = self.prompts()
|
||||
for prompt in [scene] + props:
|
||||
lower = prompt.lower()
|
||||
# the accessory template itself asks for a "neutral studio background"
|
||||
lower = prompt.lower().replace("neutral studio background", "")
|
||||
for cue in ("photograph", "studio", "actor", "camera", "headphone", "label", "caption",
|
||||
"lettered", "overlay", "output", "museum", "catalog", "head continuity",
|
||||
"evidence", "lock:", "der sturm"):
|
||||
@ -394,8 +414,9 @@ class LowerPanelPromptTests(unittest.TestCase):
|
||||
|
||||
def test_the_concept_style_is_painted(self):
|
||||
_p, _t, _s, _b, scene, props = self.prompts(style="concept")
|
||||
for prompt in [scene] + props:
|
||||
self.assertTrue(prompt.startswith("A semi-realistic hand-painted medieval high-fantasy illustration"))
|
||||
self.assertTrue(scene.startswith("A semi-realistic hand-painted medieval high-fantasy illustration"))
|
||||
for prompt in props:
|
||||
self.assertIn("A semi-realistic hand-painted medieval high-fantasy illustration", prompt)
|
||||
|
||||
def test_the_book_profile_setting_reaches_the_world(self):
|
||||
scene = self.prompts(book_profile={"setting": "Greifenfurt, eine belagerte Stadt", "era": "Frührenaissance"})[4]
|
||||
@ -409,14 +430,14 @@ class LowerPanelPromptTests(unittest.TestCase):
|
||||
self.assertIn("one complete medieval crossbow", prop)
|
||||
self.assertIn("personal, well-used possession of Marcian", prop)
|
||||
self.assertIn("Plattenrüstung mit einem flammend roten Umhang", prop)
|
||||
self.assertIn("rough oak table in a medieval room", prop)
|
||||
self.assertIn("plain, neutral studio background", prop)
|
||||
self.assertIn("The Dark Eye", prop)
|
||||
|
||||
def test_a_still_life_item_rests_on_the_table_not_the_sweep(self):
|
||||
from routes.conversation import _build_compact_character_render_prompts
|
||||
sheet = dict(self.MARCIAN, visual_props=["Amulett"])
|
||||
prop = _build_compact_character_render_prompts("Marcian", self.BOOK, sheet, {}, "cinematic", "", "")[5][0]
|
||||
self.assertIn("coiled flat on the table", prop)
|
||||
self.assertIn("coiled flat on a plain surface", prop)
|
||||
self.assertNotIn("sweep", prop)
|
||||
|
||||
def test_the_negative_prompt_names_text_and_modern_objects(self):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user