File size: 3,348 Bytes
18d028b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""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 == 400

    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