- name
- qc-skill
- description
- Deterministic media quality control / validation. Measures and checks video, audio, subtitle, and delivery artifacts - a single file, or (kind=delivery_package) N named artifacts (video + subtitle + thumbnail + metadata, ...) validated together as one delivery - against caller-supplied rules, and returns structured PASS/WARN/FAIL/UNKNOWN results with evidence. Use when you need to know facts about a media file (resolution, fps, codec, loudness, silence, black/freeze frames, subtitle timing/coverage) or whether it meets an explicit spec (expected resolution/fps/codec, loudness target+tolerance, subtitle sync tolerance, required companion artifacts). Do NOT use this skill to decide whether media may ship, to edit/re-render/repair media, to generate or rewrite subtitles, or to transcribe/understand content semantically - those belong to video-production-agent (decisions), audio-production-skill / video-editing-skill (execution), subtitle-skill (generation), and transcription-skill (ASR).
# qc-skill
## Commands
```bash
qc contract --json
qc doctor --json
qc run <request.json | -> --json [--workspace DIR] [--allowed-input-root DIR]...
```
## Request parameters
| Field | Type | Meaning |
|---|---|---|
| `operation` | `inspect` \| `check` \| `validate` | measure only, or measure+evaluate rules |
| `kind` | `video` \| `audio` \| `subtitle` \| `delivery` \| `delivery_package` | what `input` (or `artifacts`) is |
| `input` | string | path to the primary artifact (not accepted for `kind: delivery_package`) |
| `subtitle` | string, optional | companion subtitle path (`kind: delivery` only) |
| `reference_video` | string, optional | companion video path, for duration comparison (`kind: subtitle` only) |
| `artifacts` | array, optional | `[{artifact_id, artifact_type, path}, ...]` - N named artifacts validated together as one delivery (`kind: delivery_package` only, required and non-empty for it) |
| `parameters` | object, optional | detection sensitivity overrides (e.g. `silence_threshold_db`) |
| `rules` | object, optional | typed expectations: `{video, audio, subtitle, delivery, delivery_package}` (see `src/qc_skill/rules.py`) |
| `cache_policy` | `use` \| `bypass` \| `only` | default `use` |
| `timeout` | number, optional | seconds of wall clock for the whole request; when it runs out every running ffprobe/ffmpeg is killed with its process group and the call fails with `TIMEOUT` (exit 15, retryable) - ADR-019 |
Never send `command`, `argv`, `args`, `shell`, `cmd`, `exec`, `executable`,
`filter`, `filter_complex`, or `env` - the request schema rejects all of
them outright, at any nesting depth.
For `kind: "delivery_package"`, `rules.delivery_package.cross_artifact`
adds typed relationship checks across the named artifacts:
`duration_consistency` (durations must agree within a tolerance) and
`dependencies` (if artifact A is present, artifact B must be too) - never
a string comparison or semantic/LLM judgment. See `docs/checks.md`.
`rules.subtitle.timeline_integrity` (also reachable via `delivery`'s and
`delivery_package`'s nested `subtitle` sub-rule) checks whether a
subtitle's delivery-timeline cue timing still matches a caller-supplied
source-to-delivery `timeline` mapping after a trim/concat/speed edit -
qc-skill never constructs that mapping itself, only compares against
one the caller supplies. See `docs/checks.md`.
`VideoRule.max_single_luminance_excursion_sec`/
`max_total_luminance_excursion_sec` check for video signal outside the
legal 8-bit range (default 16-235) via ffmpeg's `signalstats` (a literal
per-frame pixel readout, not a heuristic classifier). See `docs/checks.md`.
Three opt-in `VideoRule` checks (each measured only when asked, ADR-016/017/018):
- `check_photosensitivity: true` - ITU-R BT.1702 general flash (20 cd/m2
changes, darker side < 160 cd/m2, > 25% of the screen, more than
`max_flashes_per_second` (default 3) in any 1 s) -> `VIDEO_FLASH_RISK`.
Red flash is not defined by the standard and is reported UNKNOWN.
- `expected_cuts: [seconds]` - compares scdet cut times with your cut
list (`max_cut_error_frames` default 1, `allow_extra_cuts` default
true) -> `VIDEO_CUT_MISMATCH`, with matched/missing/extra in evidence.
- `text_regions: [{t, box: [x, y, w, h], role: hook|cta|caption|term}]` -
WCAG contrast ratio (`min_contrast_ratio` default 4.5) and ink height
at a 1080 px short side (`min_text_height_px` per role, no default)
-> `TEXT_ILLEGIBLE`. Measured facts only; no OCR.
One opt-in full-reference comparison (ADR-021), `kind: video` or
`rules.delivery.video`, against the top-level `reference_video`:
- `reference_metrics: ["vmaf", "ssim", "psnr"]` (any subset) ->
`video.vmaf_meets_minimum` (`min_vmaf` default 93),
`video.ssim_meets_minimum` (`min_ssim` default 0.95),
`video.psnr_meets_minimum` (`min_psnr_db` default 31). A smaller input
is upscaled to the reference's size; a different frame rate or a length
more than one frame apart is refused (checks UNKNOWN, reason in
`video.reference_alignment`). Without libvmaf in the host's ffmpeg (see
doctor `filter:libvmaf`) VMAF is UNKNOWN and SSIM/PSNR still run.
## Example
```json
{
"operation": "check",
"kind": "audio",
"input": "/workspace/mix.wav",
"rules": {"audio": {"integrated_loudness_target_lufs": -16, "integrated_loudness_tolerance_lu": 1.0, "max_true_peak_dbfs": -1.0}}
}
```
Response: `{"status": "completed", "report": {"overall_status": "PASS"|"WARN"|"FAIL"|"UNKNOWN", "checks": [...], "measurements": [...], "findings": [...]}, "provenance": {...}}`.
## Workflow hints for an agent
1. Call `doctor --json` once per environment to confirm ffmpeg/ffprobe and
the filters you need (`blackdetect`, `freezedetect`, `ebur128`,
`astats`, `silencedetect`) are `AVAILABLE`, not just installed-in-theory.
Before asking for VMAF, check `checks["filter:libvmaf"].status`.
2. Use `inspect` first when you don't yet know what to expect (e.g. before
you have a delivery spec) - it returns pure measurements you can reason
over yourself.
3. Use `check`/`validate` with explicit `rules` once you know the target
spec. Never assume a default target (loudness, resolution, fps) - if
the caller doesn't supply one, the corresponding check is simply not
run, and `qc-skill` will not guess.
4. Treat `report.overall_status: "FAIL"` as **information**, not failure of
your own operation - `status: "completed"` means the QC run itself
succeeded. React to `findings[].code` to decide what to do next (that
decision is yours, not qc-skill's).
5. A response `status: "failed"` (with an `error.code`) means the QC run
itself could not complete - handle it like any other tool error.
## Boundaries
qc-skill never: makes a publish/block/re-render decision, edits or
re-encodes media, generates or rewrites subtitles, transcribes audio,
performs semantic/scene understanding, executes a caller-supplied filter
graph or shell command, or fabricates a measurement it could not actually
take (an unmeasurable value is `null` with a note, or the check is
`UNKNOWN` with a `reason` - never a guess).
View on GitHub