affaan-m/ECC · error · ValueError

fps must be a reduced positive rational

Error message

fps must be a reduced positive rational

What it means

_rate validates an fps object as a {numerator, denominator} pair of positive integers and additionally requires gcd(n, d) == 1, i.e. the rational must be in lowest terms. Non-reduced fractions like 48/2 raise ValueError('fps must be a reduced positive rational').

Solutions

  1. Reduce the fraction: divide both parts by math.gcd(numerator, denominator) before inserting into the bundle
  2. Use Fraction(n, d) and write Fraction.numerator/.denominator, which are always reduced
  3. Ensure both parts are >= 1 (zero or negative parts also fail _integer)

Example fix

// before
fps = {"numerator": 48, "denominator": 2}
// after
from math import gcd
fps = {"numerator": 48 // gcd(48, 2), "denominator": 2 // gcd(48, 2)}  # 24/1
Defensive patterns

Strategy: validation

Validate before calling

from math import gcd
def reduced_rate(n, d):
    assert n >= 1 and d >= 1 and gcd(n, d) == 1, f"unreduced rate {n}/{d}"
    return {"numerator": n, "denominator": d}

Try / catch

try:
    bundle = build_application_bundle(data)
except ValueError as e:
    if "reduced positive rational" in str(e):
        print("reduce the fps fraction with gcd before building the bundle")
    else:
        raise

Prevention

When it happens

Trigger: Building a bundle with fps given as 30000/1001 un-normalized after multiplication, or any numerator/denominator sharing a common factor (e.g. 24/2 instead of 12/1).

Common situations: Computing rates by multiplying/editing rationals without re-reducing, copying rates from tools that emit unreduced fractions, or hand-writing 30000/1001-style rates after scaling.

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/d8f168f6103858e2. Report an issue: GitHub.

Appendix: source

Thrown at skills/taste-application/scripts/tasteforge/integration.py:63


def _text(value: Any) -> str:
    if not isinstance(value, str) or not value.strip():
        raise ValueError("nonempty text required")
    return value


def _integer(value: Any, minimum: int = 0) -> int:
    if type(value) is not int or value < minimum:
        raise ValueError("frame/count must be an exact integer in range")
    return value


def _rate(value: Any) -> tuple[int, int]:
    _object(value, {"numerator", "denominator"})
    n, d = (_integer(value[k], 1) for k in ("numerator", "denominator"))
    if math.gcd(n, d) != 1:
        raise ValueError("fps must be a reduced positive rational")
    return n, d


def _range(value: Any, bounds: list[int] | None = None) -> list[int]:
    if not isinstance(value, list) or len(value) != 2:
        raise ValueError("range must contain two frame integers")
    start, end = (_integer(v) for v in value)
    if start >= end or (bounds is not None and (start < bounds[0] or end > bounds[1])):
        raise ValueError("frame range is empty or outside its bounds")
    return value


def _overlap(a: list[int], b: list[int]) -> bool:
    return a[0] < b[1] and b[0] < a[1]


def _sha(value: Any) -> str:
    if not isinstance(value, str) or not re.fullmatch(r"[a-f0-9]{64}", value):

View on GitHub (pinned to 8321021c54)