Try to improve prompt adherence for low variety image models
Build and Push Container / build-and-push (push) Successful in 1m5s
Build and Push Container / build-and-push (push) Successful in 1m5s
This commit is contained in:
@@ -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
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user