Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

426 Zeilen
18 KiB

  1. """test_feature_extractors.py - test pipeline feature extractors"""
  2. import sys
  3. from unittest.mock import patch, mock_open, MagicMock #
  4. sys.modules["faster_whisper"] = MagicMock() # mock faster_whisper as it is a slow import
  5. import unittest
  6. import os
  7. import random
  8. import pytest
  9. import pipeline.feature_extractors as extractors
  10. from pipeline.utils import Source, SourceMedia # technically makes this an integration test, but...
  11. class TestSource():
  12. """Provide utils.Source for testing"""
  13. def one_colour_silent_audio(self):
  14. """Provide a source with a silent mono-colour video"""
  15. TEST_DIR = os.path.dirname(os.path.realpath(__file__))
  16. SAMPLE_VIDEO = f"{TEST_DIR}/sample_videos/test_video_red_silentaudio.mp4" # silent video definitely has no laughter
  17. return Source(source=SAMPLE_VIDEO, path=SAMPLE_VIDEO, provider="test")
  18. class TestSourceMedia():
  19. """Provide utils.SourceMedia for testing"""
  20. def one_colour_silent_audio(self):
  21. """Provide a source with a silent mono-colour video"""
  22. return SourceMedia(sources=[TestSource().one_colour_silent_audio()])
  23. class MockReadJSON():
  24. """Mock read_json"""
  25. def mock_read_json_from_file(self, *args, **kwargs):
  26. """Mock _read_json_from_file()"""
  27. rJSON = [{"interval": {"start": 0.0, "duration": 1.0},
  28. "source": {"source": "test_video_red_silentaudio.mp4",
  29. "path": "test_video_red_silentaudio.mp4",
  30. "provider": "mock"},
  31. "feature_extractor": "MockFeatureExtractor",
  32. "score": 0.5
  33. }]
  34. return rJSON
  35. class TestLaughterFeatureExtractor(unittest.TestCase):
  36. def _mock_laughdetect_callout(self, *args, **kwargs):
  37. """Mock _laughdetect callout
  38. **kwargs:
  39. - n : int >=0, number of laughter instances to generate
  40. Return a list of 2-tuple floats (start, end) representing laughter instances
  41. """
  42. laughs = []
  43. n = kwargs.get("n", 0)
  44. for i in range(n):
  45. laughs.append((i, i+1))
  46. return laughs
  47. def _mock_run_get_output(self, *args, **kwargs) -> str:
  48. """Mock run_get_output callout
  49. kwargs:
  50. - n : int >=0, number of laughter instances to generate
  51. Return a string of laughter instance of the form:
  52. instance: (1.234, 5.678)
  53. """
  54. # TODO: decide if we want non-"instance" output for testing parsing?
  55. # (maybe)
  56. output = []
  57. n = kwargs.get("n", 0)
  58. for i in range(n):
  59. output.append(f"instance: ({i}.{i+1}{i+2}{i+3}, {i+4}.{i+5}{i+6}{i+7})")
  60. return "\n".join(output)
  61. def _sgo5(self, *args, **kwargs):
  62. """Mock run_get_output callout"""
  63. return self._mock_run_get_output(*args, **kwargs, n=5)
  64. """Test LaughterFeatureExtractor"""
  65. def test_init(self):
  66. test_extractor = extractors.LaughterFeatureExtractor()
  67. self.assertTrue(test_extractor)
  68. def test_setup_noinput(self):
  69. """test setup - no input files"""
  70. test_extractor = extractors.LaughterFeatureExtractor()
  71. with self.assertRaises(ValueError):
  72. test_extractor.setup()
  73. # NB test WITH sources implicitly tested in test_extract
  74. @pytest.mark.slow
  75. def test_extract_mocked_nolaughs(self):
  76. """Test extract with mocked laughter detection - no laughs"""
  77. video_source = TestSource().one_colour_silent_audio()
  78. test_extractor = extractors.LaughterFeatureExtractor(input_files=[video_source])
  79. test_extractor._laughdetect = self._mock_laughdetect_callout
  80. test_extractor.setup()
  81. test_extractor.run()
  82. test_extractor.teardown()
  83. self.assertEqual(len(test_extractor.features), 0)
  84. def test_extract_mocked_run_get_output_none(self):
  85. """Test extract with mocked laughter detection - no laughs"""
  86. video_source = TestSource().one_colour_silent_audio()
  87. test_extractor = extractors.LaughterFeatureExtractor(input_files=[video_source])
  88. test_extractor._run_get_output = self._mock_run_get_output
  89. test_extractor.setup()
  90. test_extractor.run()
  91. test_extractor.teardown()
  92. self.assertEqual(len(test_extractor.features), 0)
  93. def test_extract_mocked_run_get_output_5(self):
  94. """Test extract with mocked laughter detection - 5 laughs"""
  95. video_source = TestSource().one_colour_silent_audio()
  96. test_extractor = extractors.LaughterFeatureExtractor(input_files=[video_source])
  97. test_extractor._run_get_output = self._sgo5
  98. test_extractor.setup()
  99. test_extractor.run()
  100. test_extractor.teardown()
  101. self.assertEqual(len(test_extractor.features), 5)
  102. def test_run_get_output(self):
  103. """Test run_get_output"""
  104. video_source = TestSource().one_colour_silent_audio()
  105. test_extractor = extractors.LaughterFeatureExtractor(input_files=[video_source])
  106. test_cmd = ["echo", "foo"]
  107. test_extractor.setup()
  108. output = test_extractor._run_get_output(test_cmd)
  109. self.assertEqual(output, "foo\n")
  110. # TODO: add sample video with laughs to test _laughdetect()
  111. class TestRandomFeatureExtractor(unittest.TestCase):
  112. """Test RandomFeatureExtractor"""
  113. def test_init(self):
  114. test_extractor = extractors.RandomFeatureExtractor()
  115. self.assertTrue(test_extractor)
  116. def test_setup_noinput(self):
  117. """test setup - no input files"""
  118. test_extractor = extractors.RandomFeatureExtractor()
  119. with self.assertRaises(ValueError):
  120. test_extractor.setup()
  121. # NB test WITH sources implicitly tested in test_extract
  122. def test_extract_noinput(self):
  123. """Test extract with no input files"""
  124. test_extractor = extractors.RandomFeatureExtractor()
  125. with self.assertRaises(ValueError):
  126. test_extractor.run()
  127. def test_extract(self):
  128. """Test extract with input files"""
  129. video_source = TestSourceMedia().one_colour_silent_audio()
  130. test_extractor = extractors.RandomFeatureExtractor(input_files=video_source)
  131. test_extractor.setup()
  132. test_extractor.run()
  133. test_extractor.teardown()
  134. self.assertTrue(test_extractor.features)
  135. class TestLoudAudioFeatureExtractor(unittest.TestCase):
  136. """Test LoudAudioFeatureExtractor"""
  137. def _mock_loudnorm_5(self, *args, **kwargs):
  138. """Mock _loudnorm
  139. It returns a list of 2-tuple floats (time, loudness) representing loud audio instances
  140. """
  141. return [(0.0, 0.0), (15.0, 1.0), (25.0, 2.0), (35.0, 3.0), (45.0, 4.0)]
  142. def _mock_get_loudnessess(self, *args, length=100, min_loudness=-101, max_loudness=100,
  143. seed=42, **kwargs) -> list:
  144. """Mock _get_loudnesses()
  145. Parameters:
  146. - length : int >=0, number of loudness instances to generate
  147. - min_loudness : int, minimum loudness value (special value: -101 for "-inf")
  148. - max_loudness : int, maximum loudness value
  149. Note that int min/max loudness are divided by float 100
  150. to get the actual loudness value between -1.0 and 1.0
  151. Return a list of 2-tuple floats (timecode, loudness) representing loud audio instances
  152. """
  153. loudnesses = []
  154. random.seed(seed)
  155. for i in range(length):
  156. loudness = random.randint(min_loudness, max_loudness) / 100
  157. if min_loudness == -101:
  158. loudness = "-inf" if loudness == -1.01 else f"{loudness}"
  159. loudnesses.append((float(f"{i*20}.0"), float(loudness)))
  160. return loudnesses
  161. def test_init(self):
  162. video_source = TestSourceMedia().one_colour_silent_audio()
  163. test_extractor = extractors.LoudAudioFeatureExtractor(input_files=video_source)
  164. self.assertTrue(test_extractor)
  165. def test_init_noinput(self):
  166. """test init - no input files"""
  167. with self.assertRaises(ValueError):
  168. test_extractor = extractors.LoudAudioFeatureExtractor()
  169. def test_extract(self):
  170. """Test extract with input files"""
  171. video_source = TestSourceMedia().one_colour_silent_audio()
  172. test_extractor = extractors.LoudAudioFeatureExtractor(input_files=video_source)
  173. test_extractor.setup()
  174. test_extractor.run()
  175. test_extractor.teardown()
  176. self.assertEqual(test_extractor.features, [])
  177. def test_extract_mocked_loudnorm_5(self):
  178. """Test extract with mocked loudness detection"""
  179. video_source = TestSourceMedia().one_colour_silent_audio()
  180. test_extractor = extractors.LoudAudioFeatureExtractor(input_files=video_source)
  181. test_extractor._loudnorm = self._mock_loudnorm_5
  182. test_extractor.setup()
  183. test_extractor.run()
  184. test_extractor.teardown()
  185. self.assertEqual(len(test_extractor.features), 5)
  186. def test_extract_mocked_get_loudnesses(self):
  187. """Test extract with mocked loudness detection - 100 loudnesses generated"""
  188. video_source = TestSourceMedia().one_colour_silent_audio()
  189. test_extractor = extractors.LoudAudioFeatureExtractor(input_files=video_source, num_features=100)
  190. test_extractor._get_loudnesses = self._mock_get_loudnessess
  191. test_extractor.setup()
  192. test_extractor.run()
  193. test_extractor.teardown()
  194. self.assertEqual(len(test_extractor.features), 100)
  195. def test_keep_num(self):
  196. """Test keep_num correctly keeps 5 / 10"""
  197. min_duration = 0
  198. video_source = TestSourceMedia().one_colour_silent_audio()
  199. with self.subTest("keep 5 (default)"):
  200. test_extractor = extractors.LoudAudioFeatureExtractor(input_files=video_source,
  201. min_duration=min_duration,
  202. num_features=5)
  203. test_extractor._get_loudnesses = self._mock_get_loudnessess
  204. test_extractor.setup()
  205. test_extractor.run()
  206. test_extractor.teardown()
  207. self.assertEqual(len(test_extractor.features), 5)
  208. with self.subTest("keep 10"):
  209. test_extractor = extractors.LoudAudioFeatureExtractor(input_files=video_source,
  210. min_duration=min_duration,
  211. num_features=10)
  212. test_extractor._get_loudnesses = self._mock_get_loudnessess
  213. test_extractor.setup()
  214. test_extractor.run()
  215. test_extractor.teardown()
  216. self.assertEqual(len(test_extractor.features), 10)
  217. # test with min_duration
  218. min_duration = 100
  219. with self.subTest("min_duration"):
  220. test_extractor = extractors.LoudAudioFeatureExtractor(input_files=video_source,
  221. min_duration=min_duration,
  222. num_features=10)
  223. test_extractor._get_loudnesses = self._mock_get_loudnessess
  224. test_extractor.setup()
  225. test_extractor.run()
  226. test_extractor.teardown()
  227. for feature in test_extractor.features:
  228. self.assertGreaterEqual(feature.interval.duration, min_duration)
  229. # TODO: add sample video with loud audio to test _loudnessdetect()
  230. class TestVideoActivityFeatureExtractor(unittest.TestCase):
  231. """Test VideoActivityFeatureExtractor"""
  232. def test_init(self):
  233. video_source = TestSourceMedia().one_colour_silent_audio()
  234. test_extractor = extractors.VideoActivityFeatureExtractor(input_files=video_source)
  235. self.assertTrue(test_extractor)
  236. def test_init_noinput(self):
  237. """test init - no input files"""
  238. with self.assertRaises(ValueError):
  239. test_extractor = extractors.VideoActivityFeatureExtractor()
  240. def test_extract(self):
  241. """Test extract with basic input file runs with no errors"""
  242. num_features = 50
  243. min_duration = 0
  244. video_source = TestSourceMedia().one_colour_silent_audio()
  245. test_extractor = extractors.VideoActivityFeatureExtractor(input_files=video_source,
  246. num_features=num_features,
  247. min_duration=min_duration)
  248. test_extractor.setup()
  249. test_extractor.run()
  250. test_extractor.teardown()
  251. self.assertTrue(test_extractor.features)
  252. # TODO: add sample video with activity to test _activitydetect()
  253. class TestJSONFeatureExtractor(unittest.TestCase):
  254. """Test JSONFeatureExtractor"""
  255. def test_init(self):
  256. video_source = TestSourceMedia().one_colour_silent_audio()
  257. test_extractor = extractors.JSONFeatureExtractor(input_files=video_source)
  258. self.assertTrue(test_extractor)
  259. def test_init_noinput(self):
  260. """test init - no input files"""
  261. with self.assertRaises(ValueError):
  262. test_extractor = extractors.JSONFeatureExtractor()
  263. def test_extract(self):
  264. """Test extract with basic input file runs with no errors"""
  265. video_source = TestSourceMedia().one_colour_silent_audio()
  266. test_extractor = extractors.JSONFeatureExtractor(input_files=video_source)
  267. # mock _read_json_from_file
  268. test_extractor._read_json_from_file = MockReadJSON().mock_read_json_from_file
  269. test_extractor.setup()
  270. test_extractor.run()
  271. test_extractor.teardown()
  272. self.assertTrue(test_extractor.features)
  273. def test_read_json_from_file(self):
  274. """Test _read_json_from_file"""
  275. video_source = TestSourceMedia().one_colour_silent_audio()
  276. test_extractor = extractors.JSONFeatureExtractor(input_files=video_source)
  277. m = unittest.mock.mock_open(read_data='[{"foo": "bar"}]')
  278. with unittest.mock.patch("builtins.open", m):
  279. test_extractor._read_json_from_file("foo.json")
  280. class TestWordFeatureExtractor(unittest.TestCase):
  281. """Test WordFeatureExtractor"""
  282. @classmethod
  283. def setUpClass(cls):
  284. sys.modules["faster_whisper"] = MagicMock()
  285. _MOCK_SENTENCE = "the quick brown fox jumps over the lazy dog".split()
  286. class MockSegment():
  287. """Mock Segment -- has starte, end and text attributes"""
  288. def __init__(self, start, end, text):
  289. self.start = start
  290. self.end = end
  291. self.text = text
  292. def mock_transcribe(self, *args, **kwargs):
  293. """Mock for WhisperModel.model.transcribe
  294. returns a 2-tuple:
  295. - list of segments
  296. + segment = start, end, text
  297. - info = language, language_probability
  298. We will mock the segments- this provides 9 segments for the sentence:
  299. "the quick brown fox jumps over the lazy dog"
  300. """
  301. segments = []
  302. for i in range(len(self._MOCK_SENTENCE)):
  303. segments.append(self.MockSegment(i, i+1, self._MOCK_SENTENCE[i]))
  304. return segments, {"language": "en", "language_probability": 0.9}
  305. def test_basic_init(self):
  306. video_source = TestSourceMedia().one_colour_silent_audio()
  307. test_extractor = extractors.WordFeatureExtractor(input_files=video_source)
  308. self.assertTrue(test_extractor)
  309. def test_init_no_input_videos(self):
  310. """test init - no input files"""
  311. with self.assertRaises(ValueError):
  312. test_extractor = extractors.WordFeatureExtractor()
  313. def test_extract_no_words_supplied(self):
  314. """Test extract with basic input file but no words specirfied returns zero features"""
  315. video_source = TestSourceMedia().one_colour_silent_audio()
  316. test_extractor = extractors.WordFeatureExtractor(input_files=video_source)
  317. test_extractor.setup()
  318. test_extractor.run()
  319. test_extractor.teardown()
  320. self.assertEqual(test_extractor.features, [])
  321. def test_extract_mocked_transcribe_matching_words(self):
  322. """Mock out the actual call to transcribe but match all words in the sentence"""
  323. video_source = TestSourceMedia().one_colour_silent_audio()
  324. test_extractor = extractors.WordFeatureExtractor(input_files=video_source)
  325. # mock _transcribe and mock out model and batched pipeline for speed
  326. test_extractor._transcribe = self.mock_transcribe
  327. test_extractor._model = MagicMock()
  328. test_extractor._batched_model = MagicMock()
  329. # set up and run the extractor
  330. test_extractor.setup(words=self._MOCK_SENTENCE)
  331. test_extractor.run()
  332. test_extractor.teardown()
  333. self.assertEqual(len(test_extractor.features), 9)
  334. def test_extract_mocked_transcribe_no_matching_words(self):
  335. """Mock out the actual call to transcribe but match no words in the sentence"""
  336. video_source = TestSourceMedia().one_colour_silent_audio()
  337. test_extractor = extractors.WordFeatureExtractor(input_files=video_source)
  338. # mock _transcribe and mock out model and batched pipeline for speed
  339. test_extractor._transcribe = self.mock_transcribe
  340. test_extractor._model = MagicMock()
  341. test_extractor._batched_model = MagicMock()
  342. # set up and run the extractor
  343. test_extractor.setup(words=["nonexistentword"])
  344. test_extractor.run()
  345. test_extractor.teardown()
  346. self.assertEqual(len(test_extractor.features), 0)
  347. def test_extract_mocked_transcribe_some_matching_words(self):
  348. """Mock out the actual call to transcribe but match some words in the sentence"""
  349. video_source = TestSourceMedia().one_colour_silent_audio()
  350. test_extractor = extractors.WordFeatureExtractor(input_files=video_source)
  351. # mock _transcribe and mock out model and batched pipeline for speed
  352. test_extractor._transcribe = self.mock_transcribe
  353. test_extractor._model = MagicMock()
  354. test_extractor._batched_model = MagicMock()
  355. # set up and run the extractor
  356. test_extractor.setup(words=["quick", "jumps", "dog"])
  357. test_extractor.run()
  358. test_extractor.teardown()
  359. self.assertEqual(len(test_extractor.features), 3)