CRTC vs Ofcom Threshold Differences
The failure this page prevents is a single caption master that is compliant in one territory and rejected in another because CRTC (Canada), Ofcom (UK) and FCC (US) set different numeric ceilings on the same dimensions — a subtitle authored to the FCC’s ~20 cps reading rate overruns Ofcom’s 160–180 wpm ceiling (about 16–18 cps), and a live caption inside CRTC’s latency budget can breach the FCC’s tighter ~2-second synchronicity expectation. When one deliverable ships to all three markets, “compliant” is not a single flag: it is the intersection of three rule sets, and the only safe target is the strictest value on every axis. The table below is the reconciliation, and the Python that follows folds all three regulators into one enforceable configuration so a multi-region validator fails a cue the moment it breaches whichever territory is tightest.
| Dimension | CRTC (Canada) | Ofcom (UK) | FCC (US) | Strictest |
|---|---|---|---|---|
| Reading speed (max) | verbatim, no numeric cap | 160–180 wpm | no numeric cap | 160 wpm |
| Reading rate (cps) | ~15 cps | ~17 cps | ~20 cps | 15 cps |
| Min display / cue | ~1.0 s | ~1.0 s | ~1.0 s | 1.0 s |
| Max display / cue | ~6 s | ~7 s | — | 6 s |
| Live latency (max) | ~5 s | ~3 s | ~2 s | 2 s |
| Accuracy (min) | 98% (NER, live English) | ~98% (measured error) | 99% (pre-recorded) | 99% |
| Positioning | non-overlapping, speaker ID | bottom, ≤2 lines / 37 chars | safe-area, non-obscuring | intersection |
from dataclasses import dataclass
from typing import Optional
@dataclass(frozen=True)
class TerritoryThresholds:
"""One regulator's caption/subtitle limits. None = the regulator sets no
numeric cap on that dimension, so it never constrains the strictest set."""
name: str
reading_speed_wpm: Optional[int] # max words/min the viewer must sustain
max_cps: Optional[float] # max characters/sec (language-agnostic form)
min_display_s: float # min on-screen dwell per subtitle
max_display_s: Optional[float] # max on-screen dwell per subtitle
live_latency_s: Optional[float] # max caption-behind-audio lag for live
accuracy_pct: float # min accuracy the deliverable must reach
# CRTC Broadcasting Regulatory Policy 2011-741 / 2019-308 (Canada).
CRTC = TerritoryThresholds("CRTC", reading_speed_wpm=None, max_cps=15.0,
min_display_s=1.0, max_display_s=6.0,
live_latency_s=5.0, accuracy_pct=98.0)
# Ofcom Code on Television Access Services / best-practice guidelines (UK).
OFCOM = TerritoryThresholds("Ofcom", reading_speed_wpm=160, max_cps=17.0,
min_display_s=1.0, max_display_s=7.0,
live_latency_s=3.0, accuracy_pct=98.0)
# FCC 47 CFR § 79.1 (US) — no numeric reading-speed cap; ~2 s live target.
FCC = TerritoryThresholds("FCC", reading_speed_wpm=None, max_cps=20.0,
min_display_s=1.0, max_display_s=None,
live_latency_s=2.0, accuracy_pct=99.0)
def wpm_to_cps(wpm: int) -> float:
"""~6 chars per word incl. trailing space -> cps = wpm * 6 / 60 = wpm / 10."""
return wpm * 6 / 60.0
def strictest(territories) -> dict:
"""Fold N regulators into the single strictest constraint per dimension:
the min of every ceiling and the max of every floor. A multi-region master
that clears this dict is compliant in every listed territory at once."""
def mn(vals):
v = [x for x in vals if x is not None]
return min(v) if v else None
cps_ceilings = []
for t in territories:
cps_ceilings.append(t.max_cps)
if t.reading_speed_wpm is not None: # fold WPM cap into the cps axis
cps_ceilings.append(wpm_to_cps(t.reading_speed_wpm))
return {
"reading_speed_wpm": mn([t.reading_speed_wpm for t in territories]),
"max_cps": mn(cps_ceilings),
"min_display_s": max(t.min_display_s for t in territories), # tightest floor
"max_display_s": mn([t.max_display_s for t in territories]),
"live_latency_s": mn([t.live_latency_s for t in territories]),
"accuracy_pct": max(t.accuracy_pct for t in territories), # tightest floor
}
def validate(subtitle: dict, limits: dict) -> list:
"""Check one measured subtitle/deliverable against the strictest set."""
out = []
if limits["max_cps"] is not None and subtitle["cps"] > limits["max_cps"]:
out.append(f"reading rate {subtitle['cps']:.1f} cps > {limits['max_cps']:.1f} cps")
if subtitle["display_s"] < limits["min_display_s"]:
out.append(f"dwell {subtitle['display_s']:.2f}s < {limits['min_display_s']}s floor")
if limits["max_display_s"] is not None and subtitle["display_s"] > limits["max_display_s"]:
out.append(f"dwell {subtitle['display_s']:.2f}s > {limits['max_display_s']}s ceiling")
if limits["live_latency_s"] is not None and subtitle.get("latency_s", 0) > limits["live_latency_s"]:
out.append(f"latency {subtitle['latency_s']:.1f}s > {limits['live_latency_s']}s")
if subtitle["accuracy_pct"] < limits["accuracy_pct"]:
out.append(f"accuracy {subtitle['accuracy_pct']:.1f}% < {limits['accuracy_pct']}%")
return out
if __name__ == "__main__":
limits = strictest([CRTC, OFCOM, FCC])
print("strictest applicable set:", limits)
# A live English master authored to UK reading speed but US latency budget.
sample = {"cps": 16.5, "display_s": 1.4, "latency_s": 4.2, "accuracy_pct": 98.4}
for v in validate(sample, limits):
print("VIOLATION:", v)
Code walkthrough
TerritoryThresholds is a frozen dataclass, one instance per regulator, and the design choice that makes reconciliation tractable is the Optional fields. A regulator that imposes no numeric ceiling on a dimension — the FCC and CRTC do not cap reading speed in words per minute — stores None, which the reconciliation reads as “this territory abstains here” rather than “this territory allows anything.” That distinction is what keeps a None from silently loosening the merged set.
wpm_to_cps reconciles the two units the regulators use. Ofcom expresses reading speed in words per minute; the FCC and most QC tooling work in characters per second. Using the standard ~6-characters-per-word estimate (five letters plus a space), the conversion collapses to wpm / 10, so Ofcom’s 160 wpm becomes roughly 16 cps. Folding the converted WPM ceiling into the same cps_ceilings list lets one comparison span both unit systems — without it, a WPM-only limit would never constrain a cps-measured cue.
strictest is the core. For every ceiling — reading rate, maximum dwell, live latency — it takes the minimum across territories; for every floor — minimum dwell, minimum accuracy — it takes the maximum. The helper mn filters out None before comparing so an abstaining regulator cannot win a min. The result is a single dictionary that is simultaneously the CRTC-legal, Ofcom-legal and FCC-legal target: clear it and the master ships to all three markets. This is the same “one config, many jurisdictions” discipline the parent Ofcom code on subtitling standards applies to a single regulator, generalized to three.
validate runs a measured cue against that merged set and returns a list of human-readable breaches, empty when the cue passes. Because latency is optional per cue, it defaults to 0 for pre-recorded content so a file without a latency measurement is never falsely failed on the live-latency axis. The reading-rate check is the same rolling character-per-second logic detailed in enforcing character rate limits in QC, pointed at the strictest ceiling rather than a single regulator’s.
Threshold reference table
A concise second view of the numbers the validator enforces, with each value’s source, so TerritoryThresholds is tuned against a table rather than prose.
| Parameter | CRTC | Ofcom | FCC | Source |
|---|---|---|---|---|
reading_speed_wpm |
— | 160 | — | Ofcom access-services guidance |
max_cps |
15.0 | 17.0 | 20.0 | CRTC 2019-308 / Ofcom / CEA-608 throughput |
min_display_s |
1.0 | 1.0 | 1.0 | Decoder render floor |
max_display_s |
6.0 | 7.0 | — | Ofcom 1–7 s dwell |
live_latency_s |
5.0 | 3.0 | 2.0 | FCC § 79.1 synchronicity / CRTC 2019-308 |
accuracy_pct |
98.0 | 98.0 | 99.0 | FCC pre-recorded / CRTC NER |
Edge cases & known gotchas
- Live vs pre-recorded: the latency and accuracy axes only bind live content. CRTC 2019-308 and the FCC hold pre-recorded programming to a higher accuracy (near-verbatim) but drop the latency requirement, so run two profiles — a live set with the latency floor and a pre-recorded set without it — rather than one blended config.
- French vs English CRTC: CRTC sets separate quality regimes for French- and English-language broadcasts (different NER accuracy targets and reading-speed expectations). A bilingual Canadian deliverable needs a per-language
TerritoryThresholds, not one CRTC instance. - Ofcom children’s programming: Ofcom lowers the reading-speed ceiling for children’s output — closer to 70–120 wpm depending on age band — so a children’s master must swap in a stricter Ofcom instance before reconciliation, or the merged 160 wpm will pass content that is too fast for its audience.
- WPM↔CPS conversion drift: the
wpm / 10shortcut assumes ~6 characters per word; languages with longer average tokens (German) or logographic scripts break that ratio, so measure cps directly for non-English deliverables instead of converting from a WPM cap. - Positioning is not numeric: the positioning row resists a single scalar — Ofcom’s two-line/37-char bottom placement, CRTC speaker identification and the FCC safe-area rule are enforced as separate layout assertions, not folded into
strictest.
Integration hook
This reconciliation is the policy layer that sits above per-cue enforcement. It turns three regulators — cited here as the Ofcom Code on Television Access Services, CRTC Broadcasting Regulatory Policy 2011-741 / 2019-308 and FCC 47 CFR § 79.1 — into one limits dict that any downstream check can consume. Feed it into the validator described by Ofcom code on subtitling standards so a single multi-region master is proven compliant everywhere before it reaches packaging, and reuse the same merged ceiling in the reading-rate stage rather than re-deriving thresholds per territory.
Frequently asked questions
How do I make one caption master pass CRTC, Ofcom and FCC at once? Reconcile the three rule sets to their strictest value on every dimension — minimum of each ceiling, maximum of each floor — and validate the master against that merged set. The strictest() function above produces exactly that target dictionary.
Why convert Ofcom’s words-per-minute into characters per second? Because the FCC and most QC tooling measure reading rate in cps while Ofcom specifies wpm. Using ~6 characters per word, wpm / 10 converts the two so a single comparison spans both units; 160 wpm becomes roughly 16 cps.
Do the accuracy and latency thresholds apply to pre-recorded files? The live-latency axis does not — it binds live captioning only — but accuracy does, and for pre-recorded content it is stricter (near-verbatim, ~99% for the FCC). Run separate live and pre-recorded profiles so a file is not failed on a latency rule that does not apply to it.
Related
- Ofcom subtitle timing requirements explained — the UK dwell and reading-speed rules feeding one column of this comparison.
- Enforcing character rate limits in QC — the per-cue cps/WPM enforcement the merged ceiling drives.
- Ofcom code on subtitling standards — the parent rule set this multi-territory reconciliation extends.
Part of: Broadcast Media Closed Captioning & QC Automation — the architecture and compliance reference for broadcast caption pipelines.