Skip to content

Commit cc92769

Browse files
authored
v0.5.1, add support for reading xlsx files… (#180)
* chore: update version to 0.5.1, add support for reading xlsx files, and include Motorola datasets * strip \r from outputs; add wmt24pp-rm dataset - Strip \r from parser outputs (read_plain, read_tsv) and echo command. Closes #173 - Add ZurichNLP/wmt24pp-rm: 6 Romansh varieties (deu-roh) test sets. Closes #178 * add --strict flag for langpair ordering in mtdata list. Closes #157 * add scb-mt-en-th-2020 (12 subsets) and TALPCo (8 lang pairs). Closes #151. Closes #152 - VistecAI scb-mt-en-th-2020: 12 CSV subsets with ~1M eng-tha sentence pairs - TALPCo: jpn paired with eng, kor, mya, zsm, ind, jav, tha, vie - Fix parser: TSV with 2 files now zips (pairs) instead of flattening - Fix parser: single-col TSV yields scalar string for proper pairing * fix CSV parser and SMOL HF datasets - Add read_csv() using Python csv module for proper quoted-field handling. Fixes VistecAI scb-mt column misalignment - Support data_files in HF loader for datasets with unregistered configs (SMOL smolsent/smoldoc en_lij) - Restore Google-smol_sent and Google-smol_doc entries for eng-lij_Latn
1 parent b33427c commit cc92769

11 files changed

Lines changed: 197 additions & 24 deletions

File tree

mtdata/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# Created: 4/4/20
55

66

7-
__version__ = '0.5.0'
7+
__version__ = '0.5.1'
88
__description__ = 'mtdata is a tool to download datasets for machine translation'
99
__author__ = 'Thamme Gowda'
1010

mtdata/cache.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def get_entry(self, entry: Entry, fix_missing=True) -> Union[Path, List[Path]]:
4545
else:
4646
assert isinstance(entry.url, str)
4747
local = self.get_local_path(entry.url, filename=entry.filename, fix_missing=fix_missing, entry=entry)
48-
if isinstance(local, Path) and (zipfile.is_zipfile(local) or tarfile.is_tarfile(local)):
48+
if isinstance(local, Path) and entry.is_archive and (zipfile.is_zipfile(local) or tarfile.is_tarfile(local)):
4949
# look inside the archives and get the desired files
5050
local = self.get_local_in_paths(path=local, entry=entry)
5151
return local
@@ -174,6 +174,10 @@ def get_hf_dataset(self, url: str, entry=None):
174174
streaming=False,
175175
trust_remote_code=False,
176176
)
177+
data_files = entry.meta.get("data_files", None)
178+
if data_files:
179+
args['data_files'] = data_files
180+
args.pop('name', None) # data_files and name are mutually exclusive
177181
log.debug(f"Loading dataset {hf_id} with args: {args}")
178182
ds = load_dataset(hf_id, **args)
179183
if split is None and hasattr(ds, 'keys'):
@@ -261,7 +265,13 @@ def download(self, url: str, save_at: Path, timeout=(5, 10), entry=None):
261265
if valid_flag.exists() and save_at.exists():
262266
return save_at
263267
log.debug(f"GET {url}{save_at}")
264-
resp = requests.get(url=url, allow_redirects=True, headers=headers, stream=True, timeout=timeout)
268+
try:
269+
resp = requests.get(url=url, allow_redirects=True, headers=headers, stream=True,
270+
timeout=timeout)
271+
except requests.exceptions.SSLError as e:
272+
log.warning(f"SSL verification failed for {url}: {e}; retrying without verification")
273+
resp = requests.get(url=url, allow_redirects=True, headers=headers, stream=True,
274+
timeout=timeout, verify=False)
265275
assert resp.status_code == 200, resp.status_code
266276
buf_size = 2 ** 14
267277
tot_bytes = int(resp.headers.get('Content-Length', '0'))

mtdata/index/__init__.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -202,17 +202,21 @@ def is_compatible(lang1: Union[str, BCP47Tag], lang2: Union[str, BCP47Tag]):
202202
return lang1.is_compatible(lang2)
203203

204204

205-
def bitext_lang_match(pair1, pair2, fuzzy_match=False) -> bool:
206-
x1, y1 = sorted(pair1)
207-
x2, y2 = sorted(pair2)
205+
def bitext_lang_match(pair1, pair2, fuzzy_match=False, strict=False) -> bool:
206+
if strict:
207+
x1, y1 = pair1
208+
x2, y2 = pair2
209+
else:
210+
x1, y1 = sorted(pair1)
211+
x2, y2 = sorted(pair2)
208212
if fuzzy_match:
209213
return is_compatible(x1, x2) and is_compatible(y1, y2)
210214
else:
211215
return x1 == x2 and y1 == y2
212216

213217

214218
def get_entries(langs=None, names=None, not_names=None, fuzzy_match=False,
215-
groups=None, not_groups=None) -> List[Entry]:
219+
groups=None, not_groups=None, strict=False) -> List[Entry]:
216220
"""
217221
:param langs: language pairs to select eg ('en', 'de')
218222
:param names: names to select
@@ -236,7 +240,7 @@ def get_entries(langs=None, names=None, not_names=None, fuzzy_match=False,
236240
if langs:
237241
if len(langs) == 2:
238242
select = [e for e in select if 2 == len(e.did.langs)\
239-
and bitext_lang_match(langs, e.did.langs, fuzzy_match=fuzzy_match)]
243+
and bitext_lang_match(langs, e.did.langs, fuzzy_match=fuzzy_match, strict=strict)]
240244
else: # monolingual
241245
assert len(langs) == 1
242246
lang: BCP47Tag = langs[0]

mtdata/index/huggingface.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ def _load_hf_dataset(index, data):
9898
else:
9999
meta = dict(orig_id=hf_id, config=config_name, split=split, fields=fields)
100100

101+
if 'data_files' in config:
102+
meta['data_files'] = config['data_files']
103+
101104
data_id = DatasetId(group=group, name=name, version='1', langs=langs)
102105
if data_id in index:
103106
log.warning(f"Duplicate dataset id: {data_id}")

mtdata/index/other.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,3 +164,60 @@ def load_all(index: Index):
164164
("2", "*/mono-bho-corpus/monolingual-v0.2.bho")]:
165165
index += Entry(did=DatasetId(group='BHLTR', name=f'mono', version=version, langs=('bho',)),
166166
url=url, filename=filename, ext='zip', in_ext='txt', in_paths=[f1], cite=cite)
167+
168+
### Motorola Endangered Indigenous Languages ###
169+
_moto_base = "https://wpa_supplier.motorola.com/"
170+
_moto_datasets = [
171+
# (filename, src_lang, tgt_lang, version-date)
172+
("MotorolaMobility_All-en_US-lld_IT-2024-06-04.xlsx", "eng", "lld", "20240604"),
173+
("MotorolaMobility_All-en_US-chr-2022-03-08.xlsx", "eng", "chr", "20220308"),
174+
("MotorolaMobility_All-en_US-xnr_IN-2023-02-10.xlsx", "eng", "xnr", "20230210"),
175+
("MotorolaMobility_All-en_US-mi_NZ-2023-08-08.xlsx", "eng", "mri", "20230808"),
176+
("MotorolaMobility-en_US-yrl-2022-12-08.xlsx", "eng", "yrl", "20221208"),
177+
("MotorolaMobility-en_US-kgp-2022-12-08.xlsx", "eng", "kgp", "20221208"),
178+
("MotorolaMobility_All-pt_BR-yrl_BR-2022-07-20.xlsx", "por", "yrl", "20220720"),
179+
("MotorolaMobility_All-pt_BR-kgp_BR-2022-07-20.xlsx", "por", "kgp", "20220720"),
180+
]
181+
for fname, src, tgt, version in _moto_datasets:
182+
url = _moto_base + fname
183+
index += Entry(did=DatasetId(group='Motorola', name='lang_revitalization', version=version,
184+
langs=(src, tgt)),
185+
url=url, filename=fname, ext='xlsx', in_ext='xlsx')
186+
187+
### scb-mt-en-th-2020: Thai-English parallel corpus ###
188+
_scb_url = "https://github.com/vistec-AI/dataset-releases/releases/download/scb-mt-en-th-2020_v1.0/scb-mt-en-th-2020.zip"
189+
_scb_filename = "scb-mt-en-th-2020.zip"
190+
_scb_cite = ("lowphansirikul2021scb",)
191+
_scb_subsets = [
192+
"task_master_1", "generated_reviews_translator", "nus_sms",
193+
"msr_paraphrase", "mozilla_common_voice", "generated_reviews_crowd",
194+
"generated_reviews_yn", "assorted_government", "thai_websites",
195+
"paracrawl", "wikipedia", "apdf",
196+
]
197+
for subset in _scb_subsets:
198+
in_path = f"scb-mt-en-th-2020/{subset}.csv"
199+
index += Entry(did=DatasetId(group='VistecAI', name=f'scb_mt_{subset}', version='1',
200+
langs=('eng', 'tha')),
201+
url=_scb_url, filename=_scb_filename, ext='zip', in_ext='csvwithheader',
202+
in_paths=[in_path], cite=_scb_cite)
203+
204+
### TALPCo: TUFS Asian Language Parallel Corpus ###
205+
_talpco_base = "https://raw.githubusercontent.com/matbahasa/TALPCo/master"
206+
_talpco_cite = ("nomoto2018talpco",)
207+
_talpco_langs = [
208+
("eng", "eng"),
209+
("kor", "kor"),
210+
("mya", "myn"), # Burmese; file uses myn
211+
("zsm", "zsm"), # Malay
212+
("ind", "ind"), # Indonesian
213+
("jav", "jav"), # Javanese
214+
("tha", "tha"),
215+
("vie", "vie"),
216+
]
217+
for iso, file_code in _talpco_langs:
218+
src_url = f"{_talpco_base}/jpn/data_jpn.txt"
219+
tgt_url = f"{_talpco_base}/{file_code}/data_{file_code}.txt"
220+
index += Entry(did=DatasetId(group='TALPCo', name='talpco', version='1',
221+
langs=('jpn', iso)),
222+
url=[src_url, tgt_url], ext='txt', in_ext='tsv', cols=(1,),
223+
cite=_talpco_cite)

mtdata/main.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@
2020
DEF_N_JOBS = 1
2121

2222

23-
def list_data(langs, names, not_names=None, full=False, groups=None, not_groups=None, id_only=False):
23+
def list_data(langs, names, not_names=None, full=False, groups=None, not_groups=None, id_only=False, strict=False):
2424
from mtdata.index import get_entries
25-
entries = get_entries(langs, names, not_names, groups=groups, not_groups=not_groups, fuzzy_match=True)
25+
entries = get_entries(langs, names, not_names, groups=groups, not_groups=not_groups, fuzzy_match=True, strict=strict)
2626
for i, ent in enumerate(entries):
2727
if id_only:
2828
print(ent.did)
@@ -82,7 +82,7 @@ def echo_data(did:DatasetId, delim='\t'):
8282
for col in row:
8383
if isinstance(col, Mapping):
8484
col = json.dumps(col, indent=None, ensure_ascii=False)
85-
col = col.replace(delim, ' ').replace('\n', ' ')
85+
col = col.replace(delim, ' ').replace('\n', ' ').replace('\r', ' ')
8686
rec.append(col)
8787
rec = delim.join(rec)
8888
else:
@@ -281,6 +281,8 @@ def parse_args():
281281
list_p.add_argument('-nn', '--not-names', metavar='NAME', nargs='*', help='Exclude these names')
282282
list_p.add_argument('-g', '--groups', metavar='GROUP', nargs='*', help='Only select datasets from these groups')
283283
list_p.add_argument('-ng', '--not-groups', metavar='GROUP', nargs='*', help='Exclude these groups')
284+
list_p.add_argument('-s', '--strict', action='store_true', default=False,
285+
help='Strict langpair ordering: eng-deu and deu-eng are treated differently')
284286
list_p.add_argument('-f', '--full', action='store_true', help='Show Full Citation')
285287
list_p.add_argument('-o', '--out', type=Path, help='This arg is ignored. Only used in "get" subcommand,'
286288
' but added here for convenience of switching b/w get and list')
@@ -386,7 +388,8 @@ def main():
386388
index_datasets()
387389
elif args.task == 'list':
388390
list_data(args.langs, args.names, not_names=args.not_names, full=args.full,
389-
groups=args.groups, not_groups=args.not_groups, id_only=args.id)
391+
groups=args.groups, not_groups=args.not_groups, id_only=args.id,
392+
strict=args.strict)
390393
elif args.task == 'get':
391394
get_data(**vars(args))
392395
elif args.task == 'echo':

mtdata/parser.py

Lines changed: 74 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# Author: Thamme Gowda [tg (at) isi (dot) edu]
44
# Created: 4/4/20
55

6+
import csv
67
from typing import Optional, Union, Tuple, List
78
from dataclasses import dataclass
89
from pathlib import Path
@@ -77,7 +78,7 @@ def read_segs(self, show_pbar=True):
7778
cols = self.ent.cols
7879
readers.append(self.read_tsv(p, cols=cols, meta_fields=meta_fields))
7980
elif 'csvwithheader' in self.ext:
80-
readers.append(self.read_tsv(p, delim=',', skipheader=True, meta_fields=meta_fields))
81+
readers.append(self.read_csv(p, meta_fields=meta_fields))
8182
elif 'raw' in self.ext or 'txt' in self.ext:
8283
readers.append(self.read_plain(p))
8384
elif 'tmx' in self.ext:
@@ -91,13 +92,15 @@ def read_segs(self, show_pbar=True):
9192
readers.append(read_wmt21_xml(p))
9293
elif HF_EXT in self.ext:
9394
readers.append(self.read_hfds(p))
95+
elif 'xlsx' in self.ext:
96+
readers.append(self.read_xlsx(p))
9497
else:
9598
raise Exception(f'Not supported {self.ext} : {p}')
9699

97100
if len(readers) == 1:
98101
data = readers[0]
99-
elif self.ext == 'tmx' or self.ext == 'tsv':
100-
data = (rec for reader in readers for rec in reader) # flatten all readers
102+
elif self.ext == 'tmx' or (self.ext == 'tsv' and len(self.paths) == 1):
103+
data = (rec for reader in readers for rec in reader) # flatten readers from single source
101104
elif len(readers) == 2:
102105
def _zip_n_check():
103106
for row in zip_longest(*readers):
@@ -120,7 +123,7 @@ def read_plain(self, path):
120123
try:
121124
with IO.reader(path) as stream:
122125
for line in stream:
123-
yield line.strip()
126+
yield line.rstrip('\r\n')
124127
except:
125128
log.warning(f'Error reading file {path}')
126129
raise
@@ -138,10 +141,12 @@ def read_tsv(self, path, delim='\t', cols=None, skipheader=False, meta_fields=No
138141
if skipheader:
139142
line = stream.readline()
140143
for line in stream:
141-
row = [x.strip() for x in line.rstrip('\n').split(delim)]
144+
row = [x.strip() for x in line.rstrip('\r\n').split(delim)]
142145
out_row = row
143146
if cols:
144147
out_row = [row[idx] for idx in cols]
148+
if len(cols) == 1:
149+
out_row = out_row[0] # unwrap single-column to scalar
145150
if meta_fields:
146151
metadata = {}
147152
for key, idx in meta_fields.items():
@@ -152,6 +157,51 @@ def read_tsv(self, path, delim='\t', cols=None, skipheader=False, meta_fields=No
152157
out_row.append(metadata)
153158
yield out_row
154159

160+
def read_csv(self, path, cols=None, meta_fields=None):
161+
"""Read data from a CSV file with header using Python's csv module.
162+
Handles quoted fields with embedded commas/newlines correctly.
163+
:param path: path to CSV file
164+
:param cols: column indices to extract; default is (0, 1)
165+
"""
166+
if cols is None:
167+
cols = self.ent.cols if (self.ent and self.ent.cols) else (0, 1)
168+
with IO.reader(path) as stream:
169+
reader = csv.reader(stream)
170+
header = next(reader, None) # skip header
171+
for row in reader:
172+
if not row or len(row) <= max(cols):
173+
continue
174+
out_row = [row[c].strip() for c in cols]
175+
if meta_fields:
176+
metadata = {}
177+
for key, idx in meta_fields.items():
178+
if key in ("source", "target") or idx >= len(row) or row[idx] in ("", None):
179+
continue
180+
metadata[key] = row[idx]
181+
if metadata:
182+
out_row.append(metadata)
183+
yield out_row
184+
185+
def read_xlsx(self, path, cols=None):
186+
"""Read data from an Excel .xlsx file.
187+
:param path: path to .xlsx file
188+
:param cols: column indices to extract; default uses ent.cols or (0, 1)
189+
"""
190+
try:
191+
from openpyxl import load_workbook
192+
except ImportError as e:
193+
raise ImportError("openpyxl is required to read .xlsx files. Run: pip install openpyxl") from e
194+
if cols is None:
195+
cols = self.ent.cols if (self.ent and self.ent.cols) else (0, 1)
196+
wb = load_workbook(path, read_only=True, data_only=True)
197+
ws = wb.active
198+
for row in ws.iter_rows(min_row=2, values_only=True): # skip header
199+
out = [str(row[c]).strip() if row[c] is not None else '' for c in cols]
200+
if all(v == '' for v in out):
201+
continue
202+
yield out
203+
wb.close()
204+
155205
@staticmethod
156206
def _nested_get(row, field):
157207
"""Get a value from a dict using dot-separated path for nested access.
@@ -176,11 +226,24 @@ def read_hfds(self, ds):
176226
# in the current version, I am going to retain all fields to see what all fields exist,
177227
# and map the subset of fields as per the dict; so, created rev_map.get(orig,orig)
178228
for row in ds:
179-
out_row = [self._nested_get(row, src_field)]
180-
if tgt_field is not None:
181-
out_row.append(self._nested_get(row, tgt_field))
182-
# remap meta fields if necessary
229+
src_val = self._nested_get(row, src_field)
230+
tgt_val = self._nested_get(row, tgt_field) if tgt_field else None
183231
top_keys = {f.split('.')[0] for f in [src_field] + ([tgt_field] if tgt_field else [])}
184232
metadata = {rev_map.get(k, k): v for k, v in row.items() if k not in top_keys}
185-
out_row.append(metadata)
186-
yield out_row
233+
234+
src_is_list = isinstance(src_val, list)
235+
tgt_is_list = isinstance(tgt_val, list)
236+
if src_is_list and tgt_is_list:
237+
# Both lists (e.g. SmolDoc srcs/trgs): zip and yield each pair
238+
for s, t in zip(src_val, tgt_val):
239+
yield [s, t, metadata]
240+
elif not src_is_list and tgt_is_list:
241+
# Source is scalar, target is list (e.g. GATITOS src/trgs): expand
242+
for t in tgt_val:
243+
yield [src_val, t, metadata]
244+
else:
245+
out_row = [src_val]
246+
if tgt_val is not None:
247+
out_row.append(tgt_val)
248+
out_row.append(metadata)
249+
yield out_row

mtdata/resource/huggingface-datasets.jsonl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,7 @@
1212
{"id": "sfrontull/autonomia-lld_valbadia-ita", "configs": [{"name": null, "langs": ["ita", "lld"], "fields": {"source": "translation.ita", "target": "translation.lld_valbadia"}}]}
1313
{"id": "sfrontull/stiftungsparkasse-lld_valbadia-ita", "configs": [{"name": null, "langs": ["ita", "lld"], "fields": {"source": "translation.ita", "target": "translation.lld_valbadia"}}]}
1414
{"id": "sfrontull/pinocchio-lld_valbadia-ita", "configs": [{"name": null, "langs": ["ita", "lld"], "fields": {"source": "translation.ita", "target": "translation.lld_valbadia"}}]}
15+
# === SMOL: Professional translations for low-resource languages (google/smol) ===
16+
{"id": "google/smol", "configs": [{"name": "smolsent__en_lij", "ds_name": "smol_sent", "langs": ["eng", "lij_Latn"], "fields": {"source": "src", "target": "trg"}, "data_files": "smolsent/en_lij.jsonl"}, {"name": "smoldoc__en_lij", "ds_name": "smol_doc", "langs": ["eng", "lij_Latn"], "fields": {"source": "srcs", "target": "trgs"}, "data_files": "smoldoc/en_lij.jsonl"}, {"name": "gatitos__en_lij", "ds_name": "smol_gatitos", "langs": ["eng", "lij_Latn"], "fields": {"source": "src", "target": "trgs"}}]}
17+
# === WMT24++ Romansh (ZurichNLP/wmt24pp-rm) ===
18+
{"id": "ZurichNLP/wmt24pp-rm", "fields": {"source": "source", "target": "target", "doc_id": "document_id", "domain": "domain", "seg_id": "segment_id"}, "configs": [{"name": "de_DE-rm-rumgr", "split": "test", "langs": ["deu", "roh"], "ds_name": "wmt24pp_rm_rumgr"}, {"name": "de_DE-rm-sursilv", "split": "test", "langs": ["deu", "roh"], "ds_name": "wmt24pp_rm_sursilv"}, {"name": "de_DE-rm-sutsilv", "split": "test", "langs": ["deu", "roh"], "ds_name": "wmt24pp_rm_sutsilv"}, {"name": "de_DE-rm-surmiran", "split": "test", "langs": ["deu", "roh"], "ds_name": "wmt24pp_rm_surmiran"}, {"name": "de_DE-rm-puter", "split": "test", "langs": ["deu", "roh"], "ds_name": "wmt24pp_rm_puter"}, {"name": "de_DE-rm-vallader", "split": "test", "langs": ["deu", "roh"], "ds_name": "wmt24pp_rm_vallader"}]}

mtdata/resource/refs.bib

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,4 +726,27 @@ @article{ojha2019english
726726
author={Ojha, Atul Kr},
727727
journal={arXiv preprint arXiv:1905.02239},
728728
year={2019}
729+
}
730+
731+
@inproceedings{vamvas2025expanding,
732+
title={Expanding the {WMT}24++ Benchmark with {R}umantsch {G}rischun, {S}ursilvan, {S}utsilvan, {S}urmiran, {P}uter, and {V}allader},
733+
author={Vamvas, Jannis and P{\'e}rez Prat, Ignacio and Soliva, Not and Baltermia-Guetg, Sandra and Beeli, Andrina and Beeli, Simona and Capeder, Madlaina and Decurtins, Laura and Gregori, Gian Peder and Hobi, Flavia and Holderegger, Gabriela and Lazzarini, Arina and Lazzarini, Viviana and Rosselli, Walter and Vital, Bettina and Rutkiewicz, Anna and Sennrich, Rico},
734+
booktitle={Proceedings of the Tenth Conference on Machine Translation},
735+
year={2025},
736+
url={https://aclanthology.org/2025.wmt-1.79/}
737+
}
738+
739+
@article{lowphansirikul2021scb,
740+
title={scb-mt-en-th-2020: A Large English-Thai Parallel Corpus},
741+
author={Lowphansirikul, Lalita and Polpanumas, Charin and Jantrakulchai, Nawat and Nutanong, Sarana},
742+
journal={arXiv preprint arXiv:2007.03541},
743+
year={2020}
744+
}
745+
746+
@inproceedings{nomoto2018talpco,
747+
title={{TUFS} {A}sian {L}anguage {P}arallel {C}orpus ({TALPCo})},
748+
author={Nomoto, Hiroki and Okano, Kenji and Moeljadi, David and Sawada, Hideo},
749+
booktitle={Proceedings of the Twenty-Fourth Annual Meeting of the Association for Natural Language Processing},
750+
pages={436--439},
751+
year={2018}
729752
}

0 commit comments

Comments
 (0)