Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
158 lines
7.6 KiB
Python
158 lines
7.6 KiB
Python
"""Focus infusion: a camera focus now and then aims at a word the user's prompts keep returning to."""
|
|
|
|
import re
|
|
|
|
import pytest
|
|
|
|
from .. import engine, memory, nodes
|
|
|
|
WALK = "She walks down the street, her legs moving steadily."
|
|
CAMERA_ON = {"camera_moves": True, "camera_moves_chance": 1}
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def store(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("SHOT_CAMERA_MEMORY", str(tmp_path / "m.json"))
|
|
|
|
|
|
def _habit(*lemmas, prompts=3):
|
|
for k in range(prompts):
|
|
memory.observe(f"p{k}", list(lemmas), "P")
|
|
|
|
|
|
def _move(**values):
|
|
out, said, chose = nodes.rewrite(f"[Shot 1] {WALK}", {**CAMERA_ON, **values}, seconds=5, pieces=[], project="P")
|
|
return out, said, chose["arms"]
|
|
|
|
|
|
def test_at_zero_nothing_changes_even_with_a_habit_on_record():
|
|
_habit("hand", "head")
|
|
base = _move()[0]
|
|
assert _move(focus_infusion_chance=0)[0] == base and "hand" not in base and "head" not in base
|
|
|
|
|
|
def test_at_one_the_focus_is_a_word_from_other_prompts_not_the_shots_own():
|
|
_habit("hand", "head")
|
|
for variation in range(15):
|
|
out, said, arms = _move(focus_infusion_chance=1, variation=variation)
|
|
sentence = out.split(WALK)[1]
|
|
assert "hand" in sentence or "head" in sentence, out
|
|
if " from " not in sentence:
|
|
assert "legs" not in sentence and "street" not in sentence, "instead of the shot's own focus, unless it travels between the two"
|
|
assert "infuse:yes" in arms and any(a in arms for a in ("infused:hand", "infused:head"))
|
|
assert "infused:" in said or "infused" in said
|
|
|
|
|
|
def test_nothing_is_infused_before_there_is_a_habit_or_from_a_word_seen_in_one_prompt():
|
|
out, _, arms = _move(focus_infusion_chance=1)
|
|
assert "infuse:yes" not in arms
|
|
_habit("hand", prompts=1)
|
|
assert memory.infusion_bank("P") == []
|
|
_habit("hand", prompts=3)
|
|
assert dict(memory.infusion_bank("P")).keys() == {"hand"}
|
|
|
|
|
|
def test_the_modules_own_words_are_never_infused():
|
|
_habit("camera", "angle", "moment", "view", "zoom", "fade", "black")
|
|
for variation in range(10):
|
|
_, _, arms = _move(focus_infusion_chance=1, variation=variation)
|
|
assert "infuse:yes" not in arms and not any(a.startswith("infused:") for a in arms)
|
|
|
|
|
|
def test_a_reframe_can_be_infused_too():
|
|
_habit("hand")
|
|
a, b = "She sways her hips from side to side.", "She turns and walks out through the door."
|
|
out, info = engine.add_reframes(f"[Shot 1] {a} {b}", seed=1, chance=1, pieces=[a, b], infuse=1.0, bank=memory.infusion_bank("P"))
|
|
assert "hand" in out.split(a)[1] and "infuse:yes" in info["arms"] and "infused:hand" in info["arms"]
|
|
plain, plain_info = engine.add_reframes(f"[Shot 1] {a} {b}", seed=1, chance=1, pieces=[a, b])
|
|
assert "infuse:yes" not in plain_info["arms"] and "hand" not in plain.split(a)[1]
|
|
|
|
|
|
def test_the_chance_follows_the_slider_exactly_at_the_ends_and_is_tilted_by_ratings_between(tmp_path):
|
|
assert memory.infuse_chance(0.0) == 0.0 and memory.infuse_chance(1.0) == 1.0 and memory.infuse_chance(0.5) == 0.5
|
|
memory.record_run("good", {"views": [], "arms": ["infuse:yes", "infused:hand"]})
|
|
memory.on_rating("good", "liked")
|
|
assert memory.infuse_chance(0.5) > 0.5 and memory.infuse_chance(1.0) == 1.0
|
|
memory.on_rating("good", "liked") # same rating again changes nothing
|
|
for k in range(8):
|
|
memory.record_run(f"bad{k}", {"views": [], "arms": ["infuse:yes", "infused:hand"]})
|
|
memory.on_rating(f"bad{k}", "disliked")
|
|
assert 0.0 < memory.infuse_chance(0.5) < 0.5 and memory.infuse_chance(0.0) == 0.0
|
|
|
|
|
|
def test_a_dislike_blames_the_infused_shots_first_and_a_change_of_mind_takes_it_back():
|
|
memory.record_run("r", {"views": [], "arms": ["move:yes", "style:hold", "word:legs", "infuse:yes", "infused:hand"]})
|
|
memory.on_rating("r", "disliked")
|
|
stats = memory.arm_stats()
|
|
assert stats["infuse:yes"][1] > 0 and stats["infused:hand"][1] > 0
|
|
assert stats.get("style:hold", (0, 0))[1] == 0 and stats.get("move:yes", (0, 0))[1] == 0, "the rest of the run is not blamed"
|
|
memory.on_rating("r", "liked")
|
|
stats = memory.arm_stats()
|
|
assert stats["infuse:yes"] == (0.5, 0.0) or stats["infuse:yes"][1] == 0, "the blame was taken back"
|
|
assert stats["style:hold"][0] > 0
|
|
|
|
|
|
def test_a_run_without_infusion_shares_the_blame_as_before():
|
|
memory.record_run("r", {"views": [], "arms": ["move:yes", "style:hold", "infuse:no"]})
|
|
memory.on_rating("r", "disliked")
|
|
stats = memory.arm_stats()
|
|
assert stats["move:yes"][1] > 0 and stats["style:hold"][1] > 0 and stats["infuse:no"][1] > 0
|
|
|
|
|
|
def test_one_dislike_lowers_a_word_but_never_removes_it_and_a_like_brings_it_back():
|
|
_habit("hand", "head")
|
|
start = dict(memory.infusion_bank("P"))["hand"]
|
|
memory.record_run("r", {"views": [], "arms": ["infuse:yes", "infused:hand"]})
|
|
memory.on_rating("r", "disliked")
|
|
after = dict(memory.infusion_bank("P"))
|
|
assert 0 < after["hand"] < start and after["head"] == dict(memory.infusion_bank("P"))["head"] == start
|
|
for k in range(3):
|
|
memory.record_run(f"l{k}", {"views": [], "arms": ["infuse:yes", "infused:hand"]})
|
|
memory.on_rating(f"l{k}", "liked")
|
|
assert dict(memory.infusion_bank("P"))["hand"] > after["hand"]
|
|
|
|
|
|
def test_the_probe_lists_each_subject_with_what_ratings_taught_and_forgetting_clears_it():
|
|
_habit("hand", "head")
|
|
memory.record_run("r", {"views": [], "arms": ["infuse:yes", "infused:hand"]})
|
|
memory.on_rating("r", "disliked")
|
|
rows = {r["word"]: r for r in memory.summary()["infusion"]}
|
|
assert rows["hand"]["bad"] > 0 and rows["hand"]["seen"] == 3 and rows["head"]["bad"] == 0
|
|
assert all(not a["arm"].startswith("infused:") for a in memory.summary()["arms"]), "shown once, as a subject"
|
|
assert memory.forget("word", "hand")
|
|
assert "infused:hand" not in memory.arm_stats()
|
|
|
|
|
|
def test_a_project_only_aims_at_words_its_own_prompts_keep_returning_to():
|
|
_habit("hand", "head") # project P
|
|
assert dict(memory.infusion_bank("P")).keys() == {"hand", "head"}
|
|
assert memory.infusion_bank("other") == [] and memory.infusion_bank("") == [], "an unrelated project, or no project, has no bank"
|
|
out, _, chose = nodes.rewrite(f"[Shot 1] {WALK}", {**CAMERA_ON, "focus_infusion_chance": 1}, seconds=5, pieces=[], project="other")
|
|
assert "infuse:yes" not in chose["arms"] and "hand" not in out and "head" not in out
|
|
|
|
|
|
def test_the_oldest_projects_are_forgotten_past_the_cap_and_a_forgotten_word_leaves_every_project():
|
|
for k in range(memory.MAX_PROJECTS + 3):
|
|
memory.observe("x", ["lamp"], f"proj{k}")
|
|
kept = memory._read()["projects"]
|
|
assert len(kept) == memory.MAX_PROJECTS and "proj0" not in kept and f"proj{memory.MAX_PROJECTS + 2}" in kept
|
|
assert memory.forget("word", "lamp") and all("lamp" not in r["seen"] for r in memory._read()["projects"].values())
|
|
|
|
|
|
def test_an_infused_shot_often_travels_from_its_own_subject_to_the_infused_word_or_back():
|
|
text = "[Shot 1] A woman holds a red umbrella and a lantern while she walks along the wet street."
|
|
travels = 0
|
|
for seed in range(200):
|
|
out, report = engine.add_camera_moves(text, seed=seed, chance=1, infuse=1.0, bank=[("window", 1.0)])
|
|
assert report[0]["infused"] == "window"
|
|
added = out[len(text):]
|
|
if re.search(r"from .+ to .+", added):
|
|
assert "window" in added, added
|
|
travels += 1
|
|
assert 60 < travels < 140, f"about half travel, got {travels}"
|
|
|
|
|
|
def test_a_word_the_shot_already_says_is_not_infused_even_when_it_is_not_a_candidate_of_the_shot():
|
|
got = [engine._infused("A woman walks.", set(), [("woman", 1.0)], __import__("random").Random(k)) for k in range(20)]
|
|
assert got == [None] * 20
|