Splintir commited on
Commit
a5fd688
·
verified ·
1 Parent(s): c120609

Ship the data scripts with the checkpoint

Browse files
Files changed (1) hide show
  1. tts_data.py +267 -0
tts_data.py ADDED
@@ -0,0 +1,267 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Turn one PLD language into SpeechT5 training inputs, cached on the Hub.
2
+
3
+ Why this exists: `halohalo/finetune_tts.py` reads PLD from a raw corpus on a
4
+ local disk (`PLD_RAW`), which a Colab VM does not have. The Hub copy
5
+ (`sapinsapin/pld`) is 201 train shards of every language interleaved, so
6
+ pulling one language means touching all ~24 GB.
7
+
8
+ That is a one-time cost, and this script pays it once: stream the shards one at
9
+ a time (never more than one on disk), keep the rows for one language, run the
10
+ same text/mel/x-vector preprocessing the original training used, and write the
11
+ result to parquet. Push that to a Hub dataset repo and every later run --
12
+ including every resume after a free-tier VM disappears -- pulls ~1 GB instead
13
+ of 24.
14
+
15
+ Preprocessing is deliberately identical to `finetune_tts.py`:
16
+ - text: curly apostrophes normalized, digit-bearing lines dropped
17
+ - audio: SpeechT5Processor log-mel targets at 16 kHz
18
+ - speaker: one speechbrain x-vector per *clip*, F.normalize'd, never averaged
19
+ - caps: <=220 input ids, <=960 mel frames
20
+
21
+ Usage (on the VM):
22
+ python scripts/tts_data.py --language ceb --push-to Splintir/pld-ceb-tts-proc
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import argparse
28
+ import io
29
+ import os
30
+ import re
31
+ import shutil
32
+ import sys
33
+ import time
34
+ from pathlib import Path
35
+
36
+ import numpy as np
37
+
38
+ SR = 16000
39
+ PLD_REPO = "sapinsapin/pld"
40
+ N_TRAIN_SHARDS = 201
41
+ DIGIT_RE = re.compile(r"\d")
42
+
43
+ # Columns read from each shard. `audio` dominates the transfer; the rest are
44
+ # cheap and decide whether a row is kept at all.
45
+ COLUMNS = [
46
+ "audio",
47
+ "sentence",
48
+ "language",
49
+ "speech_type",
50
+ "num_words",
51
+ "text_is_prompt",
52
+ "speaker_id",
53
+ "duration",
54
+ ]
55
+
56
+ MAX_INPUT_IDS = 220
57
+ MAX_MEL_FRAMES = 960
58
+
59
+
60
+ def clean_text(text: str) -> str | None:
61
+ """SpeechT5's tokenizer is character-level Latin: normalize apostrophes and
62
+ reject digits rather than teach the model to skip them."""
63
+ text = text.replace("’", "'").replace("‘", "'").strip()
64
+ if not text or DIGIT_RE.search(text):
65
+ return None
66
+ return text
67
+
68
+
69
+ def keep_row(row: dict, language: str) -> bool:
70
+ """The TTS filter from halolib.finetune._PLD_FILTERS, plus the language."""
71
+ return (
72
+ row["language"] == language
73
+ and row["speech_type"] == "read"
74
+ and not row["text_is_prompt"]
75
+ and row["num_words"] >= 3
76
+ )
77
+
78
+
79
+ def build_embedder():
80
+ import torch
81
+
82
+ # speechbrain 1.1 registers optional integrations (k2, wordemb, ...) as lazy
83
+ # modules that import on any attribute access. Loading the Xvector lobe goes
84
+ # through pydoc, which probes dunders on every module in sys.modules and so
85
+ # force-imports integrations whose dependencies are absent. Dunders never
86
+ # come from a lazy import, so refusing them is safe. Same patch as
87
+ # sapin-hil/tts.py.
88
+ from speechbrain.utils import importutils as _importutils
89
+
90
+ _lazy_getattr = _importutils.LazyModule.__getattr__
91
+ _importutils.LazyModule.__getattr__ = (
92
+ lambda self, attr: (_ for _ in ()).throw(AttributeError(attr))
93
+ if attr.startswith("__") else _lazy_getattr(self, attr))
94
+
95
+ from speechbrain.inference.speaker import EncoderClassifier
96
+ from speechbrain.utils.fetching import LocalStrategy
97
+
98
+ savedir = Path(os.environ.get("HF_HOME", "~/.cache")).expanduser() / "speechbrain-xvect"
99
+ return EncoderClassifier.from_hparams(
100
+ source="speechbrain/spkrec-xvect-voxceleb",
101
+ savedir=str(savedir),
102
+ # COPY, not the default SYMLINK: symlinking needs Developer Mode on
103
+ # Windows and fails with WinError 1314 otherwise. Harmless on Linux.
104
+ local_strategy=LocalStrategy.COPY,
105
+ run_opts={"device": "cuda" if torch.cuda.is_available() else "cpu"},
106
+ )
107
+
108
+
109
+ def iter_shard_rows(language: str, token: str | None, shards: range):
110
+ """Yield matching rows shard by shard, holding one shard on disk at a time."""
111
+ import pyarrow.parquet as pq
112
+ from huggingface_hub import hf_hub_download
113
+
114
+ scratch = Path("/content/_pld_shard") if Path("/content").exists() else Path("./_pld_shard")
115
+
116
+ for i in shards:
117
+ name = f"data/train-{i:05d}-of-{N_TRAIN_SHARDS:05d}.parquet"
118
+ shutil.rmtree(scratch, ignore_errors=True)
119
+ scratch.mkdir(parents=True, exist_ok=True)
120
+ path = hf_hub_download(
121
+ PLD_REPO, name, repo_type="dataset", token=token, local_dir=str(scratch)
122
+ )
123
+ table = pq.read_table(path, columns=COLUMNS)
124
+ # to_pylist on the whole shard materializes 1500 audio blobs (~120 MB);
125
+ # row-group at a time keeps the peak an order of magnitude lower.
126
+ for batch in table.to_batches(max_chunksize=100):
127
+ for row in batch.to_pylist():
128
+ if keep_row(row, language):
129
+ yield i, row
130
+ del table
131
+ shutil.rmtree(scratch, ignore_errors=True)
132
+
133
+
134
+ def process(rows, processor, embedder, log_every: int = 250):
135
+ """(audio, text) -> (input_ids, mel labels, x-vector), dropping what cannot train."""
136
+ import soundfile as sf
137
+ import torch
138
+
139
+ kept = seen = 0
140
+ t0 = time.time()
141
+ for shard_i, row in rows:
142
+ seen += 1
143
+ text = clean_text(row["sentence"])
144
+ if text is None:
145
+ continue
146
+
147
+ wav, sr = sf.read(io.BytesIO(row["audio"]["bytes"]), dtype="float32")
148
+ if sr != SR:
149
+ raise SystemExit(f"expected {SR} Hz, shard {shard_i} gave {sr}")
150
+ if wav.ndim > 1:
151
+ wav = wav.mean(axis=1)
152
+
153
+ example = processor(
154
+ text=text, audio_target=wav, sampling_rate=SR, return_attention_mask=False
155
+ )
156
+ input_ids = example["input_ids"]
157
+ labels = np.asarray(example["labels"][0], dtype=np.float32)
158
+ if len(input_ids) > MAX_INPUT_IDS or len(labels) > MAX_MEL_FRAMES:
159
+ continue
160
+
161
+ with torch.no_grad():
162
+ emb = embedder.encode_batch(torch.tensor(wav).unsqueeze(0))
163
+ emb = torch.nn.functional.normalize(emb, dim=2).squeeze().cpu().numpy()
164
+
165
+ kept += 1
166
+ if kept % log_every == 0:
167
+ rate = seen / max(time.time() - t0, 1e-9)
168
+ print(
169
+ f" shard {shard_i:3d} scanned {seen:6d} kept {kept:6d}"
170
+ f" ({rate:.1f} rows/s)",
171
+ flush=True,
172
+ )
173
+
174
+ yield {
175
+ "input_ids": list(map(int, input_ids)),
176
+ "labels": labels.reshape(-1).tolist(),
177
+ "n_mel_frames": int(labels.shape[0]),
178
+ "speaker_embeddings": emb.astype(np.float32).tolist(),
179
+ "text": text,
180
+ "speaker_id": row["speaker_id"],
181
+ "duration": float(row["duration"]),
182
+ }
183
+
184
+
185
+ def write_parquet(records, out_dir: Path, rows_per_file: int = 1000) -> int:
186
+ """Stream records to parquet shards so peak memory stays near one shard."""
187
+ import pyarrow as pa
188
+ import pyarrow.parquet as pq
189
+
190
+ out_dir.mkdir(parents=True, exist_ok=True)
191
+ buf, n, part = [], 0, 0
192
+
193
+ def flush() -> None:
194
+ nonlocal buf, part
195
+ if not buf:
196
+ return
197
+ pq.write_table(pa.Table.from_pylist(buf), out_dir / f"part-{part:04d}.parquet")
198
+ part += 1
199
+ buf = []
200
+
201
+ for rec in records:
202
+ buf.append(rec)
203
+ n += 1
204
+ if len(buf) >= rows_per_file:
205
+ flush()
206
+ flush()
207
+ return n
208
+
209
+
210
+ def main() -> None:
211
+ sys.stdout.reconfigure(encoding="utf-8")
212
+ ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
213
+ ap.add_argument("--language", required=True, help="PLD ISO 639-3 code, e.g. ceb")
214
+ ap.add_argument("--out", default="/content/pld_proc", help="local parquet dir")
215
+ ap.add_argument("--push-to", default="", help="Hub dataset repo to upload to")
216
+ ap.add_argument("--private", action="store_true")
217
+ ap.add_argument("--shards", type=int, default=N_TRAIN_SHARDS,
218
+ help="how many train shards to scan (fewer = smaller sample)")
219
+ ap.add_argument("--max-samples", type=int, default=0, help="0 = every usable clip")
220
+ args = ap.parse_args()
221
+
222
+ os.environ.setdefault("HF_HUB_ENABLE_HF_TRANSFER", "1")
223
+ token = os.environ.get("HF_TOKEN") or None
224
+
225
+ from transformers import SpeechT5Processor
226
+
227
+ processor = SpeechT5Processor.from_pretrained("microsoft/speecht5_tts")
228
+ embedder = build_embedder()
229
+
230
+ out_dir = Path(args.out) / args.language
231
+ shutil.rmtree(out_dir, ignore_errors=True)
232
+
233
+ print(f"scanning {args.shards} PLD train shards for {args.language} ...", flush=True)
234
+ rows = iter_shard_rows(args.language, token, range(args.shards))
235
+ records = process(rows, processor, embedder)
236
+ if args.max_samples:
237
+ import itertools
238
+
239
+ records = itertools.islice(records, args.max_samples)
240
+
241
+ t0 = time.time()
242
+ n = write_parquet(records, out_dir)
243
+ print(f"wrote {n} rows to {out_dir} in {(time.time() - t0) / 60:.1f} min")
244
+ if n == 0:
245
+ raise SystemExit(f"no usable {args.language} rows -- check the filters")
246
+
247
+ size = sum(f.stat().st_size for f in out_dir.glob("*.parquet")) / 1024**3
248
+ print(f"parquet size: {size:.2f} GB")
249
+
250
+ if args.push_to:
251
+ from huggingface_hub import HfApi
252
+
253
+ api = HfApi(token=token)
254
+ api.create_repo(args.push_to, repo_type="dataset", exist_ok=True,
255
+ private=args.private)
256
+ api.upload_folder(
257
+ folder_path=str(out_dir),
258
+ repo_id=args.push_to,
259
+ repo_type="dataset",
260
+ path_in_repo=f"data/{args.language}",
261
+ commit_message=f"Preprocessed {args.language} TTS inputs ({n} clips)",
262
+ )
263
+ print(f"pushed: https://huggingface.co/datasets/{args.push_to}")
264
+
265
+
266
+ if __name__ == "__main__":
267
+ main()