From 2abd7fc30f3fcbe65f215a43b1d28cc62fe74b48 Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Wed, 30 Sep 2026 18:17:04 +0300 Subject: [PATCH] fix: the rewriter's own cut openers and views never become camera targets Co-Authored-By: Claude Sonnet 5.5 --- shot_camera.py | 23 +++++++++++++++++++++-- tests/test_shot_camera.py | 25 +++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/shot_camera.py b/shot_camera.py index 659f0e9d..de7f725a 100644 --- a/shot_camera.py +++ b/shot_camera.py @@ -232,7 +232,7 @@ def add_camera_moves(text, seed=None, chance=1.0): bodies = [text[m.end():(marks[i + 1].start() if i + 1 < len(marks) else len(text))] for i, m in enumerate(marks)] parts = [_split_sound(b) for b in bodies] - cands = [candidates(p[0]) for p in parts] + cands = [candidates(own_words_removed(p[0])) for p in parts] header_lemmas = {c[0] for c in candidates(header)} # In (nearly) every shot = about the whole prompt, not this shot. 70%: with three shots a # noun shared by two of them is still a topic of those two, only all three is a constant. @@ -326,7 +326,7 @@ def add_shot_cuts(text, seconds, seed=0, chance=0.5, pieces=()): return text, info header = text[:marks[0].start()] parts = [_split_sound(b) for b in bodies] - cands = [candidates(p[0]) for p in parts] + cands = [candidates(own_words_removed(p[0])) for p in parts] header_lemmas = {c[0] for c in candidates(header)} need = max(2, math.ceil(0.7 * len(bodies))) if len(bodies) > 1 else 10 ** 9 seen = {} @@ -413,3 +413,22 @@ def add_shot_views(text, seed=0, chance=0.4): last = view out.append(m.group(0) + body) return "".join(out), added + + +_VIEW_SENTENCE = re.compile(r"^\s*(?:" + "|".join(re.escape(v) for v in VIEWS) + r")\.\s*", re.I) + + +def own_words_removed(picture): + """`picture` without what this module wrote into it (its own cut openers and views) and + without the bare cut words of a user-written opener ("At 00:03.500, the camera cuts to"), + whose content after them is kept: those words are not the shot's content and must never + become a target or a topic.""" + picture = _OUR_OPENER.sub("", picture, count=1) + picture = _CUT_WORDS.sub("", picture, count=1) + return _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) + + 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) diff --git a/tests/test_shot_camera.py b/tests/test_shot_camera.py index 6e991e47..98a2a843 100644 --- a/tests/test_shot_camera.py +++ b/tests/test_shot_camera.py @@ -370,3 +370,28 @@ def test_the_phrases_we_write_never_say_the_camera_pulls_out(): """Only what the rewriter writes; a shortcut may say it as an action.""" for group in (sc.MOVES_DETAIL, sc.MOVES_OTHER, sc.MOVES_TRAVEL, sc.MOVES_THEN, sc.MOVES_FINISH): assert not any("pulls out" in m for m in group), group + + +def test_words_the_rewriter_wrote_never_become_targets(): + """'The camera pans from the shot transitions to the tip': the cut opener was read as + the shot's own content.""" + pytest.importorskip("spacy") + P = (HEAD + "[Shot 1] waves. [Shot 2] At 00:04.000, the shot transitions to the next " + "moment. Side view. touches 's chin, then reaches for the lamp.") + _out, rep = sc.add_camera_moves(P, seed=1, chance=1.0) + m = rep[1]["move"] or "" + assert "transition" not in m and "moment" not in m and "view" not in m.lower(), m + assert "chin" in m or "lamp" in m + + +def test_the_full_pipeline_output_has_no_own_words_in_a_move(): + pytest.importorskip("spacy") + P = (HEAD + f"[Shot 1] {PA} [Shot 2] {PB} {PC} " + MUSIC) + for seed in range(20): + a, _ = sc.add_shot_cuts(P, 12, seed=seed, chance=1.0, pieces=PIECES) + b, _ = sc.add_shot_views(a, seed=seed, chance=1.0) + _out, rep = sc.add_camera_moves(b, seed=seed, chance=1.0) + for r in rep: + t = (r["target"] or "").lower() + assert not any(w in t for w in ("transition", "moment", "beat", "new angle", "new view", + "switch", "changes")), t