qc-skill

September 6, 2026 ยท View on GitHub

Deterministic media quality control / validation. qc-skill 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 - and reports structured PASS / WARN / FAIL / UNKNOWN results with the evidence behind each one.

qc-skill is not an AI agent and does not make production decisions. It never decides whether a video may ship, whether it should be re-rendered, or whether a defect should be fixed. It answers exactly one question - "what did we measure, and did it pass the rules we were given?"

  • and returns that as data. Turning a QC result into a decision (block delivery, re-render, ask a human) belongs to video-production-agent.

Ecosystem and responsibilities

SkillResponsibility
ffmpeg-skilllow-level ffmpeg/ffprobe execution primitives
media-analysis-skillgeneral-purpose media observation/analysis
audio-production-skillaudio processing execution (gain, trim, normalize, mix, ...)
video-editing-skill / subtitle-skillediting and subtitle execution
qc-skillmeasurement + rule evaluation + structured verdicts. Never decides, never edits.
video-production-agentturns Observations (including QC reports) into Decisions and a ProductionPlan

A QC FAIL is not an error. Running qc check against a broken file and getting back {"status": "completed", ..., "overall_status": "FAIL"} is qc-skill working correctly - the execution succeeded, and the finding is that the media does not meet the rule it was checked against. Only a genuine execution problem (bad request, missing ffmpeg, a file that doesn't exist, a corrupted cache) is a structured error with a non-zero exit code. See docs/architecture.md.

CLI usage

qc contract --json          # what this skill can do
qc doctor --json            # is ffmpeg/ffprobe actually available right now
qc run request.json --json  # or: qc run - --json < request.json

A request document:

{
  "operation": "check",
  "kind": "video",
  "input": "/workspace/output/final.mp4",
  "rules": {
    "video": {
      "expected_width": 1920,
      "expected_height": 1080,
      "expected_frame_rate": 29.97,
      "max_single_black_sec": 2.0
    }
  }
}

--workspace and --allowed-input-root are CLI flags, supplied by whoever invokes the process - never by the request body itself (see docs/security.md).

What it returns

{
  "schema": "qc/response@1",
  "status": "completed",
  "skill": {"id": "qc", "version": "0.1.0"},
  "reused": false,
  "cache": {"status": "miss", "policy": "use", "key": "..."},
  "provenance": {
    "skill": "qc", "skill_version": "0.1.0", "operation": "check",
    "engine": {"ffmpeg_version": "6.1.1", "ffprobe_version": "6.1.1"},
    "input": {"fingerprint": "sha256:...", "size_bytes": 12345678},
    "identity": "...", "observed_at": "2026-01-01T00:00:00Z",
    "measurement_source": "OBSERVED"
  },
  "report": {
    "id": "qcreport_...", "version": "1", "operation": "check", "kind": "video",
    "overall_status": "FAIL",
    "checks": [
      {"check_id": "video.resolution_matches_expected", "category": "video", "status": "PASS", "measurement_ids": ["video.width", "video.height"], "finding_codes": [], "evidence": {}},
      {"check_id": "video.black_frames_within_tolerance", "category": "video", "status": "FAIL", "measurement_ids": ["video.black_segments"], "finding_codes": ["VIDEO_BLACK_FRAMES_EXCEEDED"], "evidence": {"total_sec": 3.2, "longest_sec": 2.1}}
    ],
    "measurements": [
      {"id": "video.width", "category": "video", "name": "width", "value": 1920, "unit": "px", "source": "ffprobe", "estimated": false}
    ],
    "findings": [
      {"code": "VIDEO_BLACK_FRAMES_EXCEEDED", "severity": "FAIL", "message": "longest black segment 2.1s exceeds 2.0s", "evidence": {"...": "..."}, "measurement_ids": ["video.black_segments"]}
    ]
  }
}

Operations

  • inspect - measure only. Returns measurements; checks and findings are empty and overall_status is UNKNOWN (nothing was evaluated, which is different from everything having passed).
  • check - measure, then evaluate every rule the caller supplied plus a small set of always-on baseline checks (decode integrity, clipping, structurally malformed subtitle cues - see docs/checks.md).
  • validate - the same pipeline as check; the name exists for callers that want to express "this is the final delivery gate" in their own request, typically with kind: "delivery" and a full expected spec.

Kinds, measurements, checks

See docs/checks.md for the full, current list of measurements/checks/findings per kind (video/audio/subtitle/delivery/delivery_package). qc contract --json is the authoritative, machine-readable version of the same list - the contract only ever advertises what is actually implemented.

contract["provides"] groups these checks into ten cross-repository Capability ids (measure.video.freeze, measure.video.black_frame, measure.video.format, measure.audio.integrity, measure.audio.clipping_and_dynamics, measure.audio.channel_layout, measure.audio.silence, measure.audio.loudness, measure.subtitle.timing, measure.delivery.integrity), matching the ids assigned to this Skill in kajisho5/AI-video-production-OS's docs/CAPABILITY_MATRIX.md. Three of them - measure.audio.loudness, measure.audio.silence, measure.audio.integrity - are also independently implemented by media-analysis-skill, which publishes the identical ids: this is that project's one documented Capability collision, and the two Skills agreeing on the id (with no shared code) is what makes a future registry able to see it as one Capability with two Providers instead of two unrelated things that happen to share a name. See docs/decisions.md ADR-009 for the full check-to-id mapping and the one check (audio.sample_rate_matches_expected) deliberately left ungrouped.

Measurement vs. Rule vs. Finding

Measurement  = a fact obtained from the media (never a judgment)
Rule         = a caller-supplied, typed expectation (e.g. target LUFS + tolerance)
Finding      = what happened when a Rule was evaluated against a Measurement

qc-skill never hard-codes a policy like "YouTube requires -14 LUFS" - targets and tolerances are always explicit, typed request parameters (see VideoRule / AudioRule / SubtitleRule / DeliveryRule in src/qc_skill/rules.py). Rule evaluation is a fixed set of Python comparisons - there is no expression language, no eval, no way to send a rule that executes arbitrary logic.

PASS / WARN / FAIL / UNKNOWN

UNKNOWN means the check could not be performed - a required measurement was unavailable, a dependency (ffmpeg) was missing, or a value needed for comparison (e.g. a reference video duration) wasn't supplied. UNKNOWN is never collapsed into PASS, and it is never conflated with FAIL: a QCCheck with status UNKNOWN always carries a reason explaining why it could not run. Aggregation is worst-wins: FAIL > UNKNOWN > WARN > PASS (see QCStatus.aggregate in src/qc_skill/models.py).

Security

  • No shell=True, ever. Every subprocess call is an argv list built entirely from fixed flags plus one resolved, validated path.
  • The request schema rejects command, argv, args, shell, cmd, exec, executable, filter, filter_complex, env outright, at any nesting depth - a caller cannot smuggle a command or a raw filtergraph through the request.
  • PathPolicy confines input reads to an optional allow-list of roots and output/report writes to a workspace, resolving symlinks before every containment check.

See docs/security.md for the full boundary and what is deliberately not enforced (e.g. resource/memory limits).

Provenance and reuse

Every report records skill, skill_version, operation, the actual ffmpeg/ffprobe versions used, the input's content fingerprint (sha256, never path/mtime), an identity hash, and an observed_at timestamp. measurement_source is always "OBSERVED" - qc-skill never fabricates a measurement it could not take.

Reports can be safely reused: identity = sha256(canonical_json({asset fingerprint, kind, operation, effective parameters, rules, ffmpeg/ffprobe versions})). A cached report is re-validated against its own stored hash before being returned as a hit; a corrupted or hand-edited cache entry is never returned as a successful reuse. See docs/architecture.md.

Development

pip install -e .
pip install pytest
pytest -q            # requires a real ffmpeg/ffprobe on PATH

See docs/testing.md for the test matrix and how fixtures are generated.

Documentation

License

MIT