diff --git a/CHANGELOG.md b/CHANGELOG.md index 46599680..bc34bd8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ - Export settings no longer prints a loader input the node no longer has (a stale `sla_enabled: on`) or SLA's settings while `sla` is off. ### Added -- Shot cut times (H3, off by default): every `[Shot N]` after the first opens with its cut time, spread over the scene's length by text and rounded to whole seconds; a shot whose main point changes is split in two. Prompts that already carry times are left alone. +- Shot cut times (H3, off by default): every `[Shot N]` after the first opens with its cut time, spread evenly over the scene's length, rounded to whole seconds, none shorter than 2 s; a shot whose main point changes is split in two. Prompts that already carry times are left alone. - Shot views (H3, off by default): later shots may open with POV / side / front / from above / below / behind, never the same twice in a row; shot 1 is skipped. - Composer ▸ Enhance shows the final prompt of every run (after shortcuts, $variables and camera moves) even with the enhancer off, one card per scene, with the text before camera moves under it. - Camera moves for `[Shot N]` prompts (Engine settings, H3, off by default): each shot without a camera move gets one — close-up, focus, zoom in, move around — aimed at the most specific thing in it, never a bare ``. No language model; uses spaCy when installed (`spacy` in requirements). diff --git a/conditioning.py b/conditioning.py index 069f4adb..215666e6 100644 --- a/conditioning.py +++ b/conditioning.py @@ -9338,7 +9338,7 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner): grown = (f", {info['before']} -> {info['after']} shots" if info["after"] != info["before"] else "") print(f"[FunPackVideoRefinerV2] shot cuts: Active | {where}: cut times " - f"{', '.join(info['times'])} over {seconds:g}s{grown}") + f"{', '.join(info['times'])} over {seconds:.1f}s{grown}") else: print(f"[FunPackVideoRefinerV2] shot cuts: Inactive | {where}: {info['why']}") return out diff --git a/shot_camera.py b/shot_camera.py index 4d9a49f6..9fac7ffc 100644 --- a/shot_camera.py +++ b/shot_camera.py @@ -307,7 +307,7 @@ def _switch_point(picture, constant, pieces): def add_shot_cuts(text, seconds, seed=0, chance=0.5, pieces=()): """-> (new prompt, info). Shots whose main point changes are split in two; every shot after - the first then opens with its cut time, spread over `seconds` by text length and rounded + the first then opens with its cut time, spread evenly over `seconds` and rounded to whole seconds. A prompt that already carries cut times is left alone. `pieces`: the texts of the user's shortcuts; a shot is only ever cut BETWEEN two of them, so with none given nothing is split (times are still added). @@ -347,12 +347,16 @@ def add_shot_cuts(text, seconds, seed=0, chance=0.5, pieces=()): blocks.append([picture, sound]) if len(blocks) > len(marks) and (len(blocks) > seconds or seconds / len(blocks) < MIN_SHOT_SECONDS): blocks = [[p, s] for p, s in parts] # not enough seconds for the splits - weights = [max(1, len(b[0].strip())) for b in blocks] - total = float(sum(weights)) - times, acc = [], 0.0 - for k in range(len(blocks) - 1): - acc += weights[k] - t = max(round(acc / total * seconds), (times[-1] + 1) if times else 1) + # Equal shares of the scene, rounded to whole seconds, no shot shorter than MIN_SHOT_SECONDS + # (a shortcut's text length says nothing about how long its beat should last: weighting by + # it gave one-second shots next to five-second ones). + n = len(blocks) + gap = MIN_SHOT_SECONDS if n * MIN_SHOT_SECONDS <= seconds else 1 + times = [] + for k in range(1, n): + t = round(k * seconds / n) + t = max(t, (times[-1] if times else 0) + gap) # not too close behind + t = min(t, int(seconds) - (n - k) * gap) # leave room for the shots after times.append(t) if len(blocks) > 1 and times[-1] >= seconds: info["why"] = f"{len(blocks)} shots do not fit in {seconds}s at whole seconds" diff --git a/tests/test_shot_camera.py b/tests/test_shot_camera.py index 0ae8f6ed..36887b87 100644 --- a/tests/test_shot_camera.py +++ b/tests/test_shot_camera.py @@ -347,3 +347,16 @@ def test_a_shot_that_was_given_a_view_still_gets_its_move(): assert added _out, rep = sc.add_camera_moves(viewed, seed=seed, chance=1.0) assert rep[1]["move"], (added, rep[1]["why"]) + + +def test_cut_times_split_the_scene_evenly_with_no_tiny_shots(): + """Text-length weighting gave 00:03 / 00:04 / 00:09 over 15.08s: a one-second shot.""" + pytest.importorskip("spacy") + P = HEAD + "[Shot 1] waves. [Shot 2] nods. [Shot 3] smiles. [Shot 4] sits." + _out, info = sc.add_shot_cuts(P, 15.0833) + secs = [int(t[3:5]) for t in info["times"]] + assert secs == [4, 8, 11] + edges = [0] + secs + [15.0833] + assert all(b - a >= 2 for a, b in zip(edges, edges[1:])) + assert sc.add_shot_cuts(P, 8)[1]["times"] == ["00:02.000", "00:04.000", "00:06.000"] + assert sc.add_shot_cuts(P, 4)[1]["times"] == ["00:01.000", "00:02.000", "00:03.000"]