220 lines
8.8 KiB
Markdown
220 lines
8.8 KiB
Markdown
# Canonical ASD preprocessing
|
|||
|
|
|
||
|
|
All datasets are converted to the same versioned `muvap-asd-mmap` format.
|
||
|
|
Dataset adapters only resolve source metadata; `preprocess.asd.writer` performs
|
||
|
|
all media conversion and alignment.
|
||
|
|
|
||
|
|
## Packed format
|
||
|
|
|
||
|
|
A pack is a **decoded mirror of the source corpus, not a model-ready tensor
|
||
|
|
set**. Faces and labels keep the native frame rate, audio keeps its full
|
||
|
|
duration and its original gain. Everything MuVAP+ specific - sampling onto the
|
||
|
|
25 Hz model timeline, loudness normalization, chunking - happens in
|
||
|
|
`data.dataloaders.packed` when a batch is built. That split is what lets a pack
|
||
|
|
serve a model whose conventions differ from this one.
|
||
|
|
|
||
|
|
Each corpus keeps its pack beside its own media, so `--output` defaults to
|
||
|
|
`<dataset root>/packed/<split>` and a dataset stays self-contained:
|
||
|
|
|
||
|
|
```text
|
||
|
|
AVA/packed/train/
|
||
|
|
dataset.json
|
||
|
|
shard-000000/
|
||
|
|
faces.npy # uint8 [native_frames, 112, 112], grayscale
|
||
|
|
audio.npy # int16 mono 16 kHz, unnormalized, full clip duration
|
||
|
|
labels.npy # uint8, one label per NATIVE frame
|
||
|
|
index.json # offsets, source_fps, source_frames, audio level statistics
|
||
|
|
```
|
||
|
|
|
||
|
|
Each shard is approximately 1 GB. The arrays are uncompressed and opened with
|
||
|
|
`mmap_mode="r"`, so workers and DDP ranks share the operating-system page cache
|
||
|
|
without HDF5 handles or a per-process copy. Copy complete dataset directories
|
||
|
|
to node-local storage before training if the shared filesystem is the
|
||
|
|
bottleneck.
|
||
|
|
|
||
|
|
The per-sample invariant is:
|
||
|
|
|
||
|
|
```text
|
||
|
|
source_frames faces == source_frames labels
|
||
|
|
audio_samples == round(source_frames / source_fps * 16000)
|
||
|
|
```
|
||
|
|
|
||
|
|
### What the reader adds
|
||
|
|
|
||
|
|
`index.json` carries `source_fps`, `source_frames`, and the recording's
|
||
|
|
`audio_mean`, `audio_rms`, and `audio_peak` measured after DC removal. Those
|
||
|
|
statistics are what let the loader reproduce a whole-recording normalization
|
||
|
|
from any slice: gain computed from a chunk alone would drift with that chunk's
|
||
|
|
content. The loader removes DC, scales to -25 dBFS RMS, caps gain at 20 dB and
|
||
|
|
the peak at 0.99, and samples faces and labels onto the 25 Hz timeline with one
|
||
|
|
shared index so a label stays attached to the face it describes.
|
||
|
|
|
||
|
|
The one convention still baked into the pack is the face crop: 112x112
|
||
|
|
grayscale, recorded in `dataset.json` as `face_shape` and `face_color`. It is
|
||
|
|
the ASD community's standard geometry rather than a MuVAP+ choice, but a reader
|
||
|
|
should check it rather than assume it.
|
||
|
|
|
||
|
|
The size is a constant in two places, not a config key: `FACE_SIZE` in
|
||
|
|
`preprocess/asd/schema.py`, which the writer resizes every JPEG face to, and
|
||
|
|
`VisualSpec` in `data/media.py`, which is what the loader and the model expect.
|
||
|
|
Changing the geometry means editing both and re-running preprocessing - a pack
|
||
|
|
written at one size cannot be read at another.
|
||
|
|
|
||
|
|
## AVA
|
||
|
|
|
||
|
|
```bash
|
||
|
|
python -m preprocess.asd.prepare ava \
|
||
|
|
--loader /data/AVA/csv/train_loader.csv \
|
||
|
|
--visual-root /data/AVA/clips_videos \
|
||
|
|
--audio-root /data/AVA/clips_audios \
|
||
|
|
--split train # writes /data/AVA/packed/train
|
||
|
|
```
|
||
|
|
|
||
|
|
The adapter reads nested JPEG face directories and clip-local WAV files.
|
||
|
|
Missing inputs fail preprocessing. `--skip-missing` exists only for deliberately
|
||
|
|
partial development copies.
|
||
|
|
|
||
|
|
## WASD
|
||
|
|
|
||
|
|
WASD ships the same layout and the same loader schema as AVA, so it uses the
|
||
|
|
same reader:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
python -m preprocess.asd.prepare wasd \
|
||
|
|
--loader /data/WASD/csv/train_loader.csv \
|
||
|
|
--visual-root /data/WASD/clips_videos \
|
||
|
|
--audio-root /data/WASD/clips_audios \
|
||
|
|
--split train # writes /data/WASD/packed/train
|
||
|
|
```
|
||
|
|
|
||
|
|
Two differences from AVA are worth knowing when training on both. WASD labels
|
||
|
|
are strictly binary - `SPEAKING_NOT_AUDIBLE` never occurs - so the rule that
|
||
|
|
inaudible speech counts as negative is something only AVA teaches. And every
|
||
|
|
face track in a WASD clip shares one clip-level WAV, so audio alone cannot tell
|
||
|
|
its speakers apart; any augmentation that mixes in a second voice must draw it
|
||
|
|
from a different clip.
|
||
|
|
|
||
|
|
## MSDWild
|
||
|
|
|
||
|
|
Create 30-second native-FPS clips with 10 seconds of overlap directly from the
|
||
|
|
paired raw MP4 and bbox CSV files. The first window starts at the first frame
|
||
|
|
containing any face. Tracks must be present in at least 90% of a complete
|
||
|
|
window, which removes transient detections before face cropping.
|
||
|
|
Incomplete final windows are discarded rather than padded, because the model
|
||
|
|
does not use a padding mask and would otherwise learn from artificial silence.
|
||
|
|
Speaking labels come from `all.rttm`, which ships separately from the
|
||
|
|
bounding-box release and must be downloaded before this script can run; the
|
||
|
|
final bbox CSV column is not an activity label. `prepare.py` has no MSDWild
|
||
|
|
path - there is no loader TSV to read.
|
||
|
|
|
||
|
|
```bash
|
||
|
|
python preprocess/asd/prep_msdwild.py \
|
||
|
|
--input /data/MSDWild/msdwild_boundingbox_labels \
|
||
|
|
--output /data/MSDWild/preprocessed
|
||
|
|
```
|
||
|
|
|
||
|
|
The script uses the shared audio loader and face cropper and writes the same
|
||
|
|
canonical memory-mapped shards as AVA and WASD. No intermediate per-clip files
|
||
|
|
or loader CSV are created.
|
||
|
|
|
||
|
|
## AVCC
|
||
|
|
|
||
|
|
AVCC uses the canonical headered TSV because conversation corpora vary in
|
||
|
|
their crop-generation layout:
|
||
|
|
|
||
|
|
```text
|
||
|
|
sample_id\tvisual_path\taudio_path\tsource_fps\tlabels\tstart_sec\tvisual_kind\taudio_sample_rate
|
||
|
|
conv1_spk1\tfaces/conv1_spk1.npy\taudio/conv1.wav\t25.0\t[0,1,1,0]\t12.0\tnpy\t16000
|
||
|
|
```
|
||
|
|
|
||
|
|
Paths may be absolute or relative to `--root`:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
python -m preprocess.asd.prepare avcc \
|
||
|
|
--loader /data/AVCC/asd_manifest.tsv \
|
||
|
|
--root /data/AVCC \
|
||
|
|
--output /data/packed/avcc_train
|
||
|
|
```
|
||
|
|
|
||
|
|
Each row represents one speaker face track. AVCC conversation grouping,
|
||
|
|
bounding boxes, and speaker identities remain separate MuVAP metadata; its ASD
|
||
|
|
embeddings must be extracted from these canonical samples.
|
||
|
|
|
||
|
|
## Lightning
|
||
|
|
|
||
|
|
```yaml
|
||
|
|
packed_asd:
|
||
|
|
train:
|
||
|
|
- /scratch/local/muvap/AVA/packed/train
|
||
|
|
- /scratch/local/muvap/WASD/packed/train
|
||
|
|
- /scratch/local/muvap/WASD/packed/val
|
||
|
|
val:
|
||
|
|
- /scratch/local/muvap/AVA/packed/val
|
||
|
|
```
|
||
|
|
|
||
|
|
Use `PackedASDDataModule`. Start with four workers per GPU,
|
||
|
|
`persistent_workers=True`, `prefetch_factor=2`, and pinned memory, then measure
|
||
|
|
0/2/4/8 workers on the actual allocation.
|
||
|
|
|
||
|
|
### Training batches
|
||
|
|
|
||
|
|
Track lengths are wildly uneven - AVA's median is 64 frames against WASD's 746 -
|
||
|
|
and the collator crops a batch to its shortest member. Batching at random
|
||
|
|
therefore threw away most of the data: measured on the AVA pack, a shuffled
|
||
|
|
`batch_size: 8` used **36%** of its frames per epoch.
|
||
|
|
|
||
|
|
Two settings fix that, and they only work together:
|
||
|
|
|
||
|
|
- `train_window` splits any track longer than it into near-equal chunks, so one
|
||
|
|
epoch still covers every frame and no chunk is much longer than another.
|
||
|
|
- `frame_budget` builds each batch from similar-length chunks up to a target
|
||
|
|
`batch * frames`, clamped by `min_batch` and `max_batch`.
|
||
|
|
|
||
|
|
Together they reach **99.95%** frame utilisation on AVA and 99.98% on AVA plus
|
||
|
|
WASD, which is why the model still needs no padding mask. Holding
|
||
|
|
`batch * frames` roughly constant also stabilises activation memory, the `B*T`
|
||
|
|
population the visual frontend's batch norm sees, and the per-frame gradient
|
||
|
|
noise of the mean-reduced losses. The clamp exists because the contrastive term
|
||
|
|
is a `B`-way classification whose difficulty would otherwise swing with the
|
||
|
|
chunk length.
|
||
|
|
|
||
|
|
### Context prefix
|
||
|
|
|
||
|
|
A chunk boundary creates a second, subtler problem. The model is causal, so a
|
||
|
|
frame near the start of a chunk can only attend to the few frames before it -
|
||
|
|
yet its projection target depends on 2 s of history. Without help, 31.6% of
|
||
|
|
scored frames on AVA plus WASD are asked to predict from history the encoder was
|
||
|
|
never shown.
|
||
|
|
|
||
|
|
The loader therefore reads `hist_frames` extra frames *before* each chunk and
|
||
|
|
marks them unscored. They provide history and never contribute to a loss, so:
|
||
|
|
|
||
|
|
- every frame is still scored exactly once per epoch,
|
||
|
|
- the artificial starvation disappears entirely - the remaining 21.5% are all
|
||
|
|
real track starts, where the missing history is genuine and the model has to
|
||
|
|
cope with it at inference too,
|
||
|
|
- the cost is 1.15x compute and no extra storage.
|
||
|
|
|
||
|
|
Overlapping the chunks instead would duplicate the data on disk *and* keep
|
||
|
|
training on the truncated copies, since it adds well-formed frames beside the
|
||
|
|
starved ones rather than removing them.
|
||
|
|
|
||
|
|
Validation ignores all of this: it runs whole tracks at batch size one, so every
|
||
|
|
native frame receives a score.
|
||
|
|
|
||
|
|
Because the train loader shards itself across ranks, `Trainer` is constructed
|
||
|
|
with `use_distributed_sampler=False` and validation gets an explicit
|
||
|
|
`DistributedSampler`.
|
||
|
|
|
||
|
|
Validate every completed dataset and compare amplitude distributions before
|
||
|
|
training:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
python -m preprocess.asd.validate /data/AVA/packed/train /data/WASD/packed/train
|
||
|
|
```
|
||
|
|
|
||
|
|
The report shows the stored source levels beside the levels the loader produces
|
||
|
|
after normalization. Large source differences between corpora are expected;
|
||
|
|
large differences in the *normalized* RMS usually mean peak limiting,
|
||
|
|
near-silence, or malformed input that should be inspected.
|