KonViD-150k corpus ingestion → MOS-corpus JSONL¶
The KonViD-150k dataset (Götz-Hahn et al., IEEE Access 2021 / ICIP 2019) is a ~150 000-clip user-generated-video corpus with crowdsourced subjective Mean Opinion Scores. The VMAFx fork uses it as Phase 2 of the ADR-0325 KonViD ingestion plan: the full-scale follow-on to the 1 200-clip Phase 1 predecessor (see konvid-1k-ingestion.md).
At ~120–200 GB of working-set disk and hours-to-days of download wall-clock, this path needs three things the Phase 1 path does not:
- resumable downloads (section 5);
- attrition tolerance (section 6);
- an
"ugc-mixed"encoder slot (section 8).
Status¶
ADR-0325 is Accepted (2026-05-15). Phase 3 (real-corpus MOS head training) is the next gate, tracked under ADR-0336. The shipped head is still a synthetic-corpus placeholder (see konvid_mos_head_v1); the production-flip gate (PLCC >= 0.85 mean, SROCC >= 0.82, RMSE <= 0.45) is run when a real-corpus training pass completes.
See ADR-0325 for the two-phase decision and Research-0086 for the feasibility analysis.
1. Dataset overview¶
KonViD-150k ships distorted-only content — every clip is a user-uploaded YouTube/Vimeo encode and every clip carries one subjective MOS aggregated from ≥ 5 crowdworker ratings on a 1–5 scale. There is no separate raw reference; each clip is its own opinion datum. Clips are mostly 540p / 720p / 1080p, ~5–10 s long, h264 / vp9.
Citation:
Franz Götz-Hahn, Vlad Hosu, Hanhe Lin, Dietmar Saupe. KonVid-150k: A Dataset for No-Reference Video Quality Assessment of Videos in-the-Wild. IEEE Access 2021 (companion to the ICIP 2019 release).
2. Where to download¶
KonViD-150k is distributed by the University of Konstanz / MMSP group. Depending on the drop, operators may have either a URL manifest CSV or the canonical split score CSVs plus already-extracted MP4s. The fork does not redistribute the corpus, the per-clip MOS values, or any derived per-clip statistics (license is research-only — see ADR-0325 §License).
URL-manifest layout:
.corpus/konvid-150k/
├── manifest.csv # operator drops this
├── .download-progress.json # written by this script (resumable state)
├── clips/ # populated by this script
│ ├── 0001.mp4
│ ├── 0002.mp4
│ └── ...
└── konvid_150k.jsonl # written by this script (output)
Split score-drop layout:
.corpus/konvid-150k/
├── k150ka_scores.csv # video_name,video_score
├── k150ka_extracted/ # K150K-A MP4 clips
├── k150kb_scores.csv # video_name,mos,video_score
├── k150kb_extracted/ # K150K-B MP4 clips
└── konvid_150k.jsonl # written by this script (output)
All of these paths are gitignored (.corpus/ is the fork's standard research-data root, see AGENTS.md §5). With a URL manifest, the script downloads on demand into clips/. With the split score layout, the script reads the local extracted MP4s directly; it does not try to reconstruct source URLs.
Dataset URL: https://database.mmsp-kn.de/konvid-150k-vqa-database.html
3. Pipeline¶
The Phase 2 ingestion is a per-row download → ffprobe → JSONL emit loop:
.corpus/konvid-150k/manifest.csv
│
│ ai/scripts/konvid_150k_to_corpus_jsonl.py
│ (per row: curl → ffprobe → MOS join)
│ (writes .download-progress.json after each clip)
▼
.corpus/konvid-150k/konvid_150k.jsonl
│
│ (Phase 3 — out of scope here, ADR-0325 §Phase 3)
▼
ensemble-training-kit MOS-head trainer
For each manifest row the script:
- Checks the resumable-download progress JSON. If the clip is already marked
doneand present on disk, skip the network call. - Otherwise
curl --location --failthe URL into a sibling.partfile and rename atomically on success. Failures are logged with a reason and recorded asfailedin the progress JSON. - Run ffprobe on the downloaded MP4 to recover
(width, height, fps, duration, pix_fmt, codec_name). - Join with the manifest CSV's MOS / SD / rating-count columns and emit one JSONL row.
Heavy feature extraction (libvmaf, GPU-cached parquet) does not run here — Phase 2 stays a pure metadata-and-MOS join. That keeps the working set bounded by the clip corpus itself, not by intermediate parquet shards.
4. Run command¶
After dropping either the URL manifest or the split score/extracted layout under .corpus/konvid-150k/:
The default output is .corpus/konvid-150k/konvid_150k.jsonl. The adapter writes <output>.manifest.json next to it. The replay manifest records the source paths, row caps, attrition counters and ADR-0661 run_provenance.
| Flag | Default | Meaning |
|---|---|---|
--konvid-dir | .corpus/konvid-150k/ | Input root |
--manifest-csv | <konvid-dir>/manifest.csv | Manifest CSV; strict: an explicit file that is missing fails the run instead of falling back to split CSV discovery (catches typoed paths) |
--clips-subdir | clips | Subdirectory for downloaded clips |
--output | .corpus/konvid-150k/konvid_150k.jsonl | Output JSONL |
--manifest-out | <output>.manifest.json | Replay sidecar; point it into a dated experiment bundle if needed |
--progress-path | <konvid-dir>/.download-progress.json | Resumable-state file |
--curl-bin | $CURL_BIN or curl | curl binary |
--ffprobe-bin | $FFPROBE_BIN or ffprobe | ffprobe binary |
--download-timeout-s | 120 | Per-clip curl --max-time seconds |
--attrition-warn-threshold | 0.10 | Download-failure fraction that triggers a WARNING |
--corpus-version | konvid-150k-2019 | Dataset version string |
--log-level | INFO | DEBUG, INFO, WARNING or ERROR |
The summary line lands on stderr on completion:
5. Resumable downloads¶
The single largest difference from Phase 1: a full pass takes hours-to-days, so Ctrl-C + re-run must pick up where the prior run left off without re-downloading. State is recorded in .corpus/konvid-150k/.download-progress.json, written atomically (tempfile + rename) at most every 50 clips and once at the end of every run.
State shape:
{
"0001.mp4": {"state": "done"},
"0002.mp4": {"state": "failed", "reason": "curl-rc=22: HTTP 404 Not Found"}
}
States and their re-run semantics:
| State | On disk | Re-run behaviour |
|---|---|---|
| (no key) | no | Attempt download |
done | yes | Skip network call; probe + emit |
done | no | Re-download (file went missing) |
failed | n/a | Skip; do not retry |
Warning
The failed state is non-retriable on purpose: repeatedly hammering a 404 / takedown wastes bandwidth and slows convergence. To force a retry of every previously-failed clip, delete .download-progress.json before re-running. To selectively retry, hand-edit the JSON and remove the failed entries.
6. Attrition handling (~5–8 % expected)¶
KonViD-150k clips are pulled per-URL from YouTube/Vimeo, so a non-trivial fraction will fail to download on any given pass — takedowns, region blocks, channel deletions, and Vimeo URL drift combine to a 5–8 % attrition rate per recent papers (Götz-Hahn 2021). The script tolerates this:
- Each download failure is logged at WARNING level with the curl reason.
- The summary breaks failures into
(download-failed)(the byte stream never arrived) vs(broken-clip)(downloaded but ffprobe rejected) so the operator can tell content attrition from on-disk corruption at a glance. - An advisory WARNING fires when the download-failure rate exceeds
--attrition-warn-threshold(default0.10, i.e. 10 %, slightly above the cited 5–8 % to absorb normal jitter). The run still completes — the threshold is informational, not fatal.
A typical clean run logs roughly:
[konvid-150k-jsonl] wrote 142037 rows, skipped 7891 (download-failed),
72 (broken-clip), 0 dedups -> .../konvid_150k.jsonl
7. Output schema¶
One JSON object per line:
{
"src": "0001.mp4",
"src_sha256": "ab12...",
"src_size_bytes": 4321987,
"width": 960,
"height": 540,
"framerate": 30.0,
"duration_s": 8.0,
"pix_fmt": "yuv420p",
"encoder_upstream": "h264",
"mos": 3.42,
"mos_std_dev": 0.51,
"n_ratings": 7,
"corpus": "konvid-150k",
"corpus_version": "konvid-150k-2019",
"ingested_at_utc": "2026-05-08T12:00:00+00:00"
}
The schema is identical to Phase 1 modulo corpus = "konvid-150k" and the default corpus_version (konvid-150k-2019, the ICIP release year, overridable via --corpus-version). Phase 1 and Phase 2 JSONLs can be concatenated and consumed by a single trainer; the corpus column carries the shard identity for downstream analysis.
Note
The schema is disjoint from the existing vmaf-tune Phase A CORPUS_ROW_KEYS row (no vmaf_score, encoder, preset, crf). The two corpora are merged at the trainer level in Phase 3, not at the JSONL level, because their natural keys differ. See the Phase 1 docs page for the full rationale.
8. Encoder vocabulary preparation¶
Per ADR-0325 §Decision the YouTube/Vimeo encoder distribution collapses to a single new "ugc-mixed" slot in ENCODER_VOCAB v4.
This script does not modify ENCODER_VOCAB itself — that's a separate PR per ADR-0325 §Phase 2. The script simply records the upstream codec name ffprobe reports (h264 / vp9 / av1 / etc.) in the encoder_upstream JSONL field. The trainer is responsible for collapsing every encoder_upstream value from a konvid-* corpus row into the "ugc-mixed" slot at consumption time.
9. Refusal: KonViD-1k mis-mount¶
If the operator points the script at a manifest.csv that actually holds the KonViD-1k attribute CSV (~1 200 rows), the script aborts with a hint pointing at konvid_1k_to_corpus_jsonl.py (Phase 1). The threshold is 5 000 rows — the actual KonViD-150k size is ~150 000, so the gap absorbs pre-release subsets while still catching the inverse-mount case. This mirrors the Phase 1 1500-row ceiling in the inverse direction.
10. Reproducibility and CI¶
Every step is deterministic given the same input archive. The src_sha256 field is a chunked SHA-256 of the clip bytes (1 MiB chunks; same shape vmaftune.corpus.py already emits) so re-runs across machines produce identical hashes for identical clips.
CI cannot retrain end-to-end (the corpus is non-redistributable). The adapter is exercised by ai/tests/test_konvid_150k.py, which mocks both ffprobe and curl via the script's injectable runner seam and stands up a temporary .corpus/konvid-150k/-shaped tree on disk. The tests run in well under one second and require neither curl, ffprobe, nor the corpus.
11. Operational notes¶
Resume¶
A Ctrl-C mid-download is safe and lossless. The in-flight *.part file is deleted on next-run start, the progress JSON was last flushed at most 50 clips ago, and the JSONL uses src_sha256-keyed dedup so a re-run cannot double-emit.
State file¶
.download-progress.json is written via tempfile + os.replace, so the operator never observes a half-written state file even on kill -9.
A failed entry stays failed until the operator deletes it (or the entire JSON). Rationale: most failed reasons are content takedowns that will never resolve.
CSV column aliases¶
The manifest CSV has shipped under varying spellings (MOS vs mos, SD vs mos_std, n vs num_ratings, file_name vs video_name, url vs download_url). The adapter accepts every spelling that has shipped to date; if a new release adds an alias, edit _CSV_*_KEYS at the top of the script.
k150ka_scores.csv commonly carries video_name,video_score; k150kb_scores.csv commonly carries video_name,mos,video_score. Both are accepted. Split rows have no URL, per-row standard deviation, or rating count in the score CSVs. The internal download field is empty, and the emitted JSONL records mos_std_dev = 0.0 and n_ratings = 0.
Licence¶
Per ADR-0325 §License, neither the clips, the per-clip MOS values, nor the JSONL itself ship in the repo. Only the ingestion script, this docs page, and the schema definition are in-tree. Trained model weights derived from the corpus are redistributable; per-clip data is not.
12. Next phase¶
- Phase 3. Train a sibling MOS-head ONNX via the existing ensemble-training-kit harness (ADR-0324). Hold out a 10 % fold for production-flip gating per the ADR-0303 protocol; concurrently use the held-out fold to measure how well the existing VMAF predictor correlates with subjective MOS on UGC content.