File size: 7,809 Bytes
8e5456b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
172
173
174
175
176
177
178
179
#!/usr/bin/env python3
"""Resolve Multi-VSL (WACV 2025) class ids to Vietnamese glosses, front view only.

Inputs
  Multi-VSL_WACV_2025/data/1_1000_label.numbers      id (1..1000) -> Vietnamese word
  Multi-VSL_WACV_2025/data/label_1_1000/*.csv        filename -> class label (0..999)
  WACV-Data-SLR/Data.zip                             87,817 mp4 (labels join 84,764)

Verified alignment: `label + 1 == numbers id` (checked against the word range each
recording session encodes in its filename: 0.057% violations, vs 0.339% for the
1-based alternative).

Outputs (in Multi-VSL_WACV_2025/data/)
  glosses_1_1000.csv        label, id, word, url            -- the resolved lexicon
  front_{split}.csv         name, label, word               -- front (center) view only
  lexicon_coverage.json     overlap with the Full_TriVis gloss vocabulary

Note on views: Multi-VSL names its frontal view `center`; there is no `left`/`right`
ambiguity to resolve, so "front view only" == the `*_center_ord1.csv` splits.

Note on the matching unit: a Full_TriVis gloss is `|`-separated, and each item is ONE
sign that may span several Vietnamese words ("ông bà", "bãi cỏ"). So coverage is
computed per pipe-separated ITEM, not per whitespace token.
"""
import argparse
import csv
import json
import os
import re
import unicodedata
from collections import Counter

REPO = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..')
MV = os.path.join(REPO, 'Multi-VSL_WACV_2025', 'data')
LAB = os.path.join(MV, 'label_1_1000')


def norm(s):
    """Normalize a gloss for matching: NFC, lowercase, drop parentheticals/punctuation."""
    s = unicodedata.normalize('NFC', str(s)).lower().strip()
    s = re.sub(r'\([^)]*\)', ' ', s)          # 'mét (m)' -> 'mét', 'hay là (hoặc là)' -> 'hay là'
    s = re.sub(r'[^\w\s]', ' ', s, flags=re.UNICODE)
    return re.sub(r'\s+', ' ', s).strip()


GRAMMAR_NOTES = {'đại từ', 'giới từ', 'động từ', 'danh từ', 'tính từ', 'trạng từ',
                 '2 người', '3 người', 'số nhiều', 'số ít'}


def variants(word):
    """All surface forms a Multi-VSL entry can match.

    Entries are richly annotated, and each convention needs handling:
      'cùng / với (giới từ)'      -> 'cùng', 'với'      ('/' = alternatives)
      'một ít/ một chút'          -> 'một ít', 'một chút'
      'hay là (hoặc là)'          -> 'hay là', 'hoặc là' (paren = alternative)
      'họ (2 người) (đại từ)'     -> 'họ'                (paren = grammar note, dropped)
      'có … không?'               -> 'có không'          (ellipsis = slot)
    """
    w = unicodedata.normalize('NFC', str(word)).lower()
    out = set()

    # parenthetical content is either an alternative form or a grammatical note
    for alt in re.findall(r'\(([^)]*)\)', w):
        a = norm(alt)
        if a and a not in GRAMMAR_NOTES and not a.isdigit():
            out.add(a)

    base = re.sub(r'\([^)]*\)', ' ', w)          # strip all parentheticals
    for piece in re.split(r'[/;,]', base):       # '/' etc. separate alternatives
        n = norm(piece)
        if n:
            out.add(n)

    out.add(norm(base))
    return {v for v in out if v}


def load_numbers(path):
    from numbers_parser import Document
    rows = Document(path).sheets[0].tables[0].rows(values_only=True)
    hdr = [str(c) for c in rows[0]]
    out = {}
    for r in rows[1:]:
        if r[0] is None:
            continue
        out[int(r[0])] = {'word': str(r[1]).strip(),
                          'url': str(r[2]).strip() if len(r) > 2 and r[2] else ''}
    return out, hdr


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('--numbers', default=os.path.join(MV, '1_1000_label.numbers'))
    ap.add_argument('--offset', type=int, default=1, help='numbers id = csv label + offset')
    ap.add_argument('--trivis-index', default='./dataset/VSL_upper/train_index.json')
    ap.add_argument('--trivis-csv', default=os.path.join(REPO, 'Full_TriVis', 'split_lab_front.csv'))
    args = ap.parse_args()

    words, hdr = load_numbers(args.numbers)
    print(f'numbers table columns: {hdr}')
    print(f'ids {min(words)}..{max(words)}  ({len(words)} present)')
    missing = [i for i in range(1, 1001) if i not in words]
    if missing:
        print(f'!! ids with no word: {missing}  -> those classes stay unlabelled')

    # ---------------- resolved lexicon ----------------
    out_lex = os.path.join(MV, 'glosses_1_1000.csv')
    with open(out_lex, 'w', newline='', encoding='utf-8') as f:
        w = csv.writer(f)
        w.writerow(['label', 'numbers_id', 'word', 'url'])
        for lab in range(1000):
            e = words.get(lab + args.offset)
            w.writerow([lab, lab + args.offset, e['word'] if e else '', e['url'] if e else ''])
    print(f'wrote {out_lex}')

    label2word = {lab: words[lab + args.offset]['word']
                  for lab in range(1000) if lab + args.offset in words}

    # ---------------- front-view-only splits ----------------
    counts = {}
    for split in ('train', 'val', 'test'):
        src = os.path.join(LAB, f'{split}_1_1000_center_ord1.csv')
        rows = list(csv.DictReader(open(src)))
        dst = os.path.join(MV, f'front_{split}.csv')
        with open(dst, 'w', newline='', encoding='utf-8') as f:
            w = csv.writer(f)
            w.writerow(['name', 'label', 'word'])
            for r in rows:
                lab = int(r['label'])
                w.writerow([r['name'], lab, label2word.get(lab, '')])
        counts[split] = len(rows)
        print(f'wrote {dst}  ({len(rows)} front-view clips)')

    # ---------------- coverage against Full_TriVis ----------------
    # matching unit = pipe-separated gloss item (one sign), not whitespace token
    tri_items = Counter()
    with open(args.trivis_csv, newline='', encoding='utf-8') as f:
        for r in csv.DictReader(f):
            for it in str(r['Sign_sentence']).split('|'):
                n = norm(it)
                if n:
                    tri_items[n] += 1

    mv_index = {}
    for lab, wd in label2word.items():
        for v in variants(wd):
            mv_index.setdefault(v, lab)

    covered = {k: v for k, v in tri_items.items() if k in mv_index}
    tot_types, tot_tokens = len(tri_items), sum(tri_items.values())
    cov_types, cov_tokens = len(covered), sum(covered.values())

    print(f'\n--- Full_TriVis gloss coverage by the Multi-VSL 1000-word lexicon ---')
    print(f'Full_TriVis distinct gloss items (signs): {tot_types}')
    print(f'  covered by lexicon: {cov_types}  ({100*cov_types/tot_types:.1f}% of types)')
    print(f'Full_TriVis total gloss occurrences: {tot_tokens}')
    print(f'  covered: {cov_tokens}  ({100*cov_tokens/tot_tokens:.1f}% of tokens)')
    print(f'Multi-VSL words matched at least once: '
          f'{len(set(mv_index[k] for k in covered))}/1000')

    print('\ntop covered signs:', [w for w, _ in Counter(covered).most_common(12)])
    unc = Counter({k: v for k, v in tri_items.items() if k not in mv_index})
    print('top UNcovered signs:', [w for w, _ in unc.most_common(12)])

    res = {'offset': args.offset, 'front_view_counts': counts,
           'lexicon_words': len(label2word), 'unlabelled_ids': missing,
           'trivis_gloss_types': tot_types, 'covered_types': cov_types,
           'type_coverage_pct': round(100 * cov_types / tot_types, 2),
           'trivis_gloss_tokens': tot_tokens, 'covered_tokens': cov_tokens,
           'token_coverage_pct': round(100 * cov_tokens / tot_tokens, 2)}
    with open(os.path.join(MV, 'lexicon_coverage.json'), 'w') as f:
        json.dump(res, f, indent=2, ensure_ascii=False)
    print(f"\nwrote {os.path.join(MV, 'lexicon_coverage.json')}")


if __name__ == '__main__':
    main()