"""Clean LCCC-base (Weibo dialogues) into per-turn chat text for the Chinese corpus. LCCC ships character-tokenised turns ("我 饿 了 。"); spaces are removed except between two Latin/digit tokens. Each turn becomes one chat sequence, as in the Japanese chat data (BOS + turn + EOS); the LCCC train/valid/test split is kept, so splits stay dialogue-disjoint. Per turn: Traditional -> Simplified (tw2sp, same threshold as stage A), then the stage-A term list (configs/zh-filter-terms.toml): a hit drops the turn, a lone weak term sends it to review.jsonl. Turns with fewer than MIN_HAN Chinese characters are dropped. Train turns of HELDOUT_MIN_CHARS or more that also occur in valid/test are removed (short stock replies such as 哈哈 or 好的 stay). A train turn seen n times is kept ceil(sqrt(n)) times (its first occurrences): repeats are compressed but their frequency order, which the IME prior needs, survives. Writes /{train,validation,test}.jsonl, review.jsonl and manifest.json. """ import argparse import collections import hashlib import json import math from pathlib import Path import re import sys ROOT=Path(__file__).resolve().parents[2] sys.path.insert(0,str(ROOT/'src')) from vimeml.corpus.zh_filter import (HAN, Thresholds, counts_as_hit, load_rules, term_hits, # noqa: E402 to_simplified, traditional_ratio) SPLITS={'train':'train','valid':'validation','test':'test'} ALNUM=re.compile(r'^[A-Za-z0-9]+$') MIN_HAN=2 HELDOUT_MIN_CHARS=10 # shorter turns (哈哈, 谢谢, 好的) are not leakage; keep them in train def detok(s): out=[] for t in s.split(' '): if not t: continue if out and ALNUM.match(t) and ALNUM.match(out[-1][-1]): out.append(' ') out.append(t) return ''.join(out) def sha256(path): h=hashlib.sha256() with open(path,'rb') as f: for block in iter(lambda:f.read(1<<24),b''): h.update(block) return h.hexdigest() def clean_turn(raw,rules,th): text=detok(raw).strip() han=HAN.findall(text) script='hans' if traditional_ratio(han)>th.traditional: text=to_simplified(text); script='hant' if len(han)=HELDOUT_MIN_CHARS and r['text'] in heldout: counts['train']['heldout_overlap']+=1; continue rows.append(r) freq=collections.Counter(r['text'] for r in rows); emitted=collections.Counter(); train=[] for r in rows: if emitted[r['text']]>=math.isqrt(freq[r['text']]-1)+1: counts['train']['duplicate']+=1; continue emitted[r['text']]+=1; train.append(r) counts['train']['unique']=len(freq) top=[dict(text=t,seen=n,kept=emitted[t]) for t,n in freq.most_common(50)] kept['train']=train; del rows,freq,emitted for split,rows in kept.items(): with open(out/f'{split}.jsonl','w',encoding='utf-8') as fh: for r in rows: fh.write(json.dumps(r,ensure_ascii=False)+'\n') counts[split]['written']=len(rows) counts[split]['written_chars']=sum(len(r['text']) for r in rows) with open(out/'review.jsonl','w',encoding='utf-8') as fh: for r in review: fh.write(json.dumps(r,ensure_ascii=False)+'\n') manifest=dict(source='LCCC-base (thu-coai), local Baidu Netdisk copy', source_files={f'LCCC-base_{n}.json':sha256(src/f'LCCC-base_{n}.json') for n in SPLITS}, terms_sha256=sha256(a.terms),script_sha256=sha256(__file__), filter_sha256=sha256(ROOT/'src/vimeml/corpus/zh_filter.py'), sequence_policy='BOS + one cleaned turn + EOS; LCCC dialogue splits kept', min_han=MIN_HAN,heldout_min_chars=HELDOUT_MIN_CHARS,repeat_policy='ceil(sqrt(n)) copies',top_repeats=top,traditional_threshold=th.traditional,review=len(review), counts={s:dict(c) for s,c in counts.items()}) (out/'manifest.json').write_text(json.dumps(manifest,ensure_ascii=False,indent=1),encoding='utf-8') print(json.dumps(manifest['counts'],ensure_ascii=False,indent=1)) if __name__=='__main__': main()