Files
icassp2027-experiment/third_party/MuVAP/models/asd.py

61 lines
2.7 KiB
Python

import torch
import torch.nn as nn
from .modules.attention import TransformerModel
from .modules.audio import build_audio_encoder, check_frame_rate, encoder_projection
from .modules.projection import LinearHead
from .modules.visual_encoder import VisualEncoder
#: The visual frontend samples faces onto this timeline, so the audio frontend
#: has to land on it too or the two streams cannot be fused frame for frame.
MODEL_FRAME_HZ = 25.0
class AudioVisualASD(nn.Module):
def __init__(self, cfg):
super().__init__()
cross_dim = cfg["cross"]["d_model"]
temporal_dim = cfg["temporal"]["d_model"]
if temporal_dim != cross_dim * 2:
raise ValueError("ASD temporal d_model must be twice cross d_model")
self.audio_encoder = build_audio_encoder(cfg)
check_frame_rate(self.audio_encoder, MODEL_FRAME_HZ, "the ASD visual timeline")
self.encoder_proj = encoder_projection(self.audio_encoder.out_dim, cross_dim)
self.visual_encoder = VisualEncoder()
if self.visual_encoder.out_dim != cross_dim:
raise ValueError(
f"ASD cross d_model must be {self.visual_encoder.out_dim} to match the "
f"visual frontend, got {cross_dim}"
)
self.norm_audio = nn.LayerNorm(cross_dim)
self.norm_visual = nn.LayerNorm(cross_dim)
self.cross_v2a = TransformerModel(cfg["cross"], cross_attention=True)
self.cross_a2v = TransformerModel(cfg["cross"], cross_attention=True)
self.transformer = TransformerModel(cfg["temporal"])
self.asd_head = LinearHead(temporal_dim, 6)
self.audio_head = LinearHead(cross_dim, 1)
self.visual_head = LinearHead(cross_dim, 1)
def forward(self, audio, visual, return_embeddings=False):
audio_embedding = self.norm_audio(
self.encoder_proj(self.audio_encoder(audio))
)
visual_embedding = self.norm_visual(self.visual_encoder(visual))
if audio_embedding.shape[1] != visual_embedding.shape[1]:
raise ValueError(
f"audio and visual lengths differ: {audio_embedding.shape[1]} and "
f"{visual_embedding.shape[1]}"
)
audio_cross = self.cross_v2a(audio_embedding, src=visual_embedding)
visual_cross = self.cross_a2v(visual_embedding, src=audio_embedding)
fused = self.transformer(torch.cat([audio_cross, visual_cross], dim=-1))
projection_logits = self.asd_head(fused)
audio_logits = self.audio_head(audio_cross)
visual_logits = self.visual_head(visual_cross)
if return_embeddings:
return projection_logits, audio_logits, visual_logits, fused, audio_embedding, visual_embedding
return projection_logits, audio_logits, visual_logits