affaan-m/ECC · error · ValueError

analyze() needs at least one frame

Error message

analyze() needs at least one frame

What it means

analyze() computes grade statistics from a list of decoded frames and rejects empty input up front, because downstream concatenation and percentile math would produce NaNs or IndexError on an empty stack. It is a defensive guard for callers that skipped or failed frame extraction.

Solutions

  1. Guard with `if frames:` before calling analyze() and handle the empty case explicitly
  2. Ensure sample_frames() succeeded and returned a non-empty list before analysis
  3. If frames may be filtered, raise or log earlier instead of passing an empty list downstream
  4. Catch ValueError and skip the clip with a warning if empty input is expected sometimes

Example fix

// before
stats = analyze(frames)  # ValueError on empty list
// after
if not frames:
    raise SkipClip("no frames to analyze")
stats = analyze(frames)
Defensive patterns

Strategy: validation

Validate before calling

if not frames:
    raise ValueError("no frames extracted; refusing to analyze")

Try / catch

try:
    stats = analyze(frames)
except ValueError as e:
    if "needs at least one frame" in str(e):
        log.warning("skipping clip: no frames"); stats = None
    else: raise

Prevention

When it happens

Trigger: Calling analyze([]) after sample_frames() returned nothing, after filtering out all frames, or when frame extraction was skipped due to an earlier (swallowed) error.

Common situations: Pipelines that catch the 'decoded zero frames' error and continue with an empty list; masking/filter logic that discards every frame; initializing accumulator lists that never got populated.

Understand the failure class

Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.

Related errors


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

Appendix: source

Thrown at skills/taste-distillation/scripts/taste/grade.py:214

    populated = [i for i, v in enumerate(raw) if v is not None]
    if not populated:
        g = [float(a.mean()), float(a.std()), float(b.mean()), float(b.std())]
        return [list(g) for _ in range(_N_ZONES)]

    out: list[list] = []
    for z in range(_N_ZONES):
        if raw[z] is not None:
            out.append(raw[z])
        else:
            nearest = min(populated, key=lambda p: abs(p - z))
            out.append(list(raw[nearest]))
    return out


def analyze(frames: list[np.ndarray]) -> GradeStats:
    """Distill grade statistics from a list of float32 RGB frames in [0,1]."""
    if not frames:
        raise ValueError("analyze() needs at least one frame")

    labs = [_to_lab(f) for f in frames]
    stacked = np.concatenate([l.reshape(-1, 3) for l in labs], axis=0)
    L, a, b = stacked[:, 0], stacked[:, 1], stacked[:, 2]

    chroma = np.sqrt(a.astype(np.float64) ** 2 + b.astype(np.float64) ** 2)

    return GradeStats(
        zones=_zone_stats(L, a, b),
        lab_mean=[float(L.mean()), float(a.mean()), float(b.mean())],
        lab_std=[float(L.std()), float(a.std()), float(b.std())],
        l_cdf=[float(v) for v in _cdf_of_l(L)],
        black_point=float(np.percentile(L, 1)),
        white_point=float(np.percentile(L, 99)),
        contrast=float(L.std()),
        saturation=float(chroma.mean()),
        warmth=float(b.mean()),
        tint=float(a.mean()),

View on GitHub (pinned to 8321021c54)