Try to improve prompt adherence for low variety image models
Build and Push Container / build-and-push (push) Successful in 1m5s

This commit is contained in:
2026-08-17 13:42:05 -04:00
parent 7f0c5fbd80
commit 16407dd7c8
3 changed files with 138 additions and 5 deletions
+1 -1
View File
@@ -323,7 +323,7 @@ def image_generation(
n=n,
size=size,
model=model,
timeout=120.0,
timeout=300.0,
)
except openai.APIConnectionError:
return ""
+76 -4
View File
@@ -146,13 +146,17 @@ IMAGE_PROMPT_SYSTEM_PROMPT_TEMPLATE = (
"The final image will use a {layout} canvas, so compose the scene to fit "
"that orientation.\n"
"Your prompt must cover all of the following as one flowing, descriptive "
"passage:\n"
"passage. Begin with the main subject, described completely in the very "
"first sentence:\n"
"- Subject(s): every subject with concrete specifics (species or "
"character, age, build, clothing, colors, materials, accessories), its "
"exact pose, expression, gaze direction, and its precise position in the "
'frame (for example "centered in the foreground" or "small in the '
'upper-left background"). State the relative scale of subjects to each '
"other and to the frame.\n"
"other and to the frame. If the subject is a fusion, hybrid, or anything "
"unusual, the first sentence must state in full what is joined to what "
"and exactly how it looks, and that description must be repeated near "
"the end of the passage.\n"
"- Composition and framing: the camera angle (eye-level, low, high, "
"bird's-eye), the shot type (extreme close-up, portrait, full body, wide "
"establishing shot), the focal point, the arrangement of elements across "
@@ -177,8 +181,23 @@ IMAGE_PROMPT_SYSTEM_PROMPT_TEMPLATE = (
'- Be concrete and specific. Never use vague words like "nice", '
'"cool", "epic", or "various" - name exact colors, objects, '
"positions, and quantities.\n"
"- If the request is vague or incomplete, fill in the missing details "
"with coherent choices that fit the request.\n"
"- Be literal. Interpret the user's request exactly as written. Never "
'rationalize, normalize, or "improve" it: surreal, absurd, or '
'anthropomorphic requests are intentional, not mistakes. A "fountain '
'pen wearing pants" is an anthropomorphized fountain pen character '
"wearing pants, not a pen lying next to a pair of pants.\n"
"- Preserve everything the user specified. Fill in details the user did "
"not specify with coherent choices that fit the request, but never "
"alter, drop, or reinterpret what the user did specify.\n"
"- Decompose concepts. The image model lacks world knowledge, so never "
"rely on a name alone for anything it might misrender (mythical "
"creatures, fictional characters, cultural items, animal breeds, "
"instruments, vehicles). Spell out the visual anatomy: silhouette, body "
"parts, materials, and distinguishing features, with explicit "
"disambiguation. A centaur is a single creature with a human torso, "
"arms, and head seamlessly fused to a horse's front half, the horse's "
"four legs extending from the human's waist - one fused body, not a "
"person riding a horse.\n"
"- If told to generate an image of yourself, generate a picture of a "
"canada goose. If told to generate a picture of 'me', 'myself', or some "
"other self reference, generate a picture of a canada goose.\n"
@@ -228,6 +247,54 @@ def select_image_layout(user_message: str) -> str:
return parse_image_layout(response)
IMAGE_PROMPT_VERIFY_SYSTEM_PROMPT = (
"You are the final quality check for an image generation prompt. The "
"image model that will use it has no world knowledge: it renders only "
"what is described literally and silently drops anything it does not "
"understand - a prompt that merely names a centaur without describing "
"the fused human-animal body will produce a plain horse. "
"Given the user's original request and the drafted prompt, judge "
"strictly: would the drafted prompt, taken completely literally, "
"produce exactly what the user asked for, including every unusual, "
"mythical, surreal, or anthropomorphic element? "
"Respond with ONLY the single word PASS if it would. Otherwise respond "
"with ONLY a corrected version of the prompt that would produce exactly "
"what the user asked for: one flowing descriptive passage, the "
"subject's full anatomy and every unusual element described explicitly "
"in the first sentence and repeated near the end, no other text."
)
def verify_image_prompt(user_message: str, image_prompt: str) -> str:
"""Check the drafted prompt literally produces the user's request.
Args:
user_message: The user's original image request.
image_prompt: The drafted image generation prompt.
Returns:
The original prompt when the check passes or the LLM returns an
empty response, otherwise the LLM's corrected prompt.
"""
check_prompt = f"User request: {user_message}\n\nDrafted prompt: {image_prompt}"
response = llama_wrapper.chat_completion_instruct(
system_prompt=IMAGE_PROMPT_VERIFY_SYSTEM_PROMPT,
user_prompt=check_prompt,
openai_url=CHAT_ENDPOINT,
openai_api_key=CHAT_ENDPOINT_KEY,
model=CHAT_MODEL,
max_tokens=MAX_COMPLETION_TOKENS,
)
if not response:
return image_prompt
# A passing check is the single word PASS; a correction is a full
# rewritten passage, which is always much longer.
if len(response) <= 50 and re.search(r"\bpass\b", response, re.IGNORECASE):
return image_prompt
return response
@bot.event
async def on_ready() -> None:
"""Log when the bot is ready and logged in."""
@@ -862,6 +929,11 @@ async def doodlebob(ctx: CommandsContext[Bot], *, message: str) -> None:
logger.warning("No image prompt supplied. Check for errors.")
return
# Verify the prompt literally produces the user's request; the check
# may return a corrected prompt.
image_prompt = verify_image_prompt(message, image_prompt)
logger.info("Doodlebob final image prompt: %s", image_prompt)
# Alert the user we're generating the image
db = get_database()
estimated_seconds = db.get_image_generation_time_estimate()
+61
View File
@@ -1030,6 +1030,10 @@ def test_image_prompt_system_prompt_covers_key_details() -> None:
assert "style" in lowered
assert "canada goose" in lowered
assert "only the image generation prompt" in lowered
assert "exactly as written" in lowered
assert "fountain pen wearing pants" in lowered
assert "centaur" in lowered
assert "not a person riding a horse" in lowered
def test_doodlebob_prompt_rewrite_uses_detailed_system_prompt(
@@ -1046,6 +1050,7 @@ def test_doodlebob_prompt_rewrite_uses_detailed_system_prompt(
mock_llama_wrapper.chat_completion_instruct.side_effect = [
"landscape", # layout selection
"a very detailed prompt", # prompt rewrite
"PASS", # prompt verification
]
mock_llama_wrapper.image_generation.return_value = "aW1hZ2U="
mock_database.get_image_generation_time_estimate.return_value = None
@@ -1064,6 +1069,55 @@ def test_doodlebob_prompt_rewrite_uses_detailed_system_prompt(
)
def test_verify_image_prompt_passes_unchanged(
mock_llama_wrapper: MagicMock,
) -> None:
"""A PASS verdict keeps the original prompt and sends both inputs."""
import vibe_bot.main as main_module
mock_llama_wrapper.chat_completion_instruct.return_value = "PASS"
result = main_module.verify_image_prompt("a centaur", "a detailed prompt")
assert result == "a detailed prompt"
call = mock_llama_wrapper.chat_completion_instruct.call_args
assert call.kwargs["system_prompt"] == main_module.IMAGE_PROMPT_VERIFY_SYSTEM_PROMPT
assert "a centaur" in call.kwargs["user_prompt"]
assert "a detailed prompt" in call.kwargs["user_prompt"]
def test_verify_image_prompt_case_insensitive_pass(
mock_llama_wrapper: MagicMock,
) -> None:
"""A lower-case, punctuated 'pass' verdict also keeps the original."""
import vibe_bot.main as main_module
mock_llama_wrapper.chat_completion_instruct.return_value = "pass."
assert main_module.verify_image_prompt("a centaur", "p") == "p"
def test_verify_image_prompt_correction_replaces_prompt(
mock_llama_wrapper: MagicMock,
) -> None:
"""A non-PASS response is used as the corrected prompt."""
import vibe_bot.main as main_module
corrected = (
"one fused creature, a human torso joined to a horse's front half, "
"human arms raised, standing in a rocky field"
)
mock_llama_wrapper.chat_completion_instruct.return_value = corrected
assert main_module.verify_image_prompt("a centaur", "a horse") == corrected
def test_verify_image_prompt_empty_falls_back(
mock_llama_wrapper: MagicMock,
) -> None:
"""An empty verification response falls back to the drafted prompt."""
import vibe_bot.main as main_module
mock_llama_wrapper.chat_completion_instruct.return_value = ""
assert main_module.verify_image_prompt("a centaur", "drafted") == "drafted"
def test_doodlebob_selects_portrait(
mock_ctx: MagicMock,
mock_llama_wrapper: MagicMock,
@@ -1078,6 +1132,7 @@ def test_doodlebob_selects_portrait(
mock_llama_wrapper.chat_completion_instruct.side_effect = [
"portrait", # layout selection
"a tall portrait of a lighthouse", # prompt rewrite
"PASS", # prompt verification
]
mock_llama_wrapper.image_generation.return_value = "aW1hZ2U="
mock_database.get_image_generation_time_estimate.return_value = None
@@ -1113,6 +1168,7 @@ def test_doodlebob_selects_landscape(
mock_llama_wrapper.chat_completion_instruct.side_effect = [
"landscape",
"a wide panoramic coastline",
"PASS", # prompt verification
]
mock_llama_wrapper.image_generation.return_value = "aW1hZ2U="
mock_database.get_image_generation_time_estimate.return_value = None
@@ -1137,6 +1193,7 @@ def test_doodlebob_malformed_layout_defaults_square(
mock_llama_wrapper.chat_completion_instruct.side_effect = [
"banana", # malformed layout
"a balanced composition",
"PASS", # prompt verification
]
mock_llama_wrapper.image_generation.return_value = "aW1hZ2U="
mock_database.get_image_generation_time_estimate.return_value = None
@@ -1163,6 +1220,7 @@ def test_doodlebob_empty_layout_defaults_square(
mock_llama_wrapper.chat_completion_instruct.side_effect = [
"", # empty layout response
"a balanced composition",
"PASS", # prompt verification
]
mock_llama_wrapper.image_generation.return_value = "aW1hZ2U="
mock_database.get_image_generation_time_estimate.return_value = None
@@ -1188,6 +1246,7 @@ def test_doodlebob_reports_estimate_and_elapsed(
mock_llama_wrapper.chat_completion_instruct.side_effect = [
"square",
"a test scene",
"PASS", # prompt verification
]
mock_llama_wrapper.image_generation.return_value = "aW1hZ2U="
mock_database.get_image_generation_time_estimate.return_value = 12.34
@@ -1220,6 +1279,7 @@ def test_doodlebob_no_estimate_without_history(
mock_llama_wrapper.chat_completion_instruct.side_effect = [
"square",
"a test scene",
"PASS", # prompt verification
]
mock_llama_wrapper.image_generation.return_value = "aW1hZ2U="
mock_database.get_image_generation_time_estimate.return_value = None
@@ -1247,6 +1307,7 @@ def test_doodlebob_failed_generation_not_recorded(
mock_llama_wrapper.chat_completion_instruct.side_effect = [
"square",
"a test scene",
"PASS", # prompt verification
]
mock_llama_wrapper.image_generation.return_value = ""
mock_database.get_image_generation_time_estimate.return_value = 8.0