affaan-m/ECC · error · ValueError

cadence shot cannot occupy a whole frame

Error message

cadence shot cannot occupy a whole frame

What it means

While walking the planned cadence shots, `_strict_assign` accumulates elapsed time and converts it to a frame boundary. If a shot is so short that the boundary does not advance past the current assigned frame (`boundary <= assigned`), the shot cannot occupy a whole frame and the strict no-repeat assignment is impossible, so it raises.

Solutions

  1. Remove or merge shots shorter than one frame period (1/fps seconds) from the cadence
  2. Regenerate the cadence with a higher minimum-shot-length threshold (_MIN_SHOT)
  3. Use the same fps for planning and application as during measurement
  4. Filter the planned durations through plan_shots with a floor before strict assignment

Example fix

// before
{"shots": [{"duration": 0.001}, {"duration": 0.8}]}
// after
min_shot = 1.0 / fps
{"shots": [{"duration": 0.8}]}  # sub-frame shot dropped/merged
Defensive patterns

Strategy: validation

Validate before calling

frame = 1.0 / fps
shots = [s for s in cadence.get("shots", []) if s.get("duration", 0) >= frame]
planned = plan_shots({"shots": shots, "mean_shot": cadence.get("mean_shot", frame)}, target)

Try / catch

try:
    report = apply_local(...)
except ValueError as exc:
    if "whole frame" in str(exc):
        cadence = drop_subframe_shots(cadence, 1.0 / fps)
        report = apply_local(...)  # retry with cleaned cadence
    else:
        raise

Prevention

When it happens

Trigger: A cadence containing a very short shot (e.g. 0.001s at 30fps, far less than one frame period) such that `seconds_to_frames(elapsed, fps)` does not advance by at least one frame from the previous boundary.

Common situations: Over-aggressive shot detection producing sub-frame cuts; camera-flash or strobe artifacts detected as shots; cadence.json generated at a different fps than the one used at apply time (e.g. measured at 60fps, applied at 24fps making shots sub-frame).

Understand the failure class

Background: "value must be between 0 and 1" / "out of range" / "must not be negative" errors: fixing range-validation failures across open-source libraries — this error's family across 42 libraries.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/bb31fa08454ac369. Report an issue: GitHub.

Appendix: source

Thrown at skills/taste-application/scripts/tasteforge/apply.py:71


def _strict_assign(
    planned: list[float], media: list[dict[str, Any]], target: float, fps: float
) -> list[tuple[dict[str, Any], int]]:
    """Fill the target frame count, then match whole shots to unique sources."""
    target_frames = timeline.seconds_to_frames(target, fps)
    if target_frames < 1:
        raise ValueError("target duration must contain at least one frame")
    frame_counts = []
    elapsed = 0.0
    assigned = 0
    for duration in planned:
        if assigned == target_frames:
            break
        elapsed += duration
        boundary = min(target_frames, timeline.seconds_to_frames(elapsed, fps))
        if boundary <= assigned:
            raise ValueError("cadence shot cannot occupy a whole frame")
        frame_counts.append(boundary - assigned)
        assigned = boundary
    if assigned < target_frames:
        frame_counts.append(target_frames - assigned)

    rate = timeline.fps_fraction(fps)
    sources: dict[str, tuple[dict[str, Any], int]] = {}
    for clip in media:
        path = str(Path(clip["path"]).expanduser().resolve())
        # Floor rational capacity: rounding up could read past the source end.
        capacity = math.floor(Fraction(str(clip["duration"])) * rate)
        # Accept a boundary serialized as a float only when the frame duration
        # itself compares within the supplied duration; no broad epsilon.
        if float((capacity + 1) / rate) <= clip["duration"]:
            capacity += 1
        if path in sources:
            raise ValueError("no-repeat media must contain unique normalized source paths")
        sources[path] = ({**clip, "path": path}, capacity)

View on GitHub (pinned to 8321021c54)