LucasLooTan commited on
Commit
0fb3eb2
·
1 Parent(s): 2de9ac2

refactor: extract closed vocabulary into signbridge.vocab module

Browse files

Single source of truth eliminates the drift risk between the VLM
recognizer (string-prompt vocab) and the trained-classifier head (list
order maps to logit indices). Also exports VOCAB_PROMPT_LITERAL for
prompt embedding and VOCAB_SET for membership checks.

Addresses deep-check finding A.F10 / D.F10 (vocab drift across modules).

signbridge/recognizer/classifier.py CHANGED
@@ -14,23 +14,13 @@ from pathlib import Path
14
 
15
  import numpy as np
16
 
 
 
 
 
17
  logger = logging.getLogger(__name__)
18
 
19
- # WLASL Top-50 + ASL fingerspelling alphabet + digits 0-9.
20
- # Exact list will be finalised when we lock training data on Day 2.
21
- VOCABULARY: list[str] = [
22
- # ASL fingerspelling
23
- *list("ABCDEFGHIJKLMNOPQRSTUVWXYZ"),
24
- *list("0123456789"),
25
- # WLASL Top-50 (approximate; exact set fixed by the dataset slice)
26
- "hello", "thank_you", "name", "please", "sorry", "yes", "no", "good",
27
- "bad", "help", "want", "like", "love", "family", "friend", "mother",
28
- "father", "sister", "brother", "child", "home", "school", "work",
29
- "eat", "drink", "water", "food", "more", "finish", "today", "tomorrow",
30
- "yesterday", "where", "what", "who", "why", "when", "how", "go", "come",
31
- "see", "know", "understand", "think", "feel", "happy", "sad", "tired",
32
- "hungry", "wait",
33
- ]
34
  VOCAB_SIZE = len(VOCABULARY)
35
 
36
 
 
14
 
15
  import numpy as np
16
 
17
+ # Vocabulary imported from the shared module — must match the order the
18
+ # trained classifier head was trained against.
19
+ from signbridge.vocab import VOCAB
20
+
21
  logger = logging.getLogger(__name__)
22
 
23
+ VOCABULARY = list(VOCAB)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  VOCAB_SIZE = len(VOCABULARY)
25
 
26
 
signbridge/recognizer/vlm.py CHANGED
@@ -25,28 +25,16 @@ import re
25
 
26
  import numpy as np
27
 
 
 
 
 
 
 
28
  logger = logging.getLogger(__name__)
29
 
30
  DEFAULT_VLM_MODEL = os.getenv("SIGNBRIDGE_VLM_MODEL", "Qwen/Qwen2-VL-7B-Instruct")
31
 
32
- # Closed vocabulary the VLM is asked to choose from. Same shape as
33
- # `classifier.VOCABULARY` but expressed as a prompt, not a softmax.
34
- _VLM_VOCAB = (
35
- "A B C D E F G H I J K L M N O P Q R S T U V W X Y Z "
36
- "0 1 2 3 4 5 6 7 8 9 "
37
- "hello thank_you name please sorry yes no good bad help "
38
- "want like love family friend mother father sister brother child "
39
- "home school work eat drink water food more finish today tomorrow "
40
- "yesterday where what who why when how go come "
41
- "see know understand think feel happy sad tired hungry wait "
42
- "unknown"
43
- )
44
- # Pre-built set for membership tests at recognition time. Tokens not in this
45
- # set get suppressed (confidence 0.0) — VLMs hallucinate strings like
46
- # "letter", "no_sign", "n/a" that would otherwise leak into the demo with a
47
- # fake 0.85 confidence.
48
- _VLM_VOCAB_SET = frozenset(_VLM_VOCAB.split())
49
-
50
  _PROMPT = (
51
  "You are an expert in American Sign Language (ASL). Look at this image of a "
52
  "single signed gesture. Identify which ASL sign or fingerspelled letter is "
 
25
 
26
  import numpy as np
27
 
28
+ # Closed vocabulary the VLM is asked to choose from. Imported from the
29
+ # shared `signbridge.vocab` module so the recognizer and the trained
30
+ # classifier (`signbridge.recognizer.classifier`) can never drift.
31
+ from signbridge.vocab import VOCAB_PROMPT_LITERAL as _VLM_VOCAB
32
+ from signbridge.vocab import VOCAB_SET as _VLM_VOCAB_SET
33
+
34
  logger = logging.getLogger(__name__)
35
 
36
  DEFAULT_VLM_MODEL = os.getenv("SIGNBRIDGE_VLM_MODEL", "Qwen/Qwen2-VL-7B-Instruct")
37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  _PROMPT = (
39
  "You are an expert in American Sign Language (ASL). Look at this image of a "
40
  "single signed gesture. Identify which ASL sign or fingerspelled letter is "
signbridge/vocab.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared closed-vocabulary constants for SignBridge.
2
+
3
+ Single source of truth so the VLM recognizer (`signbridge.recognizer.vlm`)
4
+ and the trained-classifier path (`signbridge.recognizer.classifier`) can
5
+ never drift. The classifier head must produce logits in this exact order;
6
+ the VLM prompt forces the model to choose only from this set.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ ALPHABET: tuple[str, ...] = tuple("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
12
+ DIGITS: tuple[str, ...] = tuple("0123456789")
13
+ WLASL_TOP50: tuple[str, ...] = (
14
+ "hello", "thank_you", "name", "please", "sorry", "yes", "no", "good",
15
+ "bad", "help", "want", "like", "love", "family", "friend", "mother",
16
+ "father", "sister", "brother", "child", "home", "school", "work",
17
+ "eat", "drink", "water", "food", "more", "finish", "today", "tomorrow",
18
+ "yesterday", "where", "what", "who", "why", "when", "how", "go", "come",
19
+ "see", "know", "understand", "think", "feel", "happy", "sad", "tired",
20
+ "hungry", "wait",
21
+ )
22
+
23
+ # Sentinel returned by the VLM when no recognized sign is present.
24
+ UNKNOWN: str = "unknown"
25
+
26
+ VOCAB: tuple[str, ...] = ALPHABET + DIGITS + WLASL_TOP50 + (UNKNOWN,)
27
+ VOCAB_SET: frozenset[str] = frozenset(VOCAB)
28
+
29
+ # Pre-rendered space-separated string for prompt embedding.
30
+ VOCAB_PROMPT_LITERAL: str = " ".join(VOCAB)
tests/test_vocab.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for the shared vocabulary module."""
2
+
3
+ from signbridge.vocab import VOCAB, VOCAB_SET, ALPHABET, DIGITS, WLASL_TOP50
4
+
5
+
6
+ def test_vocab_is_tuple_of_strings():
7
+ assert isinstance(VOCAB, tuple)
8
+ assert all(isinstance(t, str) for t in VOCAB)
9
+ assert len(VOCAB) == len(set(VOCAB)) # no duplicates
10
+
11
+
12
+ def test_vocab_set_matches_tuple():
13
+ assert VOCAB_SET == frozenset(VOCAB)
14
+
15
+
16
+ def test_alphabet_subset():
17
+ assert all(letter in VOCAB_SET for letter in ALPHABET)
18
+ assert len(ALPHABET) == 26
19
+
20
+
21
+ def test_digits_subset():
22
+ assert all(d in VOCAB_SET for d in DIGITS)
23
+ assert len(DIGITS) == 10
24
+
25
+
26
+ def test_wlasl_top50_subset():
27
+ assert all(sign in VOCAB_SET for sign in WLASL_TOP50)
28
+ assert len(WLASL_TOP50) >= 49
29
+
30
+
31
+ def test_unknown_sentinel_present():
32
+ assert "unknown" in VOCAB_SET
33
+
34
+
35
+ def test_recognizer_uses_shared_vocab():
36
+ from signbridge.recognizer.vlm import _VLM_VOCAB_SET
37
+ assert _VLM_VOCAB_SET is VOCAB_SET
38
+
39
+
40
+ def test_classifier_uses_shared_vocab():
41
+ from signbridge.recognizer.classifier import VOCABULARY
42
+ assert tuple(VOCABULARY) == VOCAB