diff --git a/modules/conditioning/shot_camera/engine.py b/modules/conditioning/shot_camera/engine.py index 481ceb2f..ae28e98f 100644 --- a/modules/conditioning/shot_camera/engine.py +++ b/modules/conditioning/shot_camera/engine.py @@ -75,6 +75,7 @@ MOVES_THEN = ("Then the camera pans to {y}.", "Then the camera racks focus to {y MOVES_FINISH = ("Then the camera zooms out.", "Then the camera pulls back.", "Then the camera pulls back to a wider view.") COUNT_WEIGHTS = (6, 3, 1) # one move / two / three, in a shot that gets any +INFUSED_TRAVEL = 0.5 # of infused shots with a subject of their own, how many travel between the two TRAVEL_SHARE = 0.25 # of one-move shots with two topics, how many travel X -> Y HOLD_SHARE = 0.25 # of the other one-move shots, how many stay still on one thing MOVES_HOLD = ("The camera holds steady on {x}.", "The camera stays still, focused on {x}.", @@ -616,7 +617,11 @@ def add_camera_moves(text, seed=None, chance=1.0, prior=None, choices=None, arms infused = _infused(picture, {c[0] for c in pool}, bank, roll) else: infuse_arm = "infuse:no" - moves, desc, opening, style = _plan(pool, last, rng, shot_prior, force=[infused] if infused else force, mode=mode, arms=arms) + aim = [infused] if infused else force + if infused and roll.random() < INFUSED_TRAVEL: # "from X to Y": the infused word and the shot's own subject, either way round + own = _draw(pool, shot_prior, roll, 1)[0] + aim = [infused, own] if roll.random() < 0.5 else [own, infused] + moves, desc, opening, style = _plan(pool, last, rng, shot_prior, force=aim, mode=mode, arms=arms) entry.update(move=moves, target=desc, infused=infused[0] if infused else None) if rng is not None and mode == "auto" and not force: # only what nobody dictated is learned entry["arms"] = ["move:yes"] + ([f"style:{style}"] if style else []) diff --git a/modules/conditioning/shot_camera/tests/test_infusion.py b/modules/conditioning/shot_camera/tests/test_infusion.py index 9c6f12d3..61ccb0d8 100644 --- a/modules/conditioning/shot_camera/tests/test_infusion.py +++ b/modules/conditioning/shot_camera/tests/test_infusion.py @@ -1,5 +1,7 @@ """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 @@ -35,7 +37,8 @@ def test_at_one_the_focus_is_a_word_from_other_prompts_not_the_shots_own(): out, said, arms = _move(focus_infusion_chance=1, variation=variation) sentence = out.split(WALK)[1] assert "hand" in sentence or "head" in sentence, out - assert "legs" not in sentence and "street" not in sentence, "instead of the shot's own focus" + 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 @@ -134,3 +137,16 @@ def test_the_oldest_projects_are_forgotten_past_the_cap_and_a_forgotten_word_lea 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}"