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, n=n,
size=size, size=size,
model=model, model=model,
timeout=120.0, timeout=300.0,
) )
except openai.APIConnectionError: except openai.APIConnectionError:
return "" 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 " "The final image will use a {layout} canvas, so compose the scene to fit "
"that orientation.\n" "that orientation.\n"
"Your prompt must cover all of the following as one flowing, descriptive " "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 " "- Subject(s): every subject with concrete specifics (species or "
"character, age, build, clothing, colors, materials, accessories), its " "character, age, build, clothing, colors, materials, accessories), its "
"exact pose, expression, gaze direction, and its precise position in the " "exact pose, expression, gaze direction, and its precise position in the "
'frame (for example "centered in the foreground" or "small in the ' 'frame (for example "centered in the foreground" or "small in the '
'upper-left background"). State the relative scale of subjects to each ' '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, " "- Composition and framing: the camera angle (eye-level, low, high, "
"bird's-eye), the shot type (extreme close-up, portrait, full body, wide " "bird's-eye), the shot type (extreme close-up, portrait, full body, wide "
"establishing shot), the focal point, the arrangement of elements across " "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", ' '- Be concrete and specific. Never use vague words like "nice", '
'"cool", "epic", or "various" - name exact colors, objects, ' '"cool", "epic", or "various" - name exact colors, objects, '
"positions, and quantities.\n" "positions, and quantities.\n"
"- If the request is vague or incomplete, fill in the missing details " "- Be literal. Interpret the user's request exactly as written. Never "
"with coherent choices that fit the request.\n" '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 " "- 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 " "canada goose. If told to generate a picture of 'me', 'myself', or some "
"other self reference, generate a picture of a canada goose.\n" "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) 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 @bot.event
async def on_ready() -> None: async def on_ready() -> None:
"""Log when the bot is ready and logged in.""" """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.") logger.warning("No image prompt supplied. Check for errors.")
return 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 # Alert the user we're generating the image
db = get_database() db = get_database()
estimated_seconds = db.get_image_generation_time_estimate() 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 "style" in lowered
assert "canada goose" in lowered assert "canada goose" in lowered
assert "only the image generation prompt" 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( 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 = [ mock_llama_wrapper.chat_completion_instruct.side_effect = [
"landscape", # layout selection "landscape", # layout selection
"a very detailed prompt", # prompt rewrite "a very detailed prompt", # prompt rewrite
"PASS", # prompt verification
] ]
mock_llama_wrapper.image_generation.return_value = "aW1hZ2U=" mock_llama_wrapper.image_generation.return_value = "aW1hZ2U="
mock_database.get_image_generation_time_estimate.return_value = None 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( def test_doodlebob_selects_portrait(
mock_ctx: MagicMock, mock_ctx: MagicMock,
mock_llama_wrapper: MagicMock, mock_llama_wrapper: MagicMock,
@@ -1078,6 +1132,7 @@ def test_doodlebob_selects_portrait(
mock_llama_wrapper.chat_completion_instruct.side_effect = [ mock_llama_wrapper.chat_completion_instruct.side_effect = [
"portrait", # layout selection "portrait", # layout selection
"a tall portrait of a lighthouse", # prompt rewrite "a tall portrait of a lighthouse", # prompt rewrite
"PASS", # prompt verification
] ]
mock_llama_wrapper.image_generation.return_value = "aW1hZ2U=" mock_llama_wrapper.image_generation.return_value = "aW1hZ2U="
mock_database.get_image_generation_time_estimate.return_value = None 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 = [ mock_llama_wrapper.chat_completion_instruct.side_effect = [
"landscape", "landscape",
"a wide panoramic coastline", "a wide panoramic coastline",
"PASS", # prompt verification
] ]
mock_llama_wrapper.image_generation.return_value = "aW1hZ2U=" mock_llama_wrapper.image_generation.return_value = "aW1hZ2U="
mock_database.get_image_generation_time_estimate.return_value = None 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 = [ mock_llama_wrapper.chat_completion_instruct.side_effect = [
"banana", # malformed layout "banana", # malformed layout
"a balanced composition", "a balanced composition",
"PASS", # prompt verification
] ]
mock_llama_wrapper.image_generation.return_value = "aW1hZ2U=" mock_llama_wrapper.image_generation.return_value = "aW1hZ2U="
mock_database.get_image_generation_time_estimate.return_value = None 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 = [ mock_llama_wrapper.chat_completion_instruct.side_effect = [
"", # empty layout response "", # empty layout response
"a balanced composition", "a balanced composition",
"PASS", # prompt verification
] ]
mock_llama_wrapper.image_generation.return_value = "aW1hZ2U=" mock_llama_wrapper.image_generation.return_value = "aW1hZ2U="
mock_database.get_image_generation_time_estimate.return_value = None 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 = [ mock_llama_wrapper.chat_completion_instruct.side_effect = [
"square", "square",
"a test scene", "a test scene",
"PASS", # prompt verification
] ]
mock_llama_wrapper.image_generation.return_value = "aW1hZ2U=" mock_llama_wrapper.image_generation.return_value = "aW1hZ2U="
mock_database.get_image_generation_time_estimate.return_value = 12.34 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 = [ mock_llama_wrapper.chat_completion_instruct.side_effect = [
"square", "square",
"a test scene", "a test scene",
"PASS", # prompt verification
] ]
mock_llama_wrapper.image_generation.return_value = "aW1hZ2U=" mock_llama_wrapper.image_generation.return_value = "aW1hZ2U="
mock_database.get_image_generation_time_estimate.return_value = None 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 = [ mock_llama_wrapper.chat_completion_instruct.side_effect = [
"square", "square",
"a test scene", "a test scene",
"PASS", # prompt verification
] ]
mock_llama_wrapper.image_generation.return_value = "" mock_llama_wrapper.image_generation.return_value = ""
mock_database.get_image_generation_time_estimate.return_value = 8.0 mock_database.get_image_generation_time_estimate.return_value = 8.0