File size: 5,550 Bytes
24f95f0
5f91e0b
 
 
 
 
 
 
 
 
24f95f0
 
 
 
 
 
5f91e0b
24f95f0
 
 
 
5f91e0b
 
 
 
 
 
 
24f95f0
5f91e0b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24f95f0
 
 
5f91e0b
 
 
 
 
 
 
 
24f95f0
5f91e0b
 
 
 
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
"""
Dissonance Engine β€” Unit / Integration Tests.

Tests the engine at three levels:
  1. Pure logic: VAD projection and cosine divergence (no models needed)
  2. API shape:  dissonance_engine.analyze() returns expected keys (no models)
  3. Calibration: refine_with_dataset() triggers CMU-MOSEI + CREMA-D lookup

Run from repo root:
    cd backend && python3 tests/test_dissonance.py
"""

import logging
import sys
import os

sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("test_dissonance")


# ---------------------------------------------------------------------------
# Phase 1: Pure logic β€” VAD projection + cosine divergence (no models needed)
# ---------------------------------------------------------------------------
def test_vad_projection():
    print("\n─── Phase 1: VAD Projection Logic ───")
    import numpy as np
    import scipy.spatial.distance as dist
    from app.services.dissonance_engine import _probs_to_vad, EMOTION_TO_VAD

    # Simulate: audio says "angry", text says "joy" β†’ should be highly dissonant
    audio_probs = {"angry": 0.8, "neutral": 0.1, "happy": 0.1}
    text_probs  = {"joy": 0.85, "love": 0.1, "surprise": 0.05}

    audio_vad = _probs_to_vad(audio_probs)
    text_vad  = _probs_to_vad(text_probs)

    score = float(dist.cosine(audio_vad, text_vad))
    print(f"  audio_vad  = {audio_vad.round(3)}")
    print(f"  text_vad   = {text_vad.round(3)}")
    print(f"  cosine div = {score:.4f}")

    assert score > 0.08, f"Expected meaningful divergence for angry vs joy, got {score:.4f}"
    print("  βœ… Meaningful divergence confirmed for angry-audio vs joy-text")

    # Same emotion β†’ near-zero divergence
    audio_probs2 = {"happy": 0.9, "neutral": 0.1}
    text_probs2  = {"joy": 0.9, "surprise": 0.1}
    audio_vad2   = _probs_to_vad(audio_probs2)
    text_vad2    = _probs_to_vad(text_probs2)
    score2 = float(dist.cosine(audio_vad2, text_vad2))
    print(f"  happy-vs-joy cosine div = {score2:.4f}")
    assert score2 < 0.05, f"Expected near-zero divergence for happy vs joy, got {score2:.4f}"
    print("  βœ… Near-zero divergence confirmed for happy-audio vs joy-text")


# ---------------------------------------------------------------------------
# Phase 2: Engine init β€” no models, check env config and threshold
# ---------------------------------------------------------------------------
def test_engine_init():
    print("\n─── Phase 2: Engine Config & Threshold ───")
    from app.services.dissonance_engine import dissonance_engine

    print(f"  Initial threshold     = {dissonance_engine.dissonance_threshold}")
    print(f"  Audio temperature T   = {dissonance_engine.audio_temperature}")
    print(f"  Fusion weights        = ({dissonance_engine.w_audio_text}, "
          f"{dissonance_engine.w_prosody}, {dissonance_engine.w_text_conf})")

    assert dissonance_engine.dissonance_threshold == 0.5
    assert dissonance_engine.audio_temperature == 1.5
    assert abs(dissonance_engine.w_audio_text + dissonance_engine.w_prosody
               + dissonance_engine.w_text_conf - 1.0) < 0.01, \
        "Fusion weights should sum to ~1.0"
    print("  βœ… Config OK")


# ---------------------------------------------------------------------------
# Phase 3: Dataset calibration (network call β€” may be skipped in CI)
# ---------------------------------------------------------------------------
def test_engine_calibration(skip_network: bool = False):
    print("\n─── Phase 3: Dataset Calibration (CMU-MOSEI + CREMA-D) ───")
    from app.services.dissonance_engine import dissonance_engine

    before = dissonance_engine.dissonance_threshold

    if skip_network:
        print("  ⚠️  Skipping network call (skip_network=True)")
        return

    dissonance_engine.refine_with_dataset()
    after = dissonance_engine.dissonance_threshold

    print(f"  Before: {before}  β†’  After: {after}")
    # If datasets were reachable, threshold should drop to 0.35
    # If not reachable, it stays at its current value β€” both are fine
    print("  βœ… Calibration completed (result depends on network access)")


# ---------------------------------------------------------------------------
# Phase 4: MMSA engine singleton check
# ---------------------------------------------------------------------------
def test_mmsa_wiring():
    print("\n─── Phase 4: MMSA ↔ DissonanceEngine Wiring ───")
    from app.services.mmsa_engine import mmsa_engine
    from app.services.dissonance_engine import dissonance_engine

    # Verify mmsa_engine holds a reference to the same singleton
    import app.services.mmsa_engine as mmsa_mod
    assert hasattr(mmsa_mod, "dissonance_engine"), \
        "mmsa_engine module must import dissonance_engine"
    assert mmsa_mod.dissonance_engine is dissonance_engine, \
        "mmsa_engine must use the same dissonance_engine singleton"
    print("  βœ… mmsa_engine is wired to dissonance_engine singleton")


if __name__ == "__main__":
    try:
        test_vad_projection()
        test_engine_init()
        test_engine_calibration(skip_network="--skip-network" in sys.argv)
        test_mmsa_wiring()
        print("\nβœ… All dissonance tests passed.")
    except AssertionError as e:
        print(f"\n❌ Assertion failed: {e}")
        sys.exit(1)
    except Exception as e:
        import traceback
        print(f"\n❌ Test error: {e}")
        traceback.print_exc()
        sys.exit(1)