affaan-m/ECC · error · ValueError

output width and height must be positive

Error message

output width and height must be positive

What it means

forge() derives output geometry from the first take's probe unless width/height overrides are given. If the computed W or H ends up zero or negative (bad probe metadata or a nonsensical override), it raises ValueError because ffmpeg cannot produce a valid video at that size.

Solutions

  1. Pass explicit positive dimensions: forge(..., width=1920, height=1080) or --width/--height on the CLI.
  2. Verify the first take is a real video with dimensions: ffprobe -select_streams v:0 -show_entries stream=width,height take0.mov.
  3. Reorder takes so a healthy video file is first, or re-encode the corrupt take.
  4. Fix the config/override values so width and height are > 0.

Example fix

# before
forge('noir', takes, width=int(cfg['width']), out='edit.mp4')  # cfg width = 0

# after
W = max(int(cfg.get('width', 0)), 1)
H = max(int(cfg.get('height', 0)), 1)
forge('noir', takes, width=W, height=H, out='edit.mp4')
Defensive patterns

Strategy: validation

Validate before calling

import subprocess, json

def probe_dims(path):
    out = subprocess.run(['ffprobe', '-v', 'error', '-select_streams', 'v:0',
                          '-show_entries', 'stream=width,height', '-of', 'json', path],
                         capture_output=True, text=True)
    s = json.loads(out.stdout)['streams'][0]
    return int(s['width']), int(s['height'])  # caller asserts both > 0

Type guard

def has_valid_dims(info):
    return info.width > 0 and info.height > 0

Try / catch

try:
    forge(genre, takes, out=out)
except ValueError as e:
    if 'width and height' in str(e):
        forge(genre, takes, width=1920, height=1080, out=out)

Prevention

When it happens

Trigger: Calling forge(..., width=0) or height=-1; probing a take whose stream reports 0x0 dimensions (corrupt file, audio-only file passed as a take, exotic raw container).

Common situations: Passing an audio file or image-less stream as takes[0]; width/height parsed from config as 0; ffprobe failing to read display dimensions from a damaged recording.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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

Appendix: source

Thrown at skills/taste-application/scripts/forge.py:91

        raise ValueError("base video must be a readable local file")
    if any(not Path(o).is_file() for o in (overlays or [])):
        raise ValueError("all overlays must be readable local files")
    if fps is not None and (not math.isfinite(fps) or fps <= 0):
        raise ValueError("fps must be finite and positive")
    sp = pack_mod.load(genre, root=root)
    tgt = grade_mod.load_stats(sp.grade_path)
    cad = cad_mod.load(sp.cadence_path)

    # Geometry comes from the first take unless overridden; everything else is
    # normalized to it so concat does not silently fail on a size mismatch.
    info0 = frame_mod.probe(takes[0])
    W = width or info0.width
    H = height or info0.height
    FPS = fps if fps is not None else info0.fps
    if not math.isfinite(FPS) or FPS <= 0:
        raise ValueError("source fps must be finite and positive; provide --fps")
    if W <= 0 or H <= 0:
        raise ValueError("output width and height must be positive")
    # Prior timelines reference these shot files. Each run owns a fresh child,
    # including failed runs, so retries cannot erase an existing edit.
    work_root = Path(work)
    work_root.mkdir(parents=True, exist_ok=True)
    work_dir = Path(tempfile.mkdtemp(prefix="run-", dir=work_root)).resolve()
    print(f"forging '{genre}'  ->  {W}x{H} @ {FPS:g}fps")
    print(f"  cadence: mean {cad.mean_shot:.2f}s, {cad.cuts_per_min:.0f} cuts/min, "
          f"variance {cad.rhythm_variance:.2f}")

    total_target = duration or sum(frame_mod.probe(t).duration for t in takes)
    # When apply.py generated these takes it already decided where the cuts
    # fall, and it told the model so ("cut into 6 pieces of ~0.8s"). Re-planning
    # here would silently cut somewhere else, against footage shot for the
    # original plan - so the caller's plan wins when there is one.
    if plan is None:
        # Plan PER TAKE against each take's own length, not by splitting the
        # target across an arbitrary number of groups.
        #

View on GitHub (pinned to 8321021c54)