QA report¶
Every yt-uniq run (unless --no-qa) emits two artefacts next to the
output file:
output.mp4
output.mp4.qa.json # machine-readable
output.mp4.qa.html # human-readable, with heatmaps + verdict banner
yt-uniq qa <input> <output> produces the same pair for an existing
input/output pair without re-encoding.
What's measured¶
For one (input, output) pair the report aggregates seven independent metric families. Each one is optional and degrades gracefully if its backing binary is missing — the QA report just notes which ones it couldn't compute.
New since v0.8.0: optional SSCD semantic-similarity scores (
yt-uniq run --metric sscdoryt-uniq qa --metric sscd, requires[ml]extra) and target-VMAF bounded-retry events for profiles that opt intotarget_vmaf. See the SSCD + target-VMAF subsections below.
File-level¶
| Field | Source | Meaning |
|---|---|---|
input_md5, output_md5 |
streaming md5 (4 MB chunks) | exact byte identity (different for any successful run — only matches on --new-variant=false resume of the exact same plan) |
input_size_bytes, output_size_bytes |
filesystem | sanity check on bitrate budget |
input_duration_sec, output_duration_sec |
ffprobe | should match within ±0.5 s; duration_match is the bool |
Visual similarity¶
| Field | Source | Range | Meaning |
|---|---|---|---|
phash_samples |
— | typically 120 | how many frames were sampled |
phash_distance_min, _mean, _max |
imagehash.phash on samples frames |
0..64 bits | Hamming distance between paired frames |
phash_similarity |
1 - mean_distance / 64 |
0..1 | aggregate; higher = closer to source |
vmaf_mean |
ffmpeg libvmaf |
0..100 | perceptual quality vs source; null if libvmaf missing |
ssim_mean |
ffmpeg ssim |
0..1 | structural similarity index; null if disabled |
Audio similarity¶
| Field | Source | Range | Meaning |
|---|---|---|---|
audio_fp_similarity |
chromaprint Jaccard over uint32 sub-fingerprints | 0..1 | strict set-equality check; see warning below |
audio_fp_hamming_per_frame |
chromaprint XOR + popcount, paired frames, mean | 0..32 bits | bit-level distance; the canonical CID-divergence audio KPI |
audio_fp_match_confidence |
1 - hamming_per_frame / 32 |
0..1 | normalized; lower = better for divergence |
About
audio_fp_similarity. This is Jaccard (|A ∩ B| / |A ∪ B|) over the 32-bit chromaprint sub-fingerprint sets. Chromaprint deliberately flips bits across the entire 32-bit code on small acoustic changes (≈1 dB loudnorm shift alone) so two 32-bit codes are exact-equal only when the audio is byte-identical. In practice this field reads 0.0 for every yt-uniq output, even on the softest profile — the audio is perfectly recognisable, the codes simply don't survive bit-exact match. Don't read it as "audio destroyed". The metric that reflects perceived similarity isaudio_fp_match_confidence(Hamming-based, normalised). The_similarityfield is retained for schema compatibility with downstream tools that already key on it.
The two Hamming fields are the explicit CID-divergence audio KPI introduced in v0.3.3 (Spec 16). Heuristic interpretation per chromaprint literature:
hamming_per_frame (bits) |
Interpretation |
|---|---|
| ≤ 5 | high-confidence match — CID will hit |
| 6–14 | match |
| 15–25 | uncertain |
| ≥ 26 | no match |
| ≥ 30 | high-confidence non-match |
SSCD semantic similarity (v0.8.0 R4, opt-in)¶
Populated only when yt-uniq run --metric sscd (or yt-uniq qa
--metric sscd) is passed. Requires the [ml] extra (torch +
transformers). The first run downloads ~200 MB of model weights to
~/.cache/yt_uniquifier/models/; subsequent runs use the cache.
Full background in docs/sscd.md.
| Field | Source | Range | Meaning |
|---|---|---|---|
sscd.mean_similarity |
mean cosine similarity over N-frame embedding pairs | 0..1 | aggregate semantic match |
sscd.min_similarity |
min cosine similarity (weakest paired frame) | 0..1 | worst case — the chunk most likely to fail human review |
sscd.per_frame[] |
{frame_idx, similarity} for each sampled pair |
0..1 | drives the SSCD heatmap in the HTML report |
sscd.band |
derived bucket: high ≥0.85, medium 0.65-0.85, low <0.65 |
enum | colour-coded in HTML |
Unlike pHash (pixel-level), SSCD reflects what a content-aware human or model would see: a recoloured + cropped + slightly-noisy clip can score 0.92 on SSCD while pHash similarity drops below 0.50.
Target-VMAF retry events (v0.8.0 R5)¶
Profiles that set target_vmaf trigger an in-flight retry when a
segment lands below the target. The QA report records every retry:
| Field | Meaning |
|---|---|
target_vmaf_events[].segment_idx |
which segment retried |
target_vmaf_events[].attempt |
1-based retry attempt |
target_vmaf_events[].measured |
VMAF actually measured |
target_vmaf_events[].target |
the target_vmaf from the profile |
target_vmaf_events[].action |
retry (re-encoded with crf--) or accept (hit cap, kept best-so-far) |
target_vmaf_max_retries caps the loop; when the cap is hit the
report includes a notes[] entry naming the segment.
Content-ID prediction (v0.2+)¶
| Field | Source | Meaning |
|---|---|---|
cid_predict_self |
weighted (visual + audio) Jaccard over 4-second chunks | 0..1; predicted self-match probability |
weakest_chunk_sec |
argmax over chunk_similarities[].combined |
(start_sec, end_sec) of the chunk most similar to source |
chunk_similarities[] |
per 4-sec chunk: {start_sec, end_sec, visual, audio, combined} |
drives the HTML heatmap |
corpus_matches[] |
comparison against yt-uniq corpus entries |
{id, path, visual, audio, combined} for files above threshold |
cid_predict_self is a predictor, not a guarantee. Real Content ID is
a black box; we model it as the convex combination of the chunk-level
visual and audio similarities, weighted to match published behaviour.
Notes + verdict¶
| Field | Meaning |
|---|---|
notes[] |
warnings for unavailable backends (e.g. "fpcalc not in PATH") |
duration_match |
bool — input/output durations within ±0.5 s |
The HTML report shows a banner colour-coded green / yellow / red based on a rule of thumb:
- green: phash similarity in (0.50, 0.85] and VMAF ≥ 85 and SSIM ≥ 0.90
- yellow: phash in (0.85, 0.97] or VMAF in (75, 85) or SSIM < 0.90
- red: phash > 0.97 (barely unique) or phash < 0.50 (unrecognisable) or VMAF < 75
KPI targets¶
These are the targets post-v0.3.3 for owned-content CID divergence
(profile cid_aware). Actual values vary by source — synthetic test
patterns score very differently from natural footage.
| KPI | Target | Notes |
|---|---|---|
phash_similarity (mean) |
< 0.75 |
lower is more visually distinct |
phash_similarity (worst 4-sec chunk) |
< 0.80 |
matters more than the mean — CID locks on the closest chunk |
vmaf_mean |
≥ 85 |
trade-off floor; below this is noticeably degraded |
ssim_mean |
≥ 0.90 |
optional sanity check |
audio_fp_hamming_per_frame |
≥ 15 bits |
"uncertain match" zone or better |
audio_fp_match_confidence |
≤ 0.55 |
mirror of the above |
cid_predict_self |
< 0.20 |
predicted self-match probability |
cid_aggressive typically pushes pHash down to ~0.60 and Hamming up to
~22 bits, at the cost of VMAF ≈ 78–82 and audible noise overlay.
Reading the heatmap¶
The HTML report renders one cell per 4-sec chunk, coloured by
combined = α·visual + (1-α)·audio:
The weakest chunk (red-most) is the one most at risk of matching. Common patterns:
- A single red chunk near the start → likely a logo / title card where random transforms barely move things. Calibrate or trim the leading sequence.
- A red band in the middle → static talking-head section; bump up
video.crop_resize.max_strengthandvideo.temporal_jitter.blackout_prob. - All cells in the green zone → done; profile is well-calibrated for this source.
Standalone QA (no encode)¶
yt-uniq qa /path/to/master.mp4 /path/to/candidate.mp4 --vs-corpus
# writes candidate.mp4.qa.json + candidate.mp4.qa.html
--vs-corpus adds the corpus_matches section so you can verify the
candidate isn't too similar to a previously-uploaded variant.
Fast QA¶
For batch workflows where you don't need VMAF (slow):
JSON shape is identical, with vmaf_mean = null and phash_samples ≈ 60.
Programmatic access¶
import json
from pathlib import Path
qa = json.loads(Path("out.mp4.qa.json").read_text())
# Audio CID divergence KPI:
hp = qa.get("audio_fp_hamming_per_frame")
if hp is not None and hp < 15:
print(f"Audio FP too close: {hp:.1f} bits/frame (want ≥ 15)")
# Worst chunk:
worst = max(c["combined"] for c in qa["chunk_similarities"])
print(f"worst chunk combined similarity: {worst:.3f}")
The Pydantic model is yt_uniquifier.core.models.QAReport if you'd
rather work with typed objects.