Spaces:
Build error
Build error
File size: 5,319 Bytes
18d028b ca5affd 18d028b ca5affd | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | """Tests for the FastAPI backend."""
from __future__ import annotations
import base64
import io
import numpy as np
import pytest
from fastapi.testclient import TestClient
from PIL import Image
from signbridge.backend import app
@pytest.fixture()
def client() -> TestClient:
return TestClient(app)
def _frame_b64(rgb: tuple[int, int, int] = (128, 128, 128), size: int = 64) -> str:
arr = np.full((size, size, 3), rgb, dtype=np.uint8)
img = Image.fromarray(arr)
buf = io.BytesIO()
img.save(buf, format="JPEG", quality=80)
return base64.b64encode(buf.getvalue()).decode("ascii")
class TestHealth:
def test_healthz_returns_ok(self, client: TestClient) -> None:
r = client.get("/healthz")
assert r.status_code == 200
assert r.json() == {"status": "ok"}
def test_info_returns_provider_block(self, client: TestClient) -> None:
r = client.get("/info")
assert r.status_code == 200
body = r.json()
for key in ("provider", "composer_model", "vlm_model", "tts_model", "recognizer_mode"):
assert key in body
class TestRecognize:
def test_empty_frame_rejected(self, client: TestClient) -> None:
r = client.post("/recognize", json={"frame": ""})
assert r.status_code in (400, 422)
def test_invalid_base64_rejected(self, client: TestClient) -> None:
r = client.post("/recognize", json={"frame": "%%%not-base64%%%"})
assert r.status_code == 400
def test_valid_frame_no_provider_returns_empty_token(self, client: TestClient) -> None:
# Without API keys, the VLM path returns ("", 0.0). The endpoint should
# still respond 200 OK.
r = client.post("/recognize", json={"frame": _frame_b64()})
assert r.status_code == 200
body = r.json()
assert body == {"token": "", "confidence": 0.0}
def test_data_url_prefix_tolerated(self, client: TestClient) -> None:
b64 = _frame_b64()
r = client.post(
"/recognize", json={"frame": f"data:image/jpeg;base64,{b64}"}
)
assert r.status_code == 200
class TestCompose:
def test_empty_signs(self, client: TestClient) -> None:
r = client.post("/compose", json={"signs": []})
assert r.status_code == 200
assert r.json() == {"sentence": ""}
def test_fingerspelled_word(self, client: TestClient) -> None:
r = client.post(
"/compose", json={"signs": ["L", "U", "C", "A", "S"]}
)
assert r.status_code == 200
# Naive joiner produces "Lucas."
assert "Lucas" in r.json()["sentence"]
def test_glosses(self, client: TestClient) -> None:
r = client.post(
"/compose", json={"signs": ["hello", "thank_you"]}
)
assert r.status_code == 200
out = r.json()["sentence"].lower()
assert "hello" in out
assert "thank you" in out
class TestSpeak:
def test_empty_text_rejected(self, client: TestClient) -> None:
r = client.post("/speak", json={"text": ""})
assert r.status_code == 400
def test_returns_audio(self, client: TestClient) -> None:
r = client.post("/speak", json={"text": "hello"})
assert r.status_code == 200
assert r.headers["content-type"].startswith("audio/")
assert len(r.content) > 0
class TestRecognizeFrames:
def test_empty_frames_rejected(self, client: TestClient) -> None:
r = client.post("/recognize", json={"frames": []})
# Pydantic min_length=1 rejects empty list (validation error 422).
# If the validator collapses to 400 due to model_validator, accept that too.
assert r.status_code in (400, 422)
def test_single_frame_in_list_rejected(self, client: TestClient) -> None:
# Multi-frame path requires >=2 frames.
b64 = _frame_b64()
r = client.post("/recognize", json={"frames": [b64]})
assert r.status_code == 400
detail = r.json().get("detail", "").lower()
assert "at least 2" in detail or "2 frames" in detail or "frames" in detail
def test_valid_multi_frame_no_provider(self, client: TestClient) -> None:
b64 = _frame_b64()
r = client.post("/recognize", json={"frames": [b64, b64, b64, b64]})
assert r.status_code == 200
assert r.json() == {"token": "", "confidence": 0.0}
def test_too_many_frames_rejected(self, client: TestClient) -> None:
b64 = _frame_b64()
r = client.post("/recognize", json={"frames": [b64] * 100})
assert r.status_code in (400, 422)
def test_oversized_frame_rejected(self, client: TestClient) -> None:
# 6 MB base64 string is well past any reasonable webcam frame.
big = "A" * (6 * 1024 * 1024)
r = client.post("/recognize", json={"frame": big})
assert r.status_code in (400, 422)
def test_both_frame_and_frames_rejected(self, client: TestClient) -> None:
b64 = _frame_b64()
r = client.post(
"/recognize",
json={"frame": b64, "frames": [b64, b64]},
)
assert r.status_code in (400, 422)
def test_neither_frame_nor_frames_rejected(self, client: TestClient) -> None:
r = client.post("/recognize", json={})
assert r.status_code in (400, 422)
|