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
- Pass explicit positive dimensions: forge(..., width=1920, height=1080) or --width/--height on the CLI.
- Verify the first take is a real video with dimensions: ffprobe -select_streams v:0 -show_entries stream=width,height take0.mov.
- Reorder takes so a healthy video file is first, or re-encode the corrupt take.
- 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
- Pass explicit --width/--height when take metadata is suspect.
- Ensure takes[0] is a real video with video streams, not audio-only or corrupt.
- Probe dimensions of the first take before forging and assert they are positive.
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
- analyze() needs at least one frame
- asset must contain a video stream
- fps must be positive, got
- merge_videos() needs at least one video URL
- nothing to concatenate
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)