File size: 3,021 Bytes
570b87b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Phase 3 vision + Qwythos API smoke tests."""

from __future__ import annotations

import json
import sys
import threading
import time
from pathlib import Path

import numpy as np

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

from hoa64.analysis import angular_error_deg
from hoa64.server import TOOL_SCHEMA, handle_analyze
from hoa64.vision import encode_boxes_to_hoa, fuse_reports, report_from_boxes


def test_vision_point_doa():
    rep = report_from_boxes(
        [{"az": 45.0, "el": -10.0, "kind": "point", "weight": 1.0, "label": "p"}],
        max_order=3,
    )
    err = angular_error_deg(45.0, -10.0, rep.doa_az_deg, rep.doa_el_deg)
    # intensity on static field works; peak should be tight
    perr = angular_error_deg(45.0, -10.0, rep.peak_az_deg, rep.peak_el_deg)
    assert perr < 8.0, f"peak err={perr} doa=({rep.doa_az_deg},{rep.doa_el_deg})"
    assert rep.kind == "spatial_vision"
    assert rep.sources_hint[0]["label"] == "p"


def test_vision_box_lobe_near_center():
    hoa = encode_boxes_to_hoa(
        [{"az": 0.0, "el": 0.0, "w_deg": 15, "h_deg": 15, "weight": 1.0}],
        max_order=3,
        n_azi=48,
        n_el=24,
    )
    assert hoa.shape[0] == 64
    assert np.linalg.norm(hoa) > 0
    # W channel should dominate for broad front lobe
    assert abs(hoa[0]) > 0


def test_fuse_agreement():
    audio = {"doa_az_deg": 10.0, "doa_el_deg": 0.0, "energy": 1.0}
    vision = {"doa_az_deg": 12.0, "doa_el_deg": 1.0, "energy": 0.8}
    f = fuse_reports(audio, vision)
    assert f["agreement"] is True
    assert f["angular_separation_deg"] < 15
    assert "one_liner" in f


def test_fuse_disagreement():
    audio = {"doa_az_deg": 0.0, "doa_el_deg": 0.0, "energy": 1.0}
    vision = {"doa_az_deg": 90.0, "doa_el_deg": 0.0, "energy": 1.0}
    f = fuse_reports(audio, vision)
    assert f["agreement"] is False
    assert f["angular_separation_deg"] > 80


def test_handle_analyze_vision_and_demo():
    d = handle_analyze({"mode": "demo_scene", "order": 2})
    assert d["schema"].startswith("spatial-hoa")
    assert "one_liner" in d
    v = handle_analyze(
        {
            "mode": "vision",
            "order": 3,
            "boxes": [{"az": -30, "el": 5, "kind": "ray", "label": "x"}],
        }
    )
    assert v["kind"] == "spatial_vision"
    err = angular_error_deg(-30, 5, v["peak_az_deg"], v["peak_el_deg"])
    assert err < 10.0


def test_tool_schema_shape():
    assert TOOL_SCHEMA["type"] == "function"
    assert TOOL_SCHEMA["function"]["name"] == "spatial_analyze"


if __name__ == "__main__":
    test_vision_point_doa()
    print("OK test_vision_point_doa")
    test_vision_box_lobe_near_center()
    print("OK test_vision_box_lobe_near_center")
    test_fuse_agreement()
    print("OK test_fuse_agreement")
    test_fuse_disagreement()
    print("OK test_fuse_disagreement")
    test_handle_analyze_vision_and_demo()
    print("OK test_handle_analyze_vision_and_demo")
    test_tool_schema_shape()
    print("OK test_tool_schema_shape")