HOA7 Spatial Field Decoder (hoa64 v0.5.0): 7th-order Ambisonics encode/decode, Wigner-D rotation, DOA analysis, vision fuse, diffusion conditioning
570b87b verified | """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") | |