You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

305 lines
12 KiB

  1. """test_feature_extractors_functional.py -- functional tests for feature extractors
  2. This module contains functional tests for FEs using crafted and/or generated media files
  3. to verify that the FEs are working as expected:
  4. - laughter detection -- uses videos with laughs at known times
  5. - video activity -- uses videos with visual activity at known times
  6. - audio loudness -- uses videos with audio at known times
  7. etc.
  8. These tests are marked slow to avoid running them during normal test runs.
  9. """
  10. import pytest
  11. import unittest
  12. import pipeline.feature_extractors as extractors
  13. import test.mocks as mocks
  14. class FEFunctionalTest(unittest.TestCase):
  15. """FEFunctionalTest -- base class for functional tests for feature extractors
  16. """
  17. SAMPLE_DIR = "/home/robert/code/softdev2023-24/summerproject/highlights/test/sample_videos"
  18. @pytest.mark.slow
  19. @pytest.mark.veryslow
  20. class TestLaughterFEFunctional(FEFunctionalTest):
  21. """TestLaughterFEFunctional -- functional tests for laughter detection feature extractor"""
  22. def test_laughter_detection_positive(self):
  23. """Test laughter detection feature extractor
  24. Uses:
  25. - sample_videos/sample-manual-audio-laughs-video-colours.mp4
  26. :: laughters at 15-20s
  27. -- pass iff laughter features extracted in this range, *but*
  28. NOTE: LaughFE subtracts from start time to capture what preceded the laughter
  29. so we need to subtract this time (and adds a little after too)
  30. FE 'exposes' these as _PREPEND_TIME and _APPEND_TIME
  31. Note: takes 8-10s to run for this 30s video using GTX 970. As such this test can be skipped with either:
  32. "-m 'not veryslow'" or "-m 'not slow'"
  33. """
  34. SAMPLE_VIDEO = f"{self.SAMPLE_DIR}/sample-manual-audio-laughs-video-colours.mp4"
  35. START_TIME = 15
  36. END_TIME = 20
  37. # create mock source with the video
  38. source = mocks.MockSource(path=SAMPLE_VIDEO)
  39. # create the feature extractor
  40. testfe = extractors.LaughterFeatureExtractor(input_files=[source])
  41. testfe.setup()
  42. testfe.run()
  43. testfe.teardown()
  44. # check if the feature was extracted:
  45. self.assertTrue(testfe.features)
  46. # check if the feature interval is within the expected range
  47. self.assertTrue(testfe.features[0].interval.start >= (START_TIME - testfe._PREPEND_TIME))
  48. self.assertTrue(testfe.features[0].interval.end <= (END_TIME + testfe._APPEND_TIME))
  49. def test_laughter_detection_negative(self):
  50. """Negative test for laughter detection feature extractor -- should not detect laughter in a silent video
  51. Uses:
  52. - sample_videos/sample-manual-audio-blank-video-colours.mp4
  53. :: silent video (30s)
  54. -- pass iff no laughter features
  55. """
  56. SAMPLE_VIDEO = f"{self.SAMPLE_DIR}/sample-manual-audio-blank-video-colours.mp4"
  57. # create mock source with the video
  58. source = mocks.MockSource(path=SAMPLE_VIDEO)
  59. # create the feature extractor
  60. testfe = extractors.LaughterFeatureExtractor(input_files=[source])
  61. testfe.setup()
  62. testfe.run()
  63. testfe.teardown()
  64. # check if the feature was extracted:
  65. self.assertFalse(testfe.features)
  66. class TestVideoActivityFEFunctional(FEFunctionalTest):
  67. """TestVisualActivityFEFunctional -- functional tests for visual activity feature extractor
  68. """
  69. def test_visual_activity_functional(self):
  70. """Test visual activity feature extractor
  71. use:
  72. - sample_videos/sample-manual-visualactivity.mp4 :: activity at 15-20s -- pass if activity detected anywhere in this range
  73. """
  74. SAMPLE_VIDEO = f"{self.SAMPLE_DIR}/sample-manual-visualactivity.mp4"
  75. START_TIME = 15
  76. END_TIME = 20
  77. # create mock source with the video
  78. source = mocks.MockSource(path=SAMPLE_VIDEO)
  79. # create the feature extractor
  80. testfe = extractors.VideoActivityFeatureExtractor(input_files=[source])
  81. testfe.setup()
  82. testfe.run()
  83. testfe.teardown()
  84. # check if the feature was extracted:
  85. self.assertTrue(testfe.features)
  86. # check if the feature interval is within the expected range
  87. self.assertTrue(testfe.features[0].interval.start >= START_TIME)
  88. class TestLoudAudioFEFunctional(FEFunctionalTest):
  89. """TestAudioLoudnessFEFunctional -- functional tests for audio loudness feature extractor
  90. """
  91. def test_audio_loudness_functional_one_feature(self):
  92. """Test audio loudness feature extractor
  93. use:
  94. - sample_videos/sample-manual-audio.mp4 :: audio at 15-20s -- pass if audio detected anywhere in this range
  95. -- peak at 16s - 18s, verify this is highest scoring
  96. """
  97. SAMPLE_VIDEO = f"{self.SAMPLE_DIR}/sample-manual-audio.mp4"
  98. START_TIME = 15
  99. END_TIME = 20
  100. PEAK_START = 16
  101. PEAK_END = 18
  102. # create mock source with the video
  103. source = mocks.MockSource(path=SAMPLE_VIDEO)
  104. # create the feature extractor
  105. testfe = extractors.LoudAudioFeatureExtractor(input_files=[source])
  106. testfe.setup()
  107. testfe.run()
  108. testfe.teardown()
  109. # check if the feature was extracted:
  110. self.assertTrue(testfe.features)
  111. # check if the feature interval is within the expected range
  112. self.assertTrue(testfe.features[0].interval.start >= START_TIME)
  113. # get sorted list of features based on feature.score
  114. sorted_features = sorted(testfe.features, key=lambda x: x.score, reverse=True)
  115. # check if the highest scoring feature is within the peak range
  116. self.assertTrue(sorted_features[0].interval.start >= PEAK_START)
  117. def test_audio_loudness_functional_no_features(self):
  118. """Test audio loudness feature extractor using a silent video. This should produce no features
  119. since "-inf" results from pyloudnorm are filtered out by the FE.
  120. Use:
  121. - sample_videos/sample-manual-audio-blank-video-colours.mp4
  122. :: silent video (30s)
  123. -- pass if no features extracted
  124. """
  125. SAMPLE_VIDEO = f"{self.SAMPLE_DIR}/sample-manual-audio-blank-video-colours.mp4"
  126. # create mock source with the video
  127. source = mocks.MockSource(path=SAMPLE_VIDEO)
  128. # create the feature extractor
  129. testfe = extractors.LoudAudioFeatureExtractor(input_files=[source])
  130. testfe.setup()
  131. testfe.run()
  132. testfe.teardown()
  133. # check if the feature was extracted:
  134. self.assertFalse(testfe.features)
  135. class TestWordFEFunctional(FEFunctionalTest):
  136. """TestWordFEFunctional -- functional tests for word detection feature extractor (uses Whisper)"""
  137. @pytest.mark.slow
  138. @pytest.mark.veryslow
  139. def test_audio_word_detection_harvard1_functional(self):
  140. """Test audio word detection feature extractor
  141. Uses:
  142. - sample-manual-audio-harvardsentences-video-colours.mp4
  143. :: Harvard sentences (list 1) up to item 1.8 ("The birch canoe... The hogs were fed")
  144. -- pass if words detected from this set
  145. """
  146. SAMPLE_VIDEO = f"{self.SAMPLE_DIR}/sample-manual-audio-harvardsentences-video-colours.mp4"
  147. DETECT_WORDS = ["birch", "smooth", "chicken", "depth",
  148. "juice", "lemons", "box", "thrown", "beside",
  149. "hogs", "fed"]
  150. # create mock source with the video
  151. source = mocks.MockSource(path=SAMPLE_VIDEO)
  152. # create the feature extractor
  153. testfe = extractors.WordFeatureExtractor(input_files=[source])
  154. testfe.setup(words=DETECT_WORDS)
  155. testfe.run()
  156. testfe.teardown()
  157. self.assertGreaterEqual(len(testfe.features), len(DETECT_WORDS))
  158. @pytest.mark.slow
  159. @pytest.mark.veryslow
  160. def test_audio_word_detection_harvard1_rdh_functional(self):
  161. """Test audio word detection feature extractor
  162. Uses:
  163. - sample-manual-audio-harvardsentences-rdh-video-colours.mp4
  164. :: Harvard sentences (list 1) up to item 1.8 ("The birch canoe... The hogs were fed") read by RDH
  165. -- pass if words detected from this set
  166. """
  167. SAMPLE_VIDEO = f"{self.SAMPLE_DIR}/sample-manual-audio-harvardsentences-rdh-video-colours.mp4"
  168. DETECT_WORDS = ["birch", "smooth", "chicken", "depth",
  169. "juice", "lemons", "box", "thrown", "beside",
  170. "hogs", "fed"] # missing "truck", "glue", "well", "punch" due to problems
  171. # create mock source with the video
  172. source = mocks.MockSource(path=SAMPLE_VIDEO)
  173. # create the feature extractor
  174. testfe = extractors.WordFeatureExtractor(input_files=[source])
  175. testfe.setup(words=DETECT_WORDS)
  176. testfe.run()
  177. testfe.teardown()
  178. self.assertGreaterEqual(len(testfe.features), len(DETECT_WORDS))
  179. def test_audio_word_detection_harvard_gluewellpunchtruck_rdh_functional(self):
  180. """Test audio word detection feature extractor
  181. Uses:
  182. - sample-manual-audio-harvardsentences-rdh2-video-colours.mp4
  183. :: only the words "glue", "well", "punch", "truck" are read by RDH
  184. """
  185. SAMPLE_VIDEO = f"{self.SAMPLE_DIR}/sample-manual-audio-harvardsentences-rdh2-video-colours.mp4"
  186. DETECT_WORDS = ["glue", "well", "punch", "truck"]
  187. # create mock source with the video
  188. source = mocks.MockSource(path=SAMPLE_VIDEO)
  189. # create the feature extractor
  190. testfe = extractors.WordFeatureExtractor(input_files=[source])
  191. testfe.setup(words=DETECT_WORDS)
  192. testfe.run()
  193. testfe.teardown()
  194. # check if the word was feature extracted:
  195. self.assertGreaterEqual(len(testfe.features), 4)
  196. def test_audio_word_detection_noaudio_nofeatures(self):
  197. """Test audio word detection feature extractor
  198. Uses:
  199. - sample-manual-audio-blank-video-colours.mp4
  200. :: silent video (30s)
  201. -- pass if no features extracted
  202. """
  203. SAMPLE_VIDEO = f"{self.SAMPLE_DIR}/sample-manual-audio-blank-video-colours.mp4"
  204. DETECT_WORDS = ["birch", "smooth", "chicken", "depth",
  205. "juice", "lemons", "box", "thrown", "beside",
  206. "hogs", "fed"]
  207. # create mock source with the video
  208. source = mocks.MockSource(path=SAMPLE_VIDEO)
  209. # create the feature extractor
  210. testfe = extractors.WordFeatureExtractor(input_files=[source])
  211. testfe.setup(words=DETECT_WORDS)
  212. # ensure no features extracted from blank audio:
  213. # self.assertEqual(len(testfe.features), 0)
  214. # Actually, Whisper throws a hissy fit if there's no audio:
  215. # RuntimeError: stack expects a non-empty TensorList
  216. # stdout: "No active speech found in audio"
  217. # TODO: consider catching this error in the FE
  218. with self.assertRaises(RuntimeError):
  219. testfe.run()
  220. def test_audio_word_detection_harvard_no_matching_words(self):
  221. """Test audio word detection feature extractor
  222. Uses:
  223. - sample-manual-audio-harvardsentences-video-colours.mp4
  224. :: Harvard sentences (list 1) up to item 1.8 ("The birch canoe... The hogs were fed")
  225. plus the sentence: "Portez ce vieux whisky au juge blond qui fume"
  226. -- pass if no features extracted
  227. """
  228. SAMPLE_VIDEO = f"{self.SAMPLE_DIR}/sample-manual-audio-harvardsentences-video-colours.mp4"
  229. DETECT_WORDS = "Portez ce vieux whisky au juge blond qui fume".split()
  230. # create mock source with the video
  231. source = mocks.MockSource(path=SAMPLE_VIDEO)
  232. # create the feature extractor
  233. testfe = extractors.WordFeatureExtractor(input_files=[source])
  234. testfe.setup(words=DETECT_WORDS)
  235. testfe.run()
  236. testfe.teardown()
  237. # ensure no Features:
  238. self.assertEqual(len(testfe.features), 0)
  239. if __name__ == "__main__":
  240. unittest.main()