test_main.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. import json
  2. import pickle
  3. from io import BytesIO
  4. from pathlib import Path
  5. from typing import Any, Callable
  6. from unittest import mock
  7. import cv2
  8. import numpy as np
  9. import pytest
  10. from fastapi.testclient import TestClient
  11. from PIL import Image
  12. from pytest_mock import MockerFixture
  13. from .config import settings
  14. from .models.base import PicklableSessionOptions
  15. from .models.cache import ModelCache
  16. from .models.clip import OpenCLIPEncoder
  17. from .models.facial_recognition import FaceRecognizer
  18. from .models.image_classification import ImageClassifier
  19. from .schemas import ModelType
  20. class TestImageClassifier:
  21. classifier_preds = [
  22. {"label": "that's an image alright", "score": 0.8},
  23. {"label": "well it ends with .jpg", "score": 0.1},
  24. {"label": "idk, im just seeing bytes", "score": 0.05},
  25. {"label": "not sure", "score": 0.04},
  26. {"label": "probably a virus", "score": 0.01},
  27. ]
  28. def test_min_score(self, pil_image: Image.Image, mocker: MockerFixture) -> None:
  29. mocker.patch.object(ImageClassifier, "load")
  30. classifier = ImageClassifier("test_model_name", min_score=0.0)
  31. assert classifier.min_score == 0.0
  32. classifier.model = mock.Mock()
  33. classifier.model.return_value = self.classifier_preds
  34. all_labels = classifier.predict(pil_image)
  35. classifier.min_score = 0.5
  36. filtered_labels = classifier.predict(pil_image)
  37. assert all_labels == [
  38. "that's an image alright",
  39. "well it ends with .jpg",
  40. "idk",
  41. "im just seeing bytes",
  42. "not sure",
  43. "probably a virus",
  44. ]
  45. assert filtered_labels == ["that's an image alright"]
  46. class TestCLIP:
  47. embedding = np.random.rand(512).astype(np.float32)
  48. cache_dir = Path("test_cache")
  49. def test_basic_image(
  50. self,
  51. pil_image: Image.Image,
  52. mocker: MockerFixture,
  53. clip_model_cfg: dict[str, Any],
  54. clip_preprocess_cfg: Callable[[Path], dict[str, Any]],
  55. ) -> None:
  56. mocker.patch.object(OpenCLIPEncoder, "download")
  57. mocker.patch.object(OpenCLIPEncoder, "model_cfg", clip_model_cfg)
  58. mocker.patch.object(OpenCLIPEncoder, "preprocess_cfg", clip_preprocess_cfg)
  59. mocker.patch("app.models.clip.AutoTokenizer.from_pretrained", autospec=True)
  60. mocked = mocker.patch("app.models.clip.ort.InferenceSession", autospec=True)
  61. mocked.return_value.run.return_value = [[self.embedding]]
  62. clip_encoder = OpenCLIPEncoder("ViT-B-32::openai", cache_dir="test_cache", mode="vision")
  63. embedding = clip_encoder.predict(pil_image)
  64. assert clip_encoder.mode == "vision"
  65. assert isinstance(embedding, list)
  66. assert len(embedding) == clip_model_cfg["embed_dim"]
  67. assert all([isinstance(num, float) for num in embedding])
  68. clip_encoder.vision_model.run.assert_called_once()
  69. def test_basic_text(
  70. self,
  71. mocker: MockerFixture,
  72. clip_model_cfg: dict[str, Any],
  73. clip_preprocess_cfg: Callable[[Path], dict[str, Any]],
  74. ) -> None:
  75. mocker.patch.object(OpenCLIPEncoder, "download")
  76. mocker.patch.object(OpenCLIPEncoder, "model_cfg", clip_model_cfg)
  77. mocker.patch.object(OpenCLIPEncoder, "preprocess_cfg", clip_preprocess_cfg)
  78. mocker.patch("app.models.clip.AutoTokenizer.from_pretrained", autospec=True)
  79. mocked = mocker.patch("app.models.clip.ort.InferenceSession", autospec=True)
  80. mocked.return_value.run.return_value = [[self.embedding]]
  81. clip_encoder = OpenCLIPEncoder("ViT-B-32::openai", cache_dir="test_cache", mode="text")
  82. embedding = clip_encoder.predict("test search query")
  83. assert clip_encoder.mode == "text"
  84. assert isinstance(embedding, list)
  85. assert len(embedding) == clip_model_cfg["embed_dim"]
  86. assert all([isinstance(num, float) for num in embedding])
  87. clip_encoder.text_model.run.assert_called_once()
  88. class TestFaceRecognition:
  89. def test_set_min_score(self, mocker: MockerFixture) -> None:
  90. mocker.patch.object(FaceRecognizer, "load")
  91. face_recognizer = FaceRecognizer("test_model_name", cache_dir="test_cache", min_score=0.5)
  92. assert face_recognizer.min_score == 0.5
  93. def test_basic(self, cv_image: cv2.Mat, mocker: MockerFixture) -> None:
  94. mocker.patch.object(FaceRecognizer, "load")
  95. face_recognizer = FaceRecognizer("test_model_name", min_score=0.0, cache_dir="test_cache")
  96. det_model = mock.Mock()
  97. num_faces = 2
  98. bbox = np.random.rand(num_faces, 4).astype(np.float32)
  99. score = np.array([[0.67]] * num_faces).astype(np.float32)
  100. kpss = np.random.rand(num_faces, 5, 2).astype(np.float32)
  101. det_model.detect.return_value = (np.concatenate([bbox, score], axis=-1), kpss)
  102. face_recognizer.det_model = det_model
  103. rec_model = mock.Mock()
  104. embedding = np.random.rand(num_faces, 512).astype(np.float32)
  105. rec_model.get_feat.return_value = embedding
  106. face_recognizer.rec_model = rec_model
  107. faces = face_recognizer.predict(cv_image)
  108. assert len(faces) == num_faces
  109. for face in faces:
  110. assert face["imageHeight"] == 800
  111. assert face["imageWidth"] == 600
  112. assert isinstance(face["embedding"], list)
  113. assert len(face["embedding"]) == 512
  114. assert all([isinstance(num, float) for num in face["embedding"]])
  115. det_model.detect.assert_called_once()
  116. assert rec_model.get_feat.call_count == num_faces
  117. @pytest.mark.asyncio
  118. class TestCache:
  119. async def test_caches(self, mock_get_model: mock.Mock) -> None:
  120. model_cache = ModelCache()
  121. await model_cache.get("test_model_name", ModelType.IMAGE_CLASSIFICATION)
  122. await model_cache.get("test_model_name", ModelType.IMAGE_CLASSIFICATION)
  123. assert len(model_cache.cache._cache) == 1
  124. mock_get_model.assert_called_once()
  125. async def test_kwargs_used(self, mock_get_model: mock.Mock) -> None:
  126. model_cache = ModelCache()
  127. await model_cache.get("test_model_name", ModelType.IMAGE_CLASSIFICATION, cache_dir="test_cache")
  128. mock_get_model.assert_called_once_with(
  129. ModelType.IMAGE_CLASSIFICATION, "test_model_name", cache_dir="test_cache"
  130. )
  131. async def test_different_clip(self, mock_get_model: mock.Mock) -> None:
  132. model_cache = ModelCache()
  133. await model_cache.get("test_image_model_name", ModelType.CLIP)
  134. await model_cache.get("test_text_model_name", ModelType.CLIP)
  135. mock_get_model.assert_has_calls(
  136. [
  137. mock.call(ModelType.CLIP, "test_image_model_name"),
  138. mock.call(ModelType.CLIP, "test_text_model_name"),
  139. ]
  140. )
  141. assert len(model_cache.cache._cache) == 2
  142. @mock.patch("app.models.cache.OptimisticLock", autospec=True)
  143. async def test_model_ttl(self, mock_lock_cls: mock.Mock, mock_get_model: mock.Mock) -> None:
  144. model_cache = ModelCache(ttl=100)
  145. await model_cache.get("test_model_name", ModelType.IMAGE_CLASSIFICATION)
  146. mock_lock_cls.return_value.__aenter__.return_value.cas.assert_called_with(mock.ANY, ttl=100)
  147. @mock.patch("app.models.cache.SimpleMemoryCache.expire")
  148. async def test_revalidate(self, mock_cache_expire: mock.Mock, mock_get_model: mock.Mock) -> None:
  149. model_cache = ModelCache(ttl=100, revalidate=True)
  150. await model_cache.get("test_model_name", ModelType.IMAGE_CLASSIFICATION)
  151. await model_cache.get("test_model_name", ModelType.IMAGE_CLASSIFICATION)
  152. mock_cache_expire.assert_called_once_with(mock.ANY, 100)
  153. @pytest.mark.skipif(
  154. not settings.test_full,
  155. reason="More time-consuming since it deploys the app and loads models.",
  156. )
  157. class TestEndpoints:
  158. def test_tagging_endpoint(
  159. self, pil_image: Image.Image, responses: dict[str, Any], deployed_app: TestClient
  160. ) -> None:
  161. byte_image = BytesIO()
  162. pil_image.save(byte_image, format="jpeg")
  163. response = deployed_app.post(
  164. "http://localhost:3003/predict",
  165. data={
  166. "modelName": "microsoft/resnet-50",
  167. "modelType": "image-classification",
  168. "options": json.dumps({"minScore": 0.0}),
  169. },
  170. files={"image": byte_image.getvalue()},
  171. )
  172. assert response.status_code == 200
  173. assert response.json() == responses["image-classification"]
  174. def test_clip_image_endpoint(
  175. self, pil_image: Image.Image, responses: dict[str, Any], deployed_app: TestClient
  176. ) -> None:
  177. byte_image = BytesIO()
  178. pil_image.save(byte_image, format="jpeg")
  179. response = deployed_app.post(
  180. "http://localhost:3003/predict",
  181. data={"modelName": "ViT-B-32::openai", "modelType": "clip", "options": json.dumps({"mode": "vision"})},
  182. files={"image": byte_image.getvalue()},
  183. )
  184. assert response.status_code == 200
  185. assert response.json() == responses["clip"]["image"]
  186. def test_clip_text_endpoint(self, responses: dict[str, Any], deployed_app: TestClient) -> None:
  187. response = deployed_app.post(
  188. "http://localhost:3003/predict",
  189. data={
  190. "modelName": "ViT-B-32::openai",
  191. "modelType": "clip",
  192. "text": "test search query",
  193. "options": json.dumps({"mode": "text"}),
  194. },
  195. )
  196. assert response.status_code == 200
  197. assert response.json() == responses["clip"]["text"]
  198. def test_face_endpoint(self, pil_image: Image.Image, responses: dict[str, Any], deployed_app: TestClient) -> None:
  199. byte_image = BytesIO()
  200. pil_image.save(byte_image, format="jpeg")
  201. response = deployed_app.post(
  202. "http://localhost:3003/predict",
  203. data={
  204. "modelName": "buffalo_l",
  205. "modelType": "facial-recognition",
  206. "options": json.dumps({"minScore": 0.034}),
  207. },
  208. files={"image": byte_image.getvalue()},
  209. )
  210. assert response.status_code == 200
  211. assert response.json() == responses["facial-recognition"]
  212. def test_sess_options() -> None:
  213. sess_options = PicklableSessionOptions()
  214. sess_options.intra_op_num_threads = 1
  215. sess_options.inter_op_num_threads = 1
  216. pickled = pickle.dumps(sess_options)
  217. unpickled = pickle.loads(pickled)
  218. assert unpickled.intra_op_num_threads == 1
  219. assert unpickled.inter_op_num_threads == 1