DEV Community

Yusuf Khan
Yusuf Khan

Posted on Fully Autonomous

Catch Metronome Editing by Measuring Shot Length Spread in Python

A reel where every cut lands about two seconds after the last one can feel mechanical, even when each shot is fine on its own. You can flag that from the shot lengths alone. Measure how much the shot lengths vary, check for runs of near-identical cuts, and print a verdict.

Two numbers, two different failures

The program below checks each reel against two rules from a small JSON spec.

  1. Coefficient of variation (CV). This is the standard deviation of the shot lengths divided by their mean. Dividing by the mean makes the number scale-free: doubling every shot length leaves CV unchanged. That lets one threshold (min_cv: 0.25) apply to fast and slow reels alike. The code uses pstdev, the population standard deviation, because the reel is the whole set of shots, not a sample of some larger set.
  2. Longest flat run. This walks neighbouring pairs and counts the longest chain of consecutive shots in which each shot is within tolerance of the one before it. Two shots count as near-equal when abs(a - b) / max(a, b) <= 0.10. Dividing by the larger value keeps the comparison symmetric, so (2.0, 2.1) and (2.1, 2.0) give the same answer.

You need both rules because CV ignores order. If you shuffle a reel, its CV stays the same, but its flat run can change completely. A reel can have plenty of overall spread and still contain a stretch that ticks like a clock.

import json
import statistics

SPEC = json.loads("""
{
  "min_cv": 0.25,
  "max_flat_run": 3,
  "tolerance": 0.10,
  "reels": {
    "metronome":   [2.0, 2.1, 2.0, 1.9, 2.0, 2.1, 2.0, 2.0],
    "flat_middle": [4.0, 1.0, 2.0, 2.1, 2.0, 1.9, 5.0, 0.7],
    "varied":      [3.5, 1.2, 0.8, 2.6, 4.8, 1.0, 1.5, 6.0]
  }
}
""")


def coefficient_of_variation(shots):
    return statistics.pstdev(shots) / statistics.mean(shots)


def longest_flat_run(shots, tolerance):
    """Longest stretch of consecutive shots whose neighbours differ by <= tolerance."""
    best = run = 1
    for a, b in zip(shots, shots[1:]):
        run = run + 1 if abs(a - b) / max(a, b) <= tolerance else 1
        best = max(best, run)
    return best


def check_reel(shots, spec):
    cv = coefficient_of_variation(shots)
    flat = longest_flat_run(shots, spec["tolerance"])
    problems = []
    if cv < spec["min_cv"]:
        problems.append(f"CV {cv:.2f} < {spec['min_cv']:.2f}")
    if flat > spec["max_flat_run"]:
        problems.append(f"{flat} near-equal shots in a row > {spec['max_flat_run']}")
    return cv, flat, problems


def bar(seconds, step=0.5):
    return "#" * max(1, round(seconds / step))


verdicts = {}
print(f"Rule: CV >= {SPEC['min_cv']}, at most {SPEC['max_flat_run']} consecutive "
      f"shots within {SPEC['tolerance']:.0%} of each other\n")
for name, shots in SPEC["reels"].items():
    cv, flat, problems = check_reel(shots, SPEC)
    verdicts[name] = not problems
    print(f"{name}: {len(shots)} shots, mean {statistics.mean(shots):.2f}s, "
          f"stdev {statistics.pstdev(shots):.2f}s, CV {cv:.2f}, flat run {flat}")
    print("  " + " ".join(bar(s) for s in shots))
    print("  " + ("PASS" if not problems else "FAIL: " + "; ".join(problems)))
    print()

assert verdicts == {"metronome": False, "flat_middle": False, "varied": True}, verdicts
passed = sum(verdicts.values())
print(f"{passed}/{len(verdicts)} reels pass the variation rule")
Enter fullscreen mode Exit fullscreen mode

What the output shows

Rule: CV >= 0.25, at most 3 consecutive shots within 10% of each other

metronome: 8 shots, mean 2.01s, stdev 0.06s, CV 0.03, flat run 8
  #### #### #### #### #### #### #### ####
  FAIL: CV 0.03 < 0.25; 8 near-equal shots in a row > 3

flat_middle: 8 shots, mean 2.34s, stdev 1.36s, CV 0.58, flat run 4
  ######## ## #### #### #### #### ########## #
  FAIL: 4 near-equal shots in a row > 3

varied: 8 shots, mean 2.67s, stdev 1.81s, CV 0.68, flat run 1
  ####### ## ## ##### ########## ## ### ############
  PASS

1/3 reels pass the variation rule
Enter fullscreen mode Exit fullscreen mode
  • metronome fails both rules. Its CV is 0.03, and every neighbouring pair is within 10%, so the flat run covers all 8 shots.
  • flat_middle is the interesting case. Its CV of 0.58 clears the 0.25 bar easily, because the 4.0s, 1.0s, 5.0s and 0.7s shots widen the spread. A CV-only check would pass it. The flat run check catches the 2.0, 2.1, 2.0, 1.9 stretch in the middle. That is a run of 4, which is over the limit of 3.
  • varied passes. Its CV is 0.68 and no two neighbours are within 10%, so its flat run is 1.

The # bars draw one mark per half second, rounded, with a minimum of one. They are a rough visual aid only. Rounding can make different neighbours look equal. In varied, for example, 1.2s and 0.8s both draw as ##. Read the numbers for the verdict.

The assert pins the expected verdicts

The final assert pins the expected verdicts for these three reels, and it can fail. Suppose someone tightens tolerance or raises max_flat_run until flat_middle passes. The script then stops before the summary line and reports the actual verdicts. Python skips asserts when run with -O. The assert also protects only these three fixtures and says nothing about reels it has never seen.

Where it bites

  • The thresholds are choices, not findings. I picked 0.25, 3 and 10% to separate these example reels. Nothing in the code shows they suit your footage. Tune them against reels you already consider good and bad.
  • The flat run is pairwise, so a ramp can count as flat. In a sequence like 2.0, 2.2, 2.42, each shot is within 10% of its neighbour, so the shots build a flat run even though the cuts are steadily slowing. If a ramp like that runs longer than max_flat_run shots, the rule flags it.
  • Empty reels crash. The statistics functions raise StatisticsError on an empty list. A one-shot reel gets CV 0 and fails, which may or may not be what you want.

Where this stops being enough

This check sees only a list of durations. It does not know where scenes change, where the music beats fall, or which long take is meant to sit still. A reel cut tightly to a steady beat may be metronomic on purpose, and this check is likely to fail it anyway. Treat a FAIL as a prompt to look at that stretch, not as a verdict on the edit. If you need to compare cut timing against audio or content, you need inputs this spec does not carry.

Disclosure: I'm the founder of Homingo. This article was generated with AI tooling; the program above was run before publishing and the output shown is its real output.

If this kind of work is useful to you, the matching skill is here: https://learn.homingo.co.in/skill

Top comments (0)