From 8ebfe4d85d4485427aba148474f8e90dbaef508d Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Tue, 13 Oct 2026 04:47:47 +0300 Subject: [PATCH] fix: transition phrases are the module's own words (no camera moves aimed at them), generic cut wording, no distance-only cuts, the slider is always shown Co-Authored-By: Claude Sonnet 5.5 --- modules/conditioning/shot_camera/__init__.py | 3 +-- modules/conditioning/shot_camera/engine.py | 27 ++++++++++--------- .../shot_camera/tests/test_cut_openers.py | 10 +++++++ 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/modules/conditioning/shot_camera/__init__.py b/modules/conditioning/shot_camera/__init__.py index cb2e0f19..0be31a1d 100644 --- a/modules/conditioning/shot_camera/__init__.py +++ b/modules/conditioning/shot_camera/__init__.py @@ -43,8 +43,7 @@ SETTINGS = { "hint": "1 = always cut between those shortcuts, 0 = never. Ratings nudge the values in between.", "when": {"cut_same_shot": True}}, "cut_difference_chance": {"type": "float", "default": 0.0, "min": 0.0, "max": 1.0, "step": 0.05, "label": "Chance of an unusual transition", - "hint": "0 = every cut is a plain cut. In between, that share of cuts is a dissolve, fade or wipe. 1 = every cut is one of those, mixed.", - "when": {"cut_same_shot": True}}, + "hint": "0 = plain cuts only. In between, that share of cuts is a dissolve, fade or wipe. 1 = every cut is one, mixed. A cut phrase you wrote yourself stays as written."}, "reframe_same_shot": {"type": "bool", "default": False, "label": "Reframe within the shot", "hint": "Where one shortcut ends and the next begins inside a shot that is not cut, the camera moves on to what the next action is about, without a cut: one continuous take, the actions follow on. The view may turn or stay. A shot that reframes gets no extra camera move."}, "reframe_chance": {"type": "float", "default": 0.5, "min": 0.0, "max": 1.0, "step": 0.05, "label": "Chance of a reframe", diff --git a/modules/conditioning/shot_camera/engine.py b/modules/conditioning/shot_camera/engine.py index 96c6c5b8..6593f74a 100644 --- a/modules/conditioning/shot_camera/engine.py +++ b/modules/conditioning/shot_camera/engine.py @@ -605,19 +605,20 @@ STAMP = re.compile(r"\b\d\d:\d\d\.\d{3}\b") # Dissolve, fade and wipe are for when the user asks for them, never default. # Each cut says what is new: a new angle, place, subject, viewpoint, state or moment. Only these four verbs. # The guide allows dissolve, fade and wipe only when the user asks for one: the transition slider is that request. +# Generic on purpose: a phrase that claims a new place or subject would be false for a cut inside one scene. TRANSITIONS = (("dissolve", "the shot cross-dissolves to a new angle"), - ("dissolve", "the shot cross-dissolves to a new location"), - ("fade", "the shot fades out to black and fades in on a new angle"), - ("fade", "the shot fades to white and back to the next moment"), + ("dissolve", "the shot cross-dissolves to the next moment"), + ("fade", "the shot fades out to black and fades in on the next moment"), + ("fade", "the shot fades to white and back to what follows"), ("wipe", "the shot wipes across to a new angle"), - ("wipe", "the shot wipes across to a new subject")) -CUT_OPENERS = ("the camera cuts to a new angle", "the camera cuts to a closer view", - "the shot cuts to a new angle", "the shot cuts to a new location", "the shot cuts to a later moment", - "the shot transitions to the next moment", "the shot transitions to a new location", - "the shot transitions to a new point of view", - "the shot changes to a new view", "the shot changes to a different subject", - "the shot changes to a new state of the scene", - "the shot switches to what follows", "the shot switches to a wider view") + ("wipe", "the shot wipes across to what follows")) +# The approved cut verbs only (cuts, transitions, changes, switches); never a distance-only change, which the guide leaves to camera motion. +CUT_OPENERS = ("the camera cuts to a new angle", "the camera cuts to the next moment", + "the shot cuts to a new angle", "the shot cuts to what follows", + "the shot transitions to the next moment", "the shot transitions to a new view", + "the shot changes to a new view", "the shot changes to what comes next", + "the shot switches to what follows", "the shot switches to the next moment") +OUR_OPENERS = CUT_OPENERS + tuple(phrase for _, phrase in TRANSITIONS) # everything this module writes after a cut time LEAD_CUT = re.compile(r"^\s*(?:the\s+)?(?:camera|shot)\s+(?:cuts|transitions|changes|switches)\s+to\b", re.I) MIN_SHOT_SECONDS = 2 @@ -979,7 +980,7 @@ def own_words_removed(picture): return _REFRAME.sub("", _VIEW_SENTENCE.sub("", picture, count=1)) -_OUR_OPENER = re.compile(r"^\s*At \d\d:\d\d\.\d{3},\s*(?:" + "|".join(re.escape(o) for o in CUT_OPENERS) +_OUR_OPENER = re.compile(r"^\s*At \d\d:\d\d\.\d{3},\s*(?:" + "|".join(re.escape(o) for o in OUR_OPENERS) + r")\.\s*", re.I) _CUT_WORDS = re.compile(r"^\s*(?:At \d\d:\d\d\.\d{3},\s*)?(?:the\s+)?(?:camera|shot)\s+" r"(?:cuts|transitions|changes|switches)\s+to\b\s*", re.I) @@ -990,7 +991,7 @@ def content_fingerprint(text): labels this module added on that run, so regenerating one prompt is one prompt.""" import hashlib body = re.sub(r"\[\s*Shot\s+\d+\s*\]", " ", text or "", flags=re.I) - body = re.sub(r"At \d\d:\d\d\.\d{3},\s*(?:" + "|".join(re.escape(o) for o in CUT_OPENERS) + r")\.", + body = re.sub(r"At \d\d:\d\d\.\d{3},\s*(?:" + "|".join(re.escape(o) for o in OUR_OPENERS) + r")\.", " ", body, flags=re.I) body = _REFRAME.sub(" ", body) body = re.sub(r"\s+", " ", body) diff --git a/modules/conditioning/shot_camera/tests/test_cut_openers.py b/modules/conditioning/shot_camera/tests/test_cut_openers.py index e6087540..8e43d3be 100644 --- a/modules/conditioning/shot_camera/tests/test_cut_openers.py +++ b/modules/conditioning/shot_camera/tests/test_cut_openers.py @@ -26,3 +26,13 @@ def test_the_transition_slider_at_zero_is_always_a_plain_cut_and_at_one_always_a 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"