multisense_df / src /models /lipsync_branch.py
vijay-2132's picture
Clean deployment without large checkpoints
a94bd47
Raw
History Blame Contribute Delete
4.94 kB
"""
MultiSense-DF — Lip-Sync Consistency Module
SyncNet-derived dual-stream network: mouth crops + mel-spectrogram
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
class MouthVisualNet(nn.Module):
"""5-layer CNN for 96×96 mouth-region crops."""
def __init__(self, out_dim=256):
super().__init__()
self.net = nn.Sequential(
nn.Conv2d(3, 32, 3, 2, 1), nn.BatchNorm2d(32), nn.ReLU(), # 48
nn.Conv2d(32, 64, 3, 2, 1), nn.BatchNorm2d(64), nn.ReLU(), # 24
nn.Conv2d(64, 128, 3, 2, 1), nn.BatchNorm2d(128), nn.ReLU(),# 12
nn.Conv2d(128, 256, 3, 2, 1), nn.BatchNorm2d(256), nn.ReLU(),# 6
nn.Conv2d(256, 512, 3, 2, 1), nn.BatchNorm2d(512), nn.ReLU(),# 3
nn.AdaptiveAvgPool2d(1)
)
self.fc = nn.Linear(512, out_dim)
def forward(self, x):
# x: (B*T, 3, 96, 96)
return F.normalize(self.fc(self.net(x).squeeze(-1).squeeze(-1)), dim=-1)
class AudioSpecNet(nn.Module):
"""5-layer CNN for mel-spectrogram windows aligned to mouth frames."""
def __init__(self, out_dim=256):
super().__init__()
self.net = nn.Sequential(
nn.Conv2d(1, 32, 3, 2, 1), nn.BatchNorm2d(32), nn.ReLU(),
nn.Conv2d(32, 64, 3, 2, 1), nn.BatchNorm2d(64), nn.ReLU(),
nn.Conv2d(64, 128, 3, 2, 1), nn.BatchNorm2d(128), nn.ReLU(),
nn.Conv2d(128, 256, 3, 2, 1), nn.BatchNorm2d(256), nn.ReLU(),
nn.Conv2d(256, 512, 3, (2, 1), 1), nn.BatchNorm2d(512), nn.ReLU(),
nn.AdaptiveAvgPool2d(1)
)
self.fc = nn.Linear(512, out_dim)
def forward(self, x):
# x: (B*T, 1, 80, W) mel-spectrogram window
return F.normalize(self.fc(self.net(x).squeeze(-1).squeeze(-1)), dim=-1)
class LipSyncBranch(nn.Module):
"""
SyncNet-derived lip-sync consistency module.
Computes per-frame cosine similarity between audio and visual embeddings,
then summarises temporal synchrony profile with a 2-layer LSTM.
Input : mouth_crops (B, T, 3, 96, 96)
mel_specs (B, T, 1, 80, W)
Output: (B, 512) lip-sync embedding + (B, 1) logit
"""
def __init__(self, sync_dim=256, embed_dim=512, dropout=0.1):
super().__init__()
self.visual_net = MouthVisualNet(out_dim=sync_dim)
self.audio_net = AudioSpecNet(out_dim=sync_dim)
# Summarise temporal synchrony profile
self.lstm = nn.LSTM(
input_size=sync_dim * 2 + 1, # [v_feat, a_feat, cos_sim]
hidden_size=embed_dim // 2,
num_layers=2,
batch_first=True,
bidirectional=True,
dropout=dropout
)
self.norm = nn.LayerNorm(embed_dim)
self.classifier = nn.Sequential(
nn.Linear(embed_dim, 256),
nn.GELU(),
nn.Dropout(dropout),
nn.Linear(256, 1)
)
def forward(self, mouth_crops, mel_specs):
B, T, C, H, W = mouth_crops.shape
_, _, _, F_mel, W_mel = mel_specs.shape
# Per-frame embeddings
v = self.visual_net(mouth_crops.reshape(B * T, C, H, W)) # (B*T, 256)
a = self.audio_net(mel_specs.reshape(B * T, 1, F_mel, W_mel)) # (B*T, 256)
v = v.reshape(B, T, -1) # (B, T, 256)
a = a.reshape(B, T, -1) # (B, T, 256)
# Per-frame cosine similarity (scalar)
cos_sim = F.cosine_similarity(v, a, dim=-1, eps=1e-8).unsqueeze(-1) # (B, T, 1)
# Concatenate features + similarity for LSTM input
seq = torch.cat([v, a, cos_sim], dim=-1) # (B, T, 513)
out, _ = self.lstm(seq) # (B, T, 512)
embed = self.norm(out[:, -1]) # last timestep → (B, 512)
logit = self.classifier(embed) # (B, 1)
return embed, logit, cos_sim.squeeze(-1) # also return sync profile
def contrastive_loss(self, mouth_crops, mel_specs, is_synced):
"""
Contrastive loss for pre-training: in-sync pairs pull together,
offset pairs push apart.
is_synced: (B,) bool tensor
"""
B, T, C, H, W = mouth_crops.shape
_, _, _, F_mel, W_mel = mel_specs.shape
v = self.visual_net(mouth_crops.reshape(B * T, C, H, W)).reshape(B, T, -1).mean(1)
a = self.audio_net(mel_specs.reshape(B * T, 1, F_mel, W_mel)).reshape(B, T, -1).mean(1)
sim = F.cosine_similarity(v, a, dim=-1) # (B,)
margin = 0.4
loss_pos = (1 - sim) * is_synced.float()
loss_neg = torch.clamp(sim - margin, min=0) * (~is_synced).float()
return (loss_pos + loss_neg).mean()
if __name__ == '__main__':
model = LipSyncBranch()
mouths = torch.randn(2, 125, 3, 96, 96)
mels = torch.randn(2, 125, 1, 80, 16)
emb, logit, sync = model(mouths, mels)
print(f'LipSync embed: {emb.shape}, logit: {logit.shape}, sync profile: {sync.shape}')