8.8 KiB
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:
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:
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
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:
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.
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:
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:
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
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_windowsplits 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_budgetbuilds each batch from similar-length chunks up to a targetbatch * frames, clamped bymin_batchandmax_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:
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.