Coverage for tests/test_wrapper_fantasytalking.py: 100%

92 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-09 04:47 +0000

1#!/usr/bin/env python3 

2 

3import math 

4import sys 

5import pytest 

6 

7from unittest.mock import patch 

8from unittest.mock import MagicMock 

9from tests.torch_mock import TorchMock 

10 

11from PIL import Image 

12 

13mock_torch = TorchMock() 

14 

15sys.path.append("wrapper") 

16sys.path.append("wrapper/fantasytalking") 

17 

18mock_modules = { 

19 'nvidia_smi': MagicMock(), 

20 'imageio': MagicMock(), 

21 'cv2': MagicMock(), 

22 'torch': mock_torch, 

23 'torchvision': MagicMock(), 

24 'torchvision.transforms': MagicMock(), 

25 'torchvision.transforms.functional': MagicMock(), 

26 'xfuser': MagicMock(), 

27 'xfuser.config': MagicMock(), 

28 'xfuser.core': MagicMock(), 

29 'xfuser.core.distributed': MagicMock(), 

30 'diffsynth': MagicMock(), 

31 'diffsynth.models': MagicMock(), 

32 'diffsynth.models.wan_video_dit': MagicMock(), 

33 'wan.distributed': MagicMock(), 

34 'wan.distributed.xdit_context_parallel': MagicMock(), 

35 'wan.distributed.fsdp': MagicMock(), 

36 'transformers': MagicMock(), 

37 'model': MagicMock(), 

38 'utils': MagicMock(), 

39} 

40mock_modules.update(mock_torch.get_sub_modules()) 

41 

42with patch.dict(sys.modules, mock_modules): 

43 from fantasytalking.wrapper_fantasytalking import FantasyTalking 

44 from fantasytalking.wrapper_fantasytalking import resample_frames 

45 from fantasytalking.wrapper_fantasytalking import resample_and_normalize_frames 

46 

47 

48@pytest.mark.asyncio 

49async def test_fantasytalking_e2e() -> None: 

50 model = FantasyTalking() 

51 assert model is not None 

52 assert model.model_name == "fantasytalking" 

53 assert model.status == "initializing" 

54 

55 model.init() 

56 assert model.status == "ok" 

57 health = model.get_health() 

58 assert health is not None 

59 timestamps = model.get_timestamps() 

60 assert timestamps is not None 

61 

62 with pytest.raises(ValueError, match="Missing JSON body"): 

63 await model.get_rest_args(None) 

64 

65 with pytest.raises(ValueError, match="Missing 'audio' parameter"): 

66 await model.get_rest_args({}) 

67 

68 with pytest.raises(ValueError, match="Missing 'prompt' parameter"): 

69 await model.get_rest_args({"audio": "test"}) 

70 

71 rest_args = await model.get_rest_args({ 

72 "audio": "test", 

73 "prompt": "test prompt", 

74 }) 

75 assert "args" in rest_args 

76 assert rest_args["args"]["audio_path"].startswith("/tmp/tmp") 

77 assert rest_args["args"]["audio_path"].endswith(".wav") 

78 

79 await model.get_rest_args({ 

80 "audio": "test", 

81 "prompt": "test prompt", 

82 "job_id": "testjob", 

83 }) 

84 assert "args" in rest_args 

85 assert rest_args["args"]["audio_path"].endswith(".wav") 

86 assert rest_args["args"]["audio_cfg_scale"] == 5.0 

87 

88 with pytest.raises((TypeError, ValueError)): 

89 await model.warmup() 

90 

91 with pytest.raises(TypeError, match="required positional arguments"): 

92 await model.generate() 

93 

94 with pytest.raises(ValueError, match="Audio file 'nonexisting.wav' does not exist"): 

95 await model.generate( 

96 img=Image.new('RGB', (100, 100)), 

97 audio_path="nonexisting.wav", 

98 prompt="test prompt", 

99 video=None) 

100 

101 with pytest.raises((TypeError, ValueError)): 

102 # TODO improve mocking 

103 await model.generate( 

104 img=Image.new('RGB', (100, 100)), 

105 audio_path="tests/data/audio_4675.wav", 

106 prompt="test prompt", 

107 video=None) 

108 

109 with pytest.raises(TypeError): 

110 # TODO improve mocking 

111 await model.generate( 

112 video=[ 

113 Image.new('RGB', (100, 100)) 

114 for _ in range(3) 

115 ], 

116 audio_path="tests/data/audio_4675.wav", 

117 prompt="test prompt") 

118 

119 with pytest.raises((TypeError, ValueError)): 

120 # TODO improve mocking 

121 await model.generate( 

122 img=None, 

123 video=None, 

124 audio_path="tests/data/audio_4675.wav", 

125 prompt="test prompt") 

126 

127 del model 

128 

129 

130def test_resample_frames() -> None: 

131 resampled_frames = resample_frames([], 30, 23) 

132 assert resampled_frames == [] 

133 

134 resampled_frames = resample_frames([ 

135 Image.new("RGB", (100, 100)) 

136 for _ in range(3) 

137 ], 30, 23) 

138 assert len(resampled_frames) == 2 

139 

140 resampled_frames = resample_frames([ 

141 Image.new("RGB", (100, 100)) 

142 for _ in range(24) 

143 ], 24, 23, 0.5) 

144 assert len(resampled_frames) == 12 

145 

146 

147def test_resample_frames_may_differ_from_num_frames() -> None: 

148 """Reproduce the bug scenario: 73 frames at 30 FPS with ~2.4s audio. 

149 

150 Before the fix, resample_frames returns 55 frames while num_frames (computed 

151 from audio at 23 FPS aligned to 1+4n) is 57, causing a latent/noise tensor 

152 shape mismatch (14 vs 15 frames) inside CustomWanVideoPipeline.__call__. 

153 This test documents that resample_frames can return fewer frames than 

154 num_frames; the fix in generate() normalises the result via 

155 resample_and_normalize_frames() so both tensors share the same latent dimension. 

156 """ 

157 

158 FPS = 23.0 

159 SRC_FPS = 30.0 

160 vae_stride = 4 

161 

162 audio_duration = 2.4 # seconds — representative value from the bug report 

163 audio_num_frames = int(math.ceil(FPS * audio_duration)) 

164 num_frames = int(1 + math.ceil((audio_num_frames - 1) / vae_stride) * vae_stride) 

165 

166 src_frames = [Image.new("RGB", (100, 100)) for _ in range(73)] 

167 

168 # resample_frames alone produces fewer frames than num_frames due to rounding 

169 resampled_raw = resample_frames(src_frames, SRC_FPS, FPS, audio_duration) 

170 assert len(resampled_raw) < num_frames, ( 

171 f"Expected resampled ({len(resampled_raw)}) < num_frames ({num_frames})" 

172 ) 

173 

174 # resample_and_normalize_frames must return exactly num_frames 

175 normalized = resample_and_normalize_frames( 

176 src_frames, SRC_FPS, FPS, num_frames, audio_duration) 

177 assert len(normalized) == num_frames, ( 

178 f"After normalization, expected {num_frames} frames but got {len(normalized)}" 

179 ) 

180 

181 # Verify that the latent dimensions now match 

182 lat_expected = (num_frames - 1) // vae_stride + 1 

183 lat_actual = (len(normalized) - 1) // vae_stride + 1 

184 assert lat_expected == lat_actual, ( 

185 f"Latent frame count mismatch: noise={lat_expected}, latents={lat_actual}" 

186 ) 

187 

188 

189def test_normalize_frames_trimming() -> None: 

190 """Test that resample_and_normalize_frames trims oversized resampled video to num_frames.""" 

191 

192 FPS = 23.0 

193 SRC_FPS = 30.0 

194 vae_stride = 4 

195 

196 # Audio duration just long enough that audio_num_frames rounds up such that 

197 # num_frames (1+4n aligned) is smaller than what resample_frames would produce 

198 # without the truncation step. We achieve this by supplying no audio_duration 

199 # to resample_frames so the truncation branch is skipped, giving more dst frames. 

200 audio_duration = 1.0 # 1 second 

201 audio_num_frames = int(math.ceil(FPS * audio_duration)) # 23 

202 num_frames = int(1 + math.ceil((audio_num_frames - 1) / vae_stride) * vae_stride) # 1+24=25 

203 

204 # Provide a video that, when resampled WITHOUT the audio-duration truncation, 

205 # produces more frames than num_frames. 

206 # 30 frames at 30 FPS = 1.0 s → round(1.0 * 23) = 23 frames < 25, so use a 

207 # slightly longer source to get more dest frames. 

208 # 35 frames at 30 FPS = 1.167 s → round(1.167 * 23) = round(26.83) = 27 frames > 25 

209 src_frames = [Image.new("RGB", (100, 100)) for _ in range(35)] 

210 

211 # Without normalization, resample_frames produces more frames than num_frames 

212 resampled_raw = resample_frames(src_frames, SRC_FPS, FPS) 

213 assert len(resampled_raw) > num_frames, ( 

214 f"Expected resampled ({len(resampled_raw)}) > num_frames ({num_frames})" 

215 ) 

216 

217 # resample_and_normalize_frames must return exactly num_frames 

218 normalized = resample_and_normalize_frames(src_frames, SRC_FPS, FPS, num_frames) 

219 assert len(normalized) == num_frames 

220 

221 lat_expected = (num_frames - 1) // vae_stride + 1 

222 lat_actual = (len(normalized) - 1) // vae_stride + 1 

223 assert lat_expected == lat_actual