Files
JDJoe 5b5ed5166c Expose each plate id and choose seeds for the plates that have none.
A trailing underscore such as p04_ is a plate. Fewer seed lines than plates keep the written seeds and roll the rest.
2026-11-08 15:40:25 +02:00

199 lines
8.6 KiB
Python

import unittest
from prompt_batch import PromptBatchError, SEED_MAX, build_batch, seeds_are_rolled
CHARACTERS = """
CHAR1
A woman in her thirties with short black hair and a grey coat.
CHAR2
A tall man with a scar on his left cheek.
"""
PROMPTS = """
p01
CHAR1 stands in the rain outside a station.
p02
CHAR1 hands CHAR2 a letter in a dark hallway.
"""
def batch(characters, prompts, seeds, rolls=(), prefix=""):
pending = iter(rolls)
def roll():
return next(pending)
return build_batch(characters, prompts, seeds, roll, prefix)
class BuildBatchTests(unittest.TestCase):
def test_example_from_the_spec(self):
shots = batch(CHARACTERS, PROMPTS, "657539441289641\n\n", rolls=[42])
self.assertEqual(
[shot.prompt for shot in shots],
[
"A woman in her thirties with short black hair and a grey coat. stands in the rain outside a station.",
"A woman in her thirties with short black hair and a grey coat. hands A tall man with a scar on his left cheek. a letter in a dark hallway.",
],
)
self.assertEqual([shot.seed for shot in shots], [657539441289641, 42])
self.assertEqual(shots[0].character_tokens, ("CHAR1",))
self.assertEqual(shots[1].character_tokens, ("CHAR1", "CHAR2"))
self.assertEqual([shot.plate_id for shot in shots], ["p01", "p02"])
def test_empty_characters_leave_prompts_unchanged(self):
shots = batch("", "p01\nA red door.\n\np02\nA blue door.", "1\n2")
self.assertEqual([shot.prompt for shot in shots], ["A red door.", "A blue door."])
self.assertEqual(shots[0].character_tokens, ())
def test_prompt_that_names_nobody_is_not_rewritten(self):
shots = batch(CHARACTERS, "p01\nAn empty platform.", "7")
self.assertEqual(shots[0].prompt, "An empty platform.")
self.assertEqual(shots[0].character_tokens, ())
def test_unknown_token_stays(self):
shots = batch(CHARACTERS, "p01\nCHAR3 waits.", "7")
self.assertEqual(shots[0].prompt, "CHAR3 waits.")
self.assertEqual(shots[0].character_tokens, ())
def test_longest_token_wins(self):
characters = "CHAR1\nred coat\n\nCHAR10\nblue coat"
shots = batch(characters, "p01\nCHAR10 meets CHAR1.\n\np02\nCHAR100 waves.", "1\n2")
self.assertEqual(shots[0].prompt, "blue coat meets red coat.")
self.assertEqual(shots[0].character_tokens, ("CHAR10", "CHAR1"))
self.assertEqual(shots[1].prompt, "CHAR100 waves.")
def test_description_can_share_the_token_line(self):
shots = batch("CHAR1 blaa blaa blaa", "p01\nCHAR1 waits.", "1")
self.assertEqual(shots[0].prompt, "blaa blaa blaa waits.")
def test_description_is_one_phrase_and_is_not_expanded(self):
characters = "CHAR1\nshort black hair\nand a grey coat, friend of CHAR2\n\nCHAR2\nscarred man"
shots = batch(characters, "p01\nCHAR1.", "1")
self.assertEqual(shots[0].prompt, "short black hair and a grey coat, friend of CHAR2.")
self.assertEqual(shots[0].character_tokens, ("CHAR1",))
def test_blank_lines_inside_a_plate_stay(self):
shots = batch("", "p01\nline one\n\nline two\n\np02_letter\nstill one plate", "3\n4")
self.assertEqual(shots[0].prompt, "line one\n\nline two")
self.assertEqual(shots[1].prompt, "still one plate")
self.assertEqual(shots[1].plate_id, "p02_letter")
def test_text_on_the_header_line_is_the_prompt(self):
shots = batch("", "p01 CHAR1 stands in the rain.\n\np1_station\nsecond", "1\n2")
self.assertEqual(shots[0].prompt, "CHAR1 stands in the rain.")
self.assertEqual(shots[1].plate_id, "p1_station")
def test_p1_and_p001_are_the_same_plate(self):
with self.assertRaises(PromptBatchError) as caught:
batch("", "p1\nfirst\n\np001\nsecond", "1\n2")
self.assertIn("same plate", str(caught.exception))
def test_trailing_underscore_is_still_a_plate(self):
shots = batch("", "p04_\nSTYLE: inkconf\nA bazaar.\n\np05_\nA cockpit.", "1\n2")
self.assertEqual([shot.plate_id for shot in shots], ["p04", "p05"])
self.assertEqual(shots[0].prompt, "STYLE: inkconf\nA bazaar.")
self.assertEqual(shots[1].prompt, "A cockpit.")
def test_p04_and_p04_underscore_are_the_same_plate(self):
with self.assertRaises(PromptBatchError) as caught:
batch("", "p04\nfirst\n\np04_\nsecond", "1\n2")
self.assertIn("same plate", str(caught.exception))
def test_text_before_the_first_plate_is_an_error(self):
with self.assertRaises(PromptBatchError) as caught:
batch("", "CHAR1 stands here.\n\np01\nmore", "1")
self.assertIn("before the first plate id", str(caught.exception))
def test_empty_plate_body(self):
with self.assertRaises(PromptBatchError) as caught:
batch("", "p01\n\np02\nhello", "1\n2")
self.assertIn("p01 has no prompt text", str(caught.exception))
def test_empty_seed_box_rolls_one_seed_per_prompt(self):
shots = batch("", "p01\none\n\np02\ntwo\n\np03\nthree", "", rolls=[4, 5, 6])
self.assertEqual([shot.seed for shot in shots], [4, 5, 6])
def test_blank_seed_line_rolls_and_trailing_newline_does_not_add_a_line(self):
shots = batch("", "p01\none\n\np02\ntwo", "8\n\n", rolls=[9])
self.assertEqual([shot.seed for shot in shots], [8, 9])
def test_missing_seed_lines_roll_the_rest(self):
shots = batch("", "p01\none\n\np02\ntwo\n\np03\nthree", "8\n9", rolls=[4])
self.assertEqual([shot.seed for shot in shots], [8, 9, 4])
def test_extra_seed_lines_are_an_error(self):
with self.assertRaises(PromptBatchError) as caught:
batch("", "p01\none", "1\n2")
self.assertIn("Seeds has 2 lines", str(caught.exception))
self.assertIn("Prompts has 1 prompt", str(caught.exception))
def test_bad_seed(self):
with self.assertRaises(PromptBatchError) as caught:
batch("", "p01\none", "nope")
self.assertIn("Seed line 1", str(caught.exception))
def test_seed_above_ksampler_max(self):
with self.assertRaises(PromptBatchError):
batch("", "p01\none", str(SEED_MAX + 1))
def test_empty_prompts(self):
with self.assertRaises(PromptBatchError) as caught:
batch("", "\n\n", "")
self.assertIn("Prompts is empty", str(caught.exception))
def test_bad_character_token(self):
with self.assertRaises(PromptBatchError) as caught:
batch("Hero\ntall", "Hero walks.", "1")
self.assertIn("Characters block 1", str(caught.exception))
def test_char_zero_is_rejected(self):
with self.assertRaises(PromptBatchError):
batch("CHAR0\nnobody", "CHAR0.", "1")
def test_duplicate_character(self):
with self.assertRaises(PromptBatchError) as caught:
batch("CHAR1\nred\n\nCHAR1\nblue", "CHAR1.", "1")
self.assertIn("defined more than once", str(caught.exception))
def test_character_without_description(self):
with self.assertRaises(PromptBatchError) as caught:
batch("CHAR1\n", "CHAR1.", "1")
self.assertIn("no description", str(caught.exception))
def test_prefix_is_pasted_in_front_of_every_plate(self):
shots = batch(
CHARACTERS,
PROMPTS,
"1\n2",
prefix="35mm photo, natural light\nCHAR1 in frame",
)
self.assertTrue(shots[0].prompt.startswith("35mm photo, natural light\n"))
self.assertIn("stands in the rain", shots[0].prompt)
self.assertIn("short black hair", shots[0].prompt)
self.assertTrue(shots[1].prompt.startswith("35mm photo, natural light\n"))
self.assertIn("CHAR1", shots[0].character_tokens)
self.assertNotIn("p01", shots[0].prompt)
def test_empty_prefix_adds_nothing(self):
shots = batch("", "p01\nA red door.", "1", prefix=" \n")
self.assertEqual(shots[0].prompt, "A red door.")
def test_seeds_are_rolled_for_blanks_and_missing_lines(self):
self.assertTrue(seeds_are_rolled(""))
self.assertTrue(seeds_are_rolled("1\n\n2"))
self.assertFalse(seeds_are_rolled("1\n2\n"))
self.assertFalse(seeds_are_rolled("0"))
# Two written seeds and eleven plates: the tail is chosen at queue time.
self.assertTrue(seeds_are_rolled("8\n9", prompt_count=11))
self.assertFalse(seeds_are_rolled("8\n9", prompt_count=2))
# Extra lines are an error in execute, so they do not count as a roll.
self.assertFalse(seeds_are_rolled("8\n9\n10", prompt_count=2))
if __name__ == "__main__":
unittest.main()