Files
olivv-cs-ComfyUI-FunPack/modules/conditioning/shot_camera/tests/test_cut_openers.py
T

141 lines
7.9 KiB
Python

"""Every cut opener is a hard cut that reads as a cut, never as a camera move."""
import random
import re
from .. import SETTINGS, engine, nodes
def test_every_opener_is_a_hard_cut_and_no_opener_reads_as_a_camera_move():
for opener in engine.CUT_OPENERS:
assert not engine.CAMERA.search(opener), opener
assert not any(w in opener for w in ("dissolve", "fade", "wipe")), opener
assert engine.CUT.search(opener), opener
assert re.search(r"\b(?:cuts|transitions|changes|switches) to\b", opener), opener
def test_the_transition_slider_at_zero_is_always_a_plain_cut_and_at_one_always_a_mixed_transition():
for seed in range(200):
opener, kind = engine._opener(random.Random(seed), 0.0, None)
assert kind is None and opener in engine.CUT_OPENERS
last, kinds = None, []
for seed in range(300):
opener, last = engine._opener(random.Random(seed), 1.0, last)
assert last is not None, "1 is never a plain cut"
kinds.append(last)
assert len(set(kinds)) == 3, "a mixture of dissolve, fade and wipe"
assert all(a != b for a, b in zip(kinds, kinds[1:])), "never the same kind twice running"
def test_every_phrase_the_module_writes_after_a_cut_time_is_its_own_words_never_content():
for opener in engine.OUR_OPENERS:
assert not engine.CAMERA.search(opener), opener
picture = f" At 00:05.000, {opener}. She waves at the camera."
assert engine.own_words_removed(picture).strip() == "She waves at the camera.", opener
prints = {engine.content_fingerprint(f"[Shot 1] A woman walks in. [Shot 2] At 00:05.000, {o}. She waves at the camera.")
for o in engine.OUR_OPENERS}
assert len(prints) == 1, "a re-roll of the cut wording is the same prompt"
DANCE = "She sways her hips from side to side, smiling at the room."
WAVE = "She raises her hand and waves at the camera with her fingers."
KINDS = ("dissolve", "fades", "wipes")
def _cut(monkeypatch, tmp_path, **values):
monkeypatch.setenv("SHOT_CAMERA_MEMORY", str(tmp_path / "m.json"))
settings = {"cut_same_shot": True, "shot_cuts_chance": 1, **values}
return nodes.rewrite(f"[Shot 1] {DANCE} {WAVE} {DANCE}", settings, seconds=12, pieces=[DANCE, WAVE])[0]
def test_the_slider_default_is_zero_and_zero_leaves_every_cut_plain(monkeypatch, tmp_path):
assert SETTINGS["cut_difference_chance"]["default"] == 0.0
assert engine.add_shot_cuts.__defaults__[-1] == 0.0, "no transition unless asked"
for values in ({}, {"cut_difference_chance": 0}):
out = _cut(monkeypatch, tmp_path, **values)
assert out.count("At 00:") == 2 and not any(k in out for k in KINDS), out
def test_the_slider_set_in_the_node_makes_every_cut_a_transition_and_they_are_mixed(monkeypatch, tmp_path):
out = _cut(monkeypatch, tmp_path, cut_difference_chance=1)
openers = [o for o in engine.OUR_OPENERS for _ in range(out.count(o))]
assert out.count("At 00:") == 2 and len(openers) == 2, out
kinds = [dict((p, k) for k, p in engine.TRANSITIONS).get(o) for o in openers]
assert None not in kinds, "every cut is a transition at 1"
assert kinds[0] != kinds[1], "mixed: never the same kind twice running"
def test_a_transition_the_user_wrote_is_not_given_a_second_opener(monkeypatch, tmp_path):
monkeypatch.setenv("SHOT_CAMERA_MEMORY", str(tmp_path / "m.json"))
text = f"[Shot 1] {DANCE} [Shot 2] The shot cross-dissolves to her hands. {WAVE}"
out, _ = engine.add_shot_cuts(text, 10, seed=1, chance=0, pieces=[DANCE, WAVE])
assert out.count("cross-dissolves") == 1 and sum(out.count(o) for o in engine.OUR_OPENERS) == 0, out
assert "At 00:" in out and "At 00:05.000, the shot cross-dissolves to her hands." in out, out
def test_a_move_is_never_aimed_at_a_transition_word(monkeypatch, tmp_path):
for variation in range(20):
out = _cut(monkeypatch, tmp_path, cut_difference_chance=1, camera_moves=True, camera_moves_chance=1, variation=variation)
for sentence in out.replace("\n", " ").split(". "):
if "camera" in sentence and "At 00:" not in sentence:
assert not any(w in sentence for w in ("black", "white", "angle", "moment", "follows")), sentence
import pytest
@pytest.mark.parametrize("phrase", ["The shot fades to black.", "The shot fades out to black.", "The shot dissolves to her hand.",
"The camera wipes across to her hand.", "The shot cross-dissolves to her hand.",
"Fade to black.", "Cut to her hand.", "Dissolve to her hand.", "A cross-dissolve to her hand.",
"Smash cut to her hand.", "The shot cross-fades to her hand.", "Then the camera cuts to her hand.",
"The camera then cuts to her hand.", "The shot slowly fades to black.",
"The shot quickly wipes across to her hand.", "The shot dissolves into her hand.",
"The scene fades to black."])
def test_the_cut_phrases_the_hint_names_are_kept_without_a_second_opener(monkeypatch, tmp_path, phrase):
monkeypatch.setenv("SHOT_CAMERA_MEMORY", str(tmp_path / "m.json"))
out, _ = engine.add_shot_cuts(f"[Shot 1] {DANCE} [Shot 2] {phrase} {WAVE}", 10, seed=1, chance=0, pieces=[DANCE, WAVE], transition=1.0)
assert sum(out.count(o) for o in engine.OUR_OPENERS) == 0 and phrase.lower()[:12] in out.lower(), out
def test_the_time_before_a_user_written_cut_is_not_part_of_the_prompts_identity(monkeypatch, tmp_path):
monkeypatch.setenv("SHOT_CAMERA_MEMORY", str(tmp_path / "m.json"))
text = f"[Shot 1] {DANCE} [Shot 2] The shot cross-dissolves to her hands. {WAVE}"
prints = {engine.content_fingerprint(engine.add_shot_cuts(text, secs, seed=1, chance=0, pieces=[DANCE, WAVE])[0]) for secs in (12, 20, 30)}
assert prints == {engine.content_fingerprint(text)}, "one prompt however long the video"
def test_between_zero_and_one_the_share_of_transitions_follows_the_slider():
for difference in (0.1, 0.5, 0.9):
hits = sum(engine._opener(random.Random(seed), difference, None)[1] is not None for seed in range(800))
assert abs(hits / 800 - difference) < 0.08, difference
def test_each_cut_rolls_for_itself_so_a_shot_can_mix_plain_and_transition_cuts(monkeypatch, tmp_path):
monkeypatch.setenv("SHOT_CAMERA_MEMORY", str(tmp_path / "m.json"))
text = f"[Shot 1] {DANCE} {WAVE} {DANCE} {WAVE}"
mixes = 0
for seed in range(40):
out, _ = engine.add_shot_cuts(text, 16, seed=seed, chance=1, pieces=[DANCE, WAVE], transition=0.5)
plain = sum(out.count(o) for o in engine.CUT_OPENERS)
mixes += plain > 0 and sum(out.count(p) for _, p in engine.TRANSITIONS) > 0
assert mixes > 5
@pytest.mark.parametrize("stamp", ["At 00:04.000, the camera cuts to the door.", "At 00:03, the camera cuts to her hand.",
"At 0:03.5, the shot cuts to her hand."])
def test_cut_times_the_user_wrote_anywhere_leave_the_prompt_alone(monkeypatch, tmp_path, stamp):
monkeypatch.setenv("SHOT_CAMERA_MEMORY", str(tmp_path / "m.json"))
text = f"[Shot 1] {DANCE} [Shot 2] {WAVE} " + "and she keeps going " * 6 + stamp
out, info = engine.add_shot_cuts(text, 12, seed=1, chance=1, pieces=[DANCE, WAVE], transition=1.0)
assert out == text and "already written" in info["why"], stamp
def test_the_status_line_says_which_transitions_were_used(monkeypatch, tmp_path):
monkeypatch.setenv("SHOT_CAMERA_MEMORY", str(tmp_path / "m.json"))
said = nodes.rewrite(f"[Shot 1] {DANCE} {WAVE} {DANCE}", {"cut_same_shot": True, "shot_cuts_chance": 1, "cut_difference_chance": 1},
seconds=12, pieces=[DANCE, WAVE])[1]
assert "transitions: " in said and any(k in said for k in ("dissolve", "fade", "wipe")), said
plain = nodes.rewrite(f"[Shot 1] {DANCE} {WAVE} {DANCE}", {"cut_same_shot": True, "shot_cuts_chance": 1}, seconds=12, pieces=[DANCE, WAVE])[1]
assert "transitions" not in plain