syncnet-python 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,210 @@
1
+ import glob
2
+ import math
3
+ import os
4
+ import subprocess
5
+ import time
6
+
7
+ import cv2
8
+ import numpy
9
+ import python_speech_features
10
+ import torch
11
+
12
+ from scipy import signal
13
+ from scipy.io import wavfile
14
+
15
+
16
+ # ==================== Get OFFSET ====================
17
+
18
+
19
+ def calc_pdist(feat1, feat2, vshift=10):
20
+ win_size = vshift * 2 + 1
21
+
22
+ feat2p = torch.nn.functional.pad(feat2, (0, 0, vshift, vshift))
23
+
24
+ dists = []
25
+
26
+ for i in range(0, len(feat1)):
27
+ dists.append(
28
+ torch.nn.functional.pairwise_distance(
29
+ feat1[[i], :].repeat(win_size, 1), feat2p[i : i + win_size, :]
30
+ )
31
+ )
32
+
33
+ return dists
34
+
35
+
36
+ # ==================== MAIN DEF ====================
37
+
38
+
39
+ class SyncNetInstance(torch.nn.Module):
40
+ def __init__(
41
+ self,
42
+ net: torch.nn.Module,
43
+ device: str = "cuda",
44
+ dropout: float = 0,
45
+ num_layers_in_fc_layers: int = 1024,
46
+ ):
47
+ super(SyncNetInstance, self).__init__()
48
+ self.__S__ = net
49
+ self.device = device
50
+
51
+ def evaluate(self, opt):
52
+ self.__S__.to(self.device)
53
+ self.__S__.eval()
54
+
55
+ # ========== ==========
56
+ # Load video
57
+ # ========== ==========
58
+
59
+ images = []
60
+
61
+ flist = glob.glob(os.path.join(opt.tmp_dir, "*.jpg"))
62
+ flist.sort()
63
+
64
+ for fname in flist:
65
+ images.append(cv2.imread(fname))
66
+
67
+ im = numpy.stack(images, axis=3)
68
+ im = numpy.expand_dims(im, axis=0)
69
+ im = numpy.transpose(im, (0, 3, 4, 1, 2))
70
+
71
+ imtv = torch.autograd.Variable(torch.from_numpy(im.astype(float)).float())
72
+
73
+ # ========== ==========
74
+ # Load audio
75
+ # ========== ==========
76
+
77
+ sample_rate, audio = wavfile.read(os.path.join(opt.tmp_dir, "audio.wav"))
78
+ mfcc = zip(*python_speech_features.mfcc(audio, sample_rate))
79
+ mfcc = numpy.stack([numpy.array(i) for i in mfcc])
80
+
81
+ cc = numpy.expand_dims(numpy.expand_dims(mfcc, axis=0), axis=0)
82
+ cct = torch.autograd.Variable(torch.from_numpy(cc.astype(float)).float())
83
+
84
+ # ========== ==========
85
+ # Check audio and video input length
86
+ # ========== ==========
87
+
88
+ if (float(len(audio)) / 16000) != (float(len(images)) / 25):
89
+ print(
90
+ "WARNING: Audio (%.4fs) and video (%.4fs) lengths are different."
91
+ % (float(len(audio)) / 16000, float(len(images)) / 25)
92
+ )
93
+
94
+ min_length = min(len(images), math.floor(len(audio) / 640))
95
+
96
+ # ========== ==========
97
+ # Generate video and audio feats
98
+ # ========== ==========
99
+
100
+ lastframe = min_length - 5
101
+ im_feat = []
102
+ cc_feat = []
103
+
104
+ tS = time.time()
105
+ for i in range(0, lastframe, opt.batch_size):
106
+ im_batch = [
107
+ imtv[:, :, vframe : vframe + 5, :, :]
108
+ for vframe in range(i, min(lastframe, i + opt.batch_size))
109
+ ]
110
+ im_in = torch.cat(im_batch, 0).to(self.device)
111
+ im_out = self.__S__.forward_lip(im_in)
112
+ im_feat.append(im_out.data.cpu())
113
+
114
+ cc_batch = [
115
+ cct[:, :, :, vframe * 4 : vframe * 4 + 20]
116
+ for vframe in range(i, min(lastframe, i + opt.batch_size))
117
+ ]
118
+ cc_in = torch.cat(cc_batch, 0).to(self.device)
119
+ cc_out = self.__S__.forward_aud(cc_in)
120
+ cc_feat.append(cc_out.data.cpu())
121
+
122
+ im_feat = torch.cat(im_feat, 0)
123
+ cc_feat = torch.cat(cc_feat, 0)
124
+
125
+ # ========== ==========
126
+ # Compute offset
127
+ # ========== ==========
128
+
129
+ print("Compute time %.3f sec." % (time.time() - tS))
130
+
131
+ dists = calc_pdist(im_feat, cc_feat, vshift=opt.vshift)
132
+ mdist = torch.mean(torch.stack(dists, 1), 1)
133
+
134
+ minval, minidx = torch.min(mdist, 0)
135
+
136
+ offset = opt.vshift - minidx
137
+ conf = torch.median(mdist) - minval
138
+
139
+ fdist = numpy.stack([dist[minidx].numpy() for dist in dists])
140
+ # fdist = numpy.pad(fdist, (3,3), 'constant', constant_values=15)
141
+ fconf = torch.median(mdist).numpy() - fdist
142
+ fconfm = signal.medfilt(fconf, kernel_size=9)
143
+
144
+ numpy.set_printoptions(formatter={"float": "{: 0.3f}".format})
145
+ print("Framewise conf: ")
146
+ print(fconfm)
147
+ print(
148
+ "AV offset: \t%d \nMin dist: \t%.3f\nConfidence: \t%.3f"
149
+ % (offset, minval, conf)
150
+ )
151
+
152
+ # dists_npy = numpy.array([dist.numpy() for dist in dists])
153
+ # return offset.numpy(), conf.numpy(), minval.numpy()
154
+ return int(offset), float(conf), float(minval)
155
+
156
+ def extract_feature(self, opt, videofile):
157
+ self.__S__.eval()
158
+ # ========== ==========
159
+ # Load video
160
+ # ========== ==========
161
+ cap = cv2.VideoCapture(videofile)
162
+
163
+ frame_num = 1
164
+ images = []
165
+ while frame_num:
166
+ frame_num += 1
167
+ ret, image = cap.read()
168
+ if ret == 0:
169
+ break
170
+
171
+ images.append(image)
172
+
173
+ im = numpy.stack(images, axis=3)
174
+ im = numpy.expand_dims(im, axis=0)
175
+ im = numpy.transpose(im, (0, 3, 4, 1, 2))
176
+
177
+ imtv = torch.autograd.Variable(torch.from_numpy(im.astype(float)).float())
178
+
179
+ # ========== ==========
180
+ # Generate video feats
181
+ # ========== ==========
182
+
183
+ lastframe = len(images) - 4
184
+ im_feat = []
185
+
186
+ tS = time.time()
187
+ for i in range(0, lastframe, opt.batch_size):
188
+ im_batch = [
189
+ imtv[:, :, vframe : vframe + 5, :, :]
190
+ for vframe in range(i, min(lastframe, i + opt.batch_size))
191
+ ]
192
+ im_in = torch.cat(im_batch, 0).to(self.device)
193
+ im_out = self.__S__.forward_lipfeat(im_in)
194
+ im_feat.append(im_out.data.cpu())
195
+
196
+ im_feat = torch.cat(im_feat, 0)
197
+
198
+ # ========== ==========
199
+ # Compute offset
200
+ # ========== ==========
201
+
202
+ print("Compute time %.3f sec." % (time.time() - tS))
203
+
204
+ return im_feat
205
+
206
+ def loadParameters(self, path):
207
+ loaded_state = torch.load(path, map_location=lambda storage, loc: storage)
208
+ self_state = self.__S__.state_dict()
209
+ for name, param in loaded_state.items():
210
+ self_state[name].copy_(param)
@@ -0,0 +1,99 @@
1
+ import torch
2
+ import torch.nn as nn
3
+
4
+
5
+ def save(model, filename):
6
+ with open(filename, "wb") as f:
7
+ torch.save(model, f)
8
+ print("%s saved." % filename)
9
+
10
+
11
+ def load(filename):
12
+ net = torch.load(filename)
13
+ return net
14
+
15
+
16
+ class S(nn.Module):
17
+ def __init__(self, num_layers_in_fc_layers=1024):
18
+ super(S, self).__init__()
19
+ self.__nFeatures__ = 24
20
+ self.__nChs__ = 32
21
+ self.__midChs__ = 32
22
+ self.netcnnaud = nn.Sequential(
23
+ nn.Conv2d(1, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)),
24
+ nn.BatchNorm2d(64),
25
+ nn.ReLU(inplace=True),
26
+ nn.MaxPool2d(kernel_size=(1, 1), stride=(1, 1)),
27
+ nn.Conv2d(64, 192, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)),
28
+ nn.BatchNorm2d(192),
29
+ nn.ReLU(inplace=True),
30
+ nn.MaxPool2d(kernel_size=(3, 3), stride=(1, 2)),
31
+ nn.Conv2d(192, 384, kernel_size=(3, 3), padding=(1, 1)),
32
+ nn.BatchNorm2d(384),
33
+ nn.ReLU(inplace=True),
34
+ nn.Conv2d(384, 256, kernel_size=(3, 3), padding=(1, 1)),
35
+ nn.BatchNorm2d(256),
36
+ nn.ReLU(inplace=True),
37
+ nn.Conv2d(256, 256, kernel_size=(3, 3), padding=(1, 1)),
38
+ nn.BatchNorm2d(256),
39
+ nn.ReLU(inplace=True),
40
+ nn.MaxPool2d(kernel_size=(3, 3), stride=(2, 2)),
41
+ nn.Conv2d(256, 512, kernel_size=(5, 4), padding=(0, 0)),
42
+ nn.BatchNorm2d(512),
43
+ nn.ReLU(),
44
+ )
45
+ self.netfcaud = nn.Sequential(
46
+ nn.Linear(512, 512),
47
+ nn.BatchNorm1d(512),
48
+ nn.ReLU(),
49
+ nn.Linear(512, num_layers_in_fc_layers),
50
+ )
51
+ self.netfclip = nn.Sequential(
52
+ nn.Linear(512, 512),
53
+ nn.BatchNorm1d(512),
54
+ nn.ReLU(),
55
+ nn.Linear(512, num_layers_in_fc_layers),
56
+ )
57
+ self.netcnnlip = nn.Sequential(
58
+ nn.Conv3d(3, 96, kernel_size=(5, 7, 7), stride=(1, 2, 2), padding=0),
59
+ nn.BatchNorm3d(96),
60
+ nn.ReLU(inplace=True),
61
+ nn.MaxPool3d(kernel_size=(1, 3, 3), stride=(1, 2, 2)),
62
+ nn.Conv3d(
63
+ 96, 256, kernel_size=(1, 5, 5), stride=(1, 2, 2), padding=(0, 1, 1)
64
+ ),
65
+ nn.BatchNorm3d(256),
66
+ nn.ReLU(inplace=True),
67
+ nn.MaxPool3d(kernel_size=(1, 3, 3), stride=(1, 2, 2), padding=(0, 1, 1)),
68
+ nn.Conv3d(256, 256, kernel_size=(1, 3, 3), padding=(0, 1, 1)),
69
+ nn.BatchNorm3d(256),
70
+ nn.ReLU(inplace=True),
71
+ nn.Conv3d(256, 256, kernel_size=(1, 3, 3), padding=(0, 1, 1)),
72
+ nn.BatchNorm3d(256),
73
+ nn.ReLU(inplace=True),
74
+ nn.Conv3d(256, 256, kernel_size=(1, 3, 3), padding=(0, 1, 1)),
75
+ nn.BatchNorm3d(256),
76
+ nn.ReLU(inplace=True),
77
+ nn.MaxPool3d(kernel_size=(1, 3, 3), stride=(1, 2, 2)),
78
+ nn.Conv3d(256, 512, kernel_size=(1, 6, 6), padding=0),
79
+ nn.BatchNorm3d(512),
80
+ nn.ReLU(inplace=True),
81
+ )
82
+
83
+ def forward_aud(self, x):
84
+ mid = self.netcnnaud(x) # N x ch x 24 x M
85
+ mid = mid.view((mid.size()[0], -1)) # N x (ch x 24)
86
+ out = self.netfcaud(mid)
87
+ return out
88
+
89
+ def forward_lip(self, x):
90
+ mid = self.netcnnlip(x)
91
+ mid = mid.view((mid.size()[0], -1)) # N x (ch x 24)
92
+ out = self.netfclip(mid)
93
+ return out
94
+
95
+ def forward_lipfeat(self, x):
96
+ mid = self.netcnnlip(x)
97
+ out = mid.view((mid.size()[0], -1)) # N x (ch x 24)
98
+
99
+ return out
@@ -0,0 +1,29 @@
1
+ """SyncNet Python: Audio-visual synchronization detection.
2
+
3
+ This package provides a PyTorch implementation of SyncNet for detecting
4
+ synchronization between audio and video in multimedia content.
5
+ """
6
+
7
+ __version__ = "0.1.0"
8
+
9
+ # Import main components
10
+ try:
11
+ from .syncnet_pipeline import SyncNetPipeline
12
+ from .SyncNetModel import S as SyncNetModel
13
+ from .SyncNetInstance import SyncNetInstance
14
+ except ImportError:
15
+ # Fallback for development
16
+ SyncNetPipeline = None
17
+ SyncNetModel = None
18
+ SyncNetInstance = None
19
+
20
+ __all__ = [
21
+ "SyncNetPipeline",
22
+ "SyncNetModel",
23
+ "SyncNetInstance",
24
+ "__version__"
25
+ ]
26
+
27
+ def get_version():
28
+ """Get package version."""
29
+ return __version__
syncnet_python/cli.py ADDED
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env python3
2
+ """Command-line interface for SyncNet Python."""
3
+
4
+ import argparse
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from .syncnet_pipeline import SyncNetPipeline
10
+
11
+
12
+ def main():
13
+ """Main CLI entry point."""
14
+ parser = argparse.ArgumentParser(
15
+ description="SyncNet: Audio-visual synchronization detection",
16
+ formatter_class=argparse.RawDescriptionHelpFormatter,
17
+ epilog="""
18
+ Examples:
19
+ # Process a single video
20
+ syncnet-python video.mp4
21
+
22
+ # Process multiple videos
23
+ syncnet-python video1.mp4 video2.mp4 --output results.json
24
+
25
+ # Use CPU instead of GPU
26
+ syncnet-python video.mp4 --device cpu
27
+ """
28
+ )
29
+
30
+ parser.add_argument(
31
+ 'videos',
32
+ nargs='+',
33
+ help='Video file(s) to process'
34
+ )
35
+
36
+ parser.add_argument(
37
+ '-o', '--output',
38
+ help='Output JSON file for results'
39
+ )
40
+
41
+ parser.add_argument(
42
+ '--device',
43
+ default='cuda',
44
+ choices=['cuda', 'cpu'],
45
+ help='Device to run models on (default: cuda)'
46
+ )
47
+
48
+ parser.add_argument(
49
+ '--s3fd-weights',
50
+ default='weights/sfd_face.pth',
51
+ help='Path to S3FD weights'
52
+ )
53
+
54
+ parser.add_argument(
55
+ '--syncnet-weights',
56
+ default='weights/syncnet_v2.model',
57
+ help='Path to SyncNet weights'
58
+ )
59
+
60
+ parser.add_argument(
61
+ '--cache-dir',
62
+ help='Directory for temporary files'
63
+ )
64
+
65
+ args = parser.parse_args()
66
+
67
+ # Initialize pipeline
68
+ try:
69
+ pipeline = SyncNetPipeline(
70
+ {
71
+ "s3fd_weights": args.s3fd_weights,
72
+ "syncnet_weights": args.syncnet_weights,
73
+ },
74
+ device=args.device,
75
+ )
76
+ except Exception as e:
77
+ print(f"Error initializing pipeline: {e}", file=sys.stderr)
78
+ return 1
79
+
80
+ results_all = []
81
+
82
+ # Process videos
83
+ for video_path in args.videos:
84
+ print(f"Processing: {video_path}")
85
+
86
+ try:
87
+ results = pipeline.inference(
88
+ video_path=video_path,
89
+ audio_path=None,
90
+ cache_dir=args.cache_dir
91
+ )
92
+
93
+ offsets, confs, dists, max_conf, min_dist, s3fd_json, has_face = results
94
+
95
+ result_dict = {
96
+ "video": video_path,
97
+ "status": "success",
98
+ "offset": int(offsets[0]) if offsets else None,
99
+ "confidence": float(max_conf),
100
+ "min_distance": float(min_dist),
101
+ "has_face": has_face
102
+ }
103
+
104
+ print(f" Offset: {offsets[0] if offsets else 'N/A'} frames")
105
+ print(f" Confidence: {max_conf:.3f}")
106
+
107
+ except Exception as e:
108
+ result_dict = {
109
+ "video": video_path,
110
+ "status": "error",
111
+ "error": str(e)
112
+ }
113
+ print(f" Error: {e}")
114
+
115
+ results_all.append(result_dict)
116
+
117
+ # Output results
118
+ if args.output:
119
+ with open(args.output, 'w') as f:
120
+ json.dump(results_all, f, indent=2)
121
+ print(f"\nResults saved to: {args.output}")
122
+
123
+ return 0
124
+
125
+
126
+ if __name__ == '__main__':
127
+ sys.exit(main())
@@ -0,0 +1 @@
1
+ from .s3fd import S3FD
@@ -0,0 +1,66 @@
1
+ import logging
2
+ import time
3
+
4
+ import cv2
5
+ import numpy as np
6
+ import torch
7
+
8
+ from .box_utils import nms_
9
+ from .nets import S3FDNet
10
+
11
+ img_mean = np.array([104.0, 117.0, 123.0])[:, np.newaxis, np.newaxis].astype("float32")
12
+
13
+
14
+ class S3FD:
15
+ def __init__(self, net: S3FDNet, device="cuda"):
16
+ """
17
+ We now accept an *already-initialized* S3FDNet as `net`,
18
+ instead of loading weights here.
19
+ """
20
+ tstamp = time.time()
21
+ self.device = device
22
+ self.net = net.to(self.device)
23
+ self.net.eval()
24
+ logging.info(
25
+ f"[S3FD] S3FDNet instance is ready (initialized in {time.time()-tstamp:.4f} sec)."
26
+ )
27
+
28
+ def detect_faces(self, image, conf_th=0.8, scales=[1]):
29
+ """
30
+ Same detection code as before, but we no longer load the model here.
31
+ """
32
+ self.net.to(self.device)
33
+ self.net.eval()
34
+ w, h = image.shape[1], image.shape[0]
35
+ bboxes = np.empty(shape=(0, 5))
36
+
37
+ with torch.no_grad():
38
+ for s in scales:
39
+ scaled_img = cv2.resize(
40
+ image, dsize=(0, 0), fx=s, fy=s, interpolation=cv2.INTER_LINEAR
41
+ )
42
+ scaled_img = np.swapaxes(scaled_img, 1, 2)
43
+ scaled_img = np.swapaxes(scaled_img, 1, 0)
44
+ scaled_img = scaled_img[[2, 1, 0], :, :]
45
+ scaled_img = scaled_img.astype("float32")
46
+ scaled_img -= img_mean
47
+ scaled_img = scaled_img[[2, 1, 0], :, :]
48
+ x = torch.from_numpy(scaled_img).unsqueeze(0).to(self.device)
49
+
50
+ y = self.net(x) # forward pass
51
+ detections = y.data.to(self.device)
52
+ scale_tensor = torch.Tensor([w, h, w, h]).to(self.device)
53
+
54
+ for i in range(detections.size(1)):
55
+ j = 0
56
+ while detections[0, i, j, 0] > conf_th:
57
+ score = detections[0, i, j, 0].item()
58
+ pt = (detections[0, i, j, 1:] * scale_tensor).cpu().numpy()
59
+ bbox = (pt[0], pt[1], pt[2], pt[3], score)
60
+ bboxes = np.vstack((bboxes, bbox))
61
+ j += 1
62
+
63
+ # NMS, etc. (unchanged)
64
+ keep = nms_(bboxes, 0.1)
65
+ bboxes = bboxes[keep]
66
+ return bboxes