t2m-gpt-vsl-code / build_multivsl_lexicon.py
Tri1's picture
T2M-GPT VSL adaptation: Python sources only (82 files, no checkpoints or data)
8e5456b verified
Raw
History Blame Contribute Delete
7.81 kB
#!/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()