File size: 5,861 Bytes
7330a0e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
from __future__ import annotations

import json

from bridgelink_asl.clip_dataset import load_clip_dataset
from bridgelink_asl.asl_types import GestureWindow, SentenceEvent
from bridgelink_asl.wrapper import LocalQwen25VlmInterpreter, run_wrapper


def test_compare_mode_logs_cnn_and_vlm_predictions(tmp_path) -> None:
    manifest_path = tmp_path / "clips.jsonl"
    output_path = tmp_path / "comparison-results.jsonl"
    manifest_path.write_text(
        json.dumps(
            {
                "clip_id": "team_hello_want_drink_001",
                "split": "test",
                "source": "team",
                "gloss": ["HELLO", "WANT", "DRINK"],
                "english": "Hello, I want a drink.",
            }
        )
        + "\n",
        encoding="utf-8",
    )

    summary = run_wrapper(manifest_path, mode="compare", output_path=output_path)
    rows = [json.loads(line) for line in output_path.read_text(encoding="utf-8").splitlines() if line.strip()]

    assert summary.records_processed == 1
    assert summary.failures == 0
    assert rows[0]["clip_id"] == "team_hello_want_drink_001"
    assert rows[0]["cnn_prediction"]["model_mode"] == "cnn"
    assert rows[0]["vlm_prediction"]["model_mode"] == "vlm"
    assert "want" in rows[0]["cnn_prediction"]["sentence"].lower()
    assert rows[0]["vlm_prediction"]["sentence"] == "Hello, I want a drink."
    assert isinstance(rows[0]["cnn_latency_ms"], float)
    assert isinstance(rows[0]["vlm_latency_ms"], float)


def test_vlm_mode_falls_back_to_gloss_when_confidence_is_low(tmp_path) -> None:
    manifest_path = tmp_path / "clips.jsonl"
    output_path = tmp_path / "comparison-results.jsonl"
    manifest_path.write_text(
        json.dumps(
            {
                "clip_id": "team_no_stop_001",
                "split": "test",
                "source": "team",
                "gloss": ["NO", "STOP"],
                "english": "No, stop.",
            }
        )
        + "\n",
        encoding="utf-8",
    )

    class LowConfidenceInterpreter:
        def interpret(self, window):
            return SentenceEvent(
                gloss=tuple(token.label for token in window.token_trace),
                sentence="I am not sure.",
                confidence=0.2,
                model_mode="vlm",
            )

    summary = run_wrapper(
        manifest_path,
        mode="vlm",
        output_path=output_path,
        interpreter=LowConfidenceInterpreter(),
        vlm_confidence_floor=0.6,
    )
    row = json.loads(output_path.read_text(encoding="utf-8").strip())

    assert summary.failures == 1
    assert row["cnn_prediction"] is None
    assert row["vlm_prediction"]["sentence"] == "No stop."
    assert row["vlm_prediction"]["needs_clarification"] is True
    assert any("gloss fallback" in note for note in row["failure_notes"])


def test_cnn_mode_only_writes_cnn_fields(tmp_path) -> None:
    manifest_path = tmp_path / "clips.jsonl"
    output_path = tmp_path / "comparison-results.jsonl"
    manifest_path.write_text(
        json.dumps(
            {
                "clip_id": "team_please_help_001",
                "split": "test",
                "source": "team",
                "gloss": ["PLEASE", "HELP"],
                "english": "Please help.",
            }
        )
        + "\n",
        encoding="utf-8",
    )

    run_wrapper(manifest_path, mode="cnn", output_path=output_path)
    row = json.loads(output_path.read_text(encoding="utf-8").strip())

    assert row["cnn_prediction"] is not None
    assert row["vlm_prediction"] is None
    assert isinstance(row["token_trace"], list)
    assert row["failure_notes"] == []


def test_load_clip_dataset_accepts_hybrid_eval_rows_and_resolves_local_clip(tmp_path) -> None:
    clips_dir = tmp_path / "clips"
    clips_dir.mkdir()
    local_clip = clips_dir / "12320_computer.mp4"
    local_clip.write_bytes(b"fake")
    manifest_path = tmp_path / "hybrid.jsonl"
    manifest_path.write_text(
        json.dumps(
            {
                "candidate_model": "landmark_cnn",
                "video_id": "12320",
                "true_label": "computer",
                "video_path": "/content/drive/MyDrive/BridgeLink-ASL/vlm_eval_wlasl25_cnn/clips/12320_computer.mp4",
                "cnn_top1": "computer",
                "cnn_top5": [
                    {"label": "computer", "confidence": 0.19},
                    {"label": "snow", "confidence": 0.08},
                ],
                "vlm_prompt": "Choose the best label from the list only.",
            }
        )
        + "\n",
        encoding="utf-8",
    )

    records = load_clip_dataset(manifest_path)

    assert len(records) == 1
    assert records[0].clip_id == "12320"
    assert records[0].split == "test"
    assert records[0].gloss == ("COMPUTER",)
    assert records[0].candidate_labels == ("COMPUTER", "SNOW")
    assert records[0].video_path == local_clip.resolve()


def test_local_qwen_interpreter_parses_json_response_without_real_model() -> None:
    class FakeInterpreter(LocalQwen25VlmInterpreter):
        def _generate_response_text(self, messages):
            return json.dumps(
                {
                    "gloss": ["computer"],
                    "sentence": "Computer.",
                    "confidence": 0.88,
                    "needs_clarification": False,
                }
            )

    interpreter = FakeInterpreter(model_id="Qwen/Qwen2.5-VL-7B-Instruct")
    event = interpreter.interpret(
        GestureWindow(
            clip_id="demo",
            sampled_frames=(),
            token_trace=(),
            video_path=None,
            candidate_labels=("COMPUTER", "SNOW"),
        )
    )

    assert event.gloss == ("COMPUTER",)
    assert event.sentence == "Computer."
    assert event.model_mode == "vlm"
    assert event.confidence == 0.88