fix: cut times split the scene evenly, no shot under 2 s

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-09-30 18:07:34 +03:00
co-authored by Claude Sonnet 5.5
parent 389fbba275
commit c3c3f0fe96
4 changed files with 26 additions and 9 deletions
+1 -1
View File
@@ -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 `<Subject N>`. No language model; uses spaCy when installed (`spacy` in requirements).
+1 -1
View File
@@ -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
+11 -7
View File
@@ -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"
+13
View File
@@ -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] <Subject 1> waves. [Shot 2] <Subject 2> nods. [Shot 3] <Subject 1> smiles. [Shot 4] <Subject 2> 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"]