giangndm commited on
Commit
cf4aa53
·
verified ·
1 Parent(s): f4c4c21

Add Text Column Extract Gradio app

Browse files
Files changed (3) hide show
  1. README.md +79 -5
  2. app.py +810 -0
  3. requirements.txt +7 -0
README.md CHANGED
@@ -1,13 +1,87 @@
1
  ---
2
  title: Text Column Extract
3
- emoji: 🔥
4
- colorFrom: yellow
5
- colorTo: green
6
  sdk: gradio
7
  sdk_version: 6.28.0
8
- python_version: '3.12'
9
  app_file: app.py
10
  pinned: false
 
 
 
 
 
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: Text Column Extract
3
+ emoji: 🧲
4
+ colorFrom: indigo
5
+ colorTo: purple
6
  sdk: gradio
7
  sdk_version: 6.28.0
 
8
  app_file: app.py
9
  pinned: false
10
+ license: mit
11
+ hf_oauth: true
12
+ hf_oauth_expiration_minutes: 480
13
+ hf_oauth_scopes:
14
+ - email
15
+ - read-repos
16
+ - write-repos
17
+ - manage-repos
18
  ---
19
 
20
+ # 🧲 Text Column Extract
21
+
22
+ App Gradio để **trích xuất một cột text** từ một dataset trên Hugging Face Hub và ghi nó
23
+ thành **một split** của một dataset khác — dùng thư viện [`datasets`](https://huggingface.co/docs/datasets).
24
+
25
+ ## Tính năng
26
+
27
+ - **Đăng nhập bằng tài khoản Hugging Face** (OAuth của Spaces) hoặc dán Access Token thủ công.
28
+ - Nhập **dataset nguồn**: `repo`, `subset` (config), `split`, `column_name`.
29
+ - Nhập **dataset đích**: `repo` (tự do — không hard-code namespace nào), `config`, `split`.
30
+ - Nút **✨ Generate split name** sinh tên split mặc định từ `repo + subset` của nguồn
31
+ (tuỳ chọn thêm cả tên split gốc), đã chuẩn hoá theo ràng buộc tên split của `datasets`.
32
+ - Nút **🔎 Kiểm tra dataset nguồn**: liệt kê subset, split, số cột, số dòng và kiểm tra
33
+ cột cần trích xuất có tồn tại hay không (không cần tải dữ liệu).
34
+ - Ghi kết quả dạng **Parquet** lên Hub, có 3 chế độ khi split đã tồn tại:
35
+ - **Ghi đè**: xoá file cũ của đúng split đó rồi đẩy mới.
36
+ - **Nối thêm**: đọc split cũ, kiểm tra cột khớp, rồi gộp lại.
37
+ - **Báo lỗi**: dừng lại nếu split đã tồn tại.
38
+ - Tuỳ chọn nâng cao: `revision`, đổi tên cột ở output, giới hạn số dòng, `num_proc`,
39
+ `streaming` cho dataset rất lớn, `max_shard_size`, commit message, repo private.
40
+ - Nhật ký chạy trực tiếp + xem trước 5 dòng đầu + snippet `load_dataset` sau khi xong.
41
+
42
+ ## Cách dùng nhanh
43
+
44
+ 1. Bấm **Đăng nhập bằng tài khoản Hugging Face** (hoặc dán token có quyền *Write*).
45
+ 2. Điền nguồn, ví dụ: `repo = nyu-mll/glue`, `subset = mrpc`, `split = train`, `column = sentence1`.
46
+ 3. Điền đích, ví dụ: `repo = ten-user/my-text-corpus`, rồi bấm **Generate split name** → `glue_mrpc`.
47
+ 4. Bấm **🚀 Trích xuất & đẩy lên Hub**.
48
+
49
+ Kết quả nằm ở:
50
+
51
+ ```python
52
+ from datasets import load_dataset
53
+
54
+ ds = load_dataset("ten-user/my-text-corpus", split="glue_mrpc")
55
+ ```
56
+
57
+ ## Cấu trúc file dữ liệu trên Hub
58
+
59
+ | Output config | Đường dẫn file |
60
+ | --- | --- |
61
+ | `default` | `data/<split>-00000-of-00001.parquet` |
62
+ | khác `default` | `<config>/<split>-00000-of-00001.parquet` |
63
+
64
+ Tên split chỉ được chứa chữ, số, `_` và dấu `.` phân cấp
65
+ (ràng buộc `^\w+(\.\w+)*$` của thư viện `datasets`).
66
+
67
+ > ⚠️ **Lưu ý:** mọi split trong **cùng một config** phải có **cùng tên cột** (ràng buộc của
68
+ > `datasets`). Nếu bạn đổi tên cột ở output, hãy ghi vào một **Output config khác**
69
+ > (ví dụ `vi` thay vì `default`).
70
+
71
+ ## Ghi chú kỹ thuật
72
+
73
+ - Token được giữ trong `gr.State` của từng phiên trình duyệt, không ghi ra file của Space.
74
+ - OAuth của Space xin các scope `read-repos`, `write-repos`, `manage-repos` để có thể tạo
75
+ repo và đẩy dữ liệu thay mặt người dùng.
76
+ - Nếu Space được cấu hình secret `HF_TOKEN`, app vẫn dùng được khi chưa đăng nhập.
77
+ - Chạy local: `pip install -r requirements.txt && python app.py`
78
+ (cần `HF_TOKEN` trong môi trường để Gradio mock OAuth khi phát triển ngoài Space).
79
+
80
+ ## Triển khai
81
+
82
+ Space này chạy bằng **SDK `gradio`** (`app.py` + `requirements.txt`, gradio do image của
83
+ Hugging Face cung cấp) trên hardware **ZeroGPU** — mức miễn phí cho tài khoản cá nhân
84
+ (2 Gradio Space / tài khoản). App chỉ dùng CPU nên không cần `@spaces.GPU`.
85
+
86
+ Ngoài ra repo còn kèm sẵn `Dockerfile` + `requirements.docker.txt` nếu bạn muốn chạy bằng
87
+ **SDK `docker`** (khi đó sửa metadata thành `sdk: docker` và `app_port: 7860`).
app.py ADDED
@@ -0,0 +1,810 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Text Column Extract
3
+ ===================
4
+
5
+ Gradio Space: trích xuất **một cột text** từ một dataset (repo / subset / split)
6
+ trên Hugging Face Hub và ghi nó thành **một split** của một dataset khác.
7
+
8
+ Luồng sử dụng:
9
+ 1. Đăng nhập (bằng tài khoản HF qua OAuth, hoặc dán Access Token).
10
+ 2. Nhập dataset nguồn: repo id, subset (config), split, tên cột text.
11
+ 3. Nhập dataset đích: repo id (tự do, KHÔNG hard-code namespace nào), tên split
12
+ (nút "Generate split name" sinh mặc định từ repo + subset nguồn).
13
+ 4. Bấm "Trích xuất & đẩy lên Hub".
14
+
15
+ Toàn bộ thao tác đọc/ghi đều dùng thư viện `datasets` + `huggingface_hub`.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import os
21
+ import re
22
+ import traceback
23
+ from typing import Optional
24
+
25
+ import gradio as gr
26
+ from datasets import (
27
+ concatenate_datasets,
28
+ get_dataset_config_info,
29
+ get_dataset_config_names,
30
+ get_dataset_split_names,
31
+ load_dataset,
32
+ )
33
+ from huggingface_hub import CommitOperationDelete, HfApi, login, logout, whoami
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # Hằng số / cấu hình
37
+ # ---------------------------------------------------------------------------
38
+
39
+ #: Regex hợp lệ cho tên split của thư viện `datasets` (datasets.splits._split_re).
40
+ SPLIT_RE = re.compile(r"^\w+(\.\w+)*$")
41
+
42
+ PUSH_MODES = {
43
+ "Ghi đè split đã tồn tại": "overwrite",
44
+ "Nối thêm vào split đã tồn tại": "append",
45
+ "Báo lỗi nếu split đã tồn tại": "error",
46
+ }
47
+
48
+ MAX_SPLIT_NAME_LEN = 96
49
+
50
+ #: Gradio 6 đổi `show_copy_button` -> `buttons`; giữ tương thích cả Gradio 5 và 6.
51
+ GRADIO_MAJOR = int(str(getattr(gr, "__version__", "5")).split(".")[0])
52
+ COPY_BUTTON_KWARGS = {"buttons": ["copy"]} if GRADIO_MAJOR >= 6 else {"show_copy_button": True}
53
+
54
+ CSS = """
55
+ .gradio-container {max-width: 1180px !important;}
56
+ #extract-btn {min-height: 52px; font-size: 1.05rem; font-weight: 600;}
57
+ #log-box textarea {font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .82rem;}
58
+ """
59
+
60
+
61
+ # ---------------------------------------------------------------------------
62
+ # Tiện ích
63
+ # ---------------------------------------------------------------------------
64
+
65
+
66
+ def _clean(value: Optional[str]) -> str:
67
+ """Chuẩn hoá input dạng chuỗi."""
68
+ return value.strip() if isinstance(value, str) else ""
69
+
70
+
71
+ def _as_token(value) -> Optional[str]:
72
+ """Chấp nhận cả `str` và `gr.OAuthToken` -> trả về chuỗi token."""
73
+ if value is None:
74
+ return None
75
+ if isinstance(value, str):
76
+ return value.strip() or None
77
+ token = getattr(value, "token", None) # gr.OAuthToken
78
+ return str(token).strip() if token else None
79
+
80
+
81
+ def resolve_token(*candidates) -> Optional[str]:
82
+ """Lấy token theo thứ tự ưu tiên: session -> OAuth -> biến môi trường của Space."""
83
+ for candidate in list(candidates) + [
84
+ os.environ.get("HF_TOKEN"),
85
+ os.environ.get("HUGGING_FACE_HUB_TOKEN"),
86
+ ]:
87
+ token = _as_token(candidate)
88
+ if token:
89
+ return token
90
+ return None
91
+
92
+
93
+ def token_user(token: Optional[str]) -> Optional[str]:
94
+ """Trả về username nếu token hợp lệ, ngược lại trả về None."""
95
+ if not token:
96
+ return None
97
+ try:
98
+ info = whoami(token=token)
99
+ except Exception: # noqa: BLE001
100
+ return None
101
+ if isinstance(info, dict):
102
+ return info.get("name") or info.get("user") or "unknown"
103
+ return "unknown"
104
+
105
+
106
+ def profile_username(profile) -> Optional[str]:
107
+ """Lấy username từ `gr.OAuthProfile` (dict-like)."""
108
+ if not profile:
109
+ return None
110
+ for key in ("preferred_username", "username", "name", "sub"):
111
+ try:
112
+ value = profile.get(key) if hasattr(profile, "get") else getattr(profile, key, None)
113
+ except Exception: # noqa: BLE001
114
+ value = None
115
+ if value:
116
+ return str(value)
117
+ return None
118
+
119
+
120
+ def sanitize_split_name(name: str) -> str:
121
+ """Chuyển một chuỗi bất kỳ thành tên split hợp lệ với thư viện datasets."""
122
+ name = re.sub(r"[^0-9A-Za-z]+", "_", name or "")
123
+ name = name.strip("_")[:MAX_SPLIT_NAME_LEN].strip("_")
124
+ return name
125
+
126
+
127
+ def build_default_split_name(in_repo: str, in_subset: str, in_split: str, include_split: bool) -> str:
128
+ """Sinh tên split mặc định từ repo nguồn + subset (+ split gốc nếu được chọn)."""
129
+ repo_name = _clean(in_repo).rstrip("/").split("/")[-1]
130
+ parts = [repo_name]
131
+ if _clean(in_subset):
132
+ parts.append(_clean(in_subset))
133
+ if include_split and _clean(in_split):
134
+ parts.append(_clean(in_split))
135
+ return sanitize_split_name("_".join(p for p in parts if p)) or "train"
136
+
137
+
138
+ def data_dir_for(config_name: str) -> str:
139
+ """Thư mục chứa parquet trong repo đích (khớp với `Dataset.push_to_hub`)."""
140
+ return config_name if config_name != "default" else "data"
141
+
142
+
143
+ def shard_prefixes(data_dir: str, split: str) -> list[str]:
144
+ """Các tiền tố file thuộc đúng một split trong repo dataset."""
145
+ return [f"{data_dir}/{split}-", f"{data_dir}/{split}.parquet"]
146
+
147
+
148
+ def login_badge() -> str:
149
+ return "⚠️ **Chưa đăng nhập.** Dùng nút OAuth ở trên, hoặc dán Access Token."
150
+
151
+
152
+ # ---------------------------------------------------------------------------
153
+ # Đăng nhập / đăng xuất
154
+ # ---------------------------------------------------------------------------
155
+
156
+
157
+ def on_page_load(
158
+ profile: Optional[gr.OAuthProfile],
159
+ oauth_token: Optional[gr.OAuthToken],
160
+ session_token: Optional[str],
161
+ ):
162
+ """Chạy khi trang tải (và sau mỗi lần OAuth đăng nhập/đăng xuất)."""
163
+ token = resolve_token(session_token, oauth_token)
164
+ user = token_user(token)
165
+ if user:
166
+ scope = getattr(oauth_token, "scope", "") or ""
167
+ extra = f" _(OAuth scope: `{scope}`)_" if scope else ""
168
+ return session_token, f"✅ **Đã đăng nhập:** `{user}`{extra}"
169
+ name = profile_username(profile)
170
+ if name:
171
+ return session_token, (
172
+ f"👤 Tài khoản OAuth: `{name}` — nhưng chưa nhận được access token có quyền ghi. "
173
+ "Hãy cấp quyền `write-repos` / `manage-repos`, hoặc dán Access Token."
174
+ )
175
+ return session_token, login_badge()
176
+
177
+
178
+ def do_token_login(raw_token: str, session_token: Optional[str]):
179
+ """Đăng nhập bằng Access Token dán tay."""
180
+ token = _clean(raw_token) or _as_token(session_token)
181
+ if not token:
182
+ return session_token, "❌ Bạn chưa nhập token."
183
+
184
+ user = token_user(token)
185
+ if not user:
186
+ return (
187
+ session_token,
188
+ "❌ Token không hợp lệ hoặc đã hết hạn. Tạo token mới tại "
189
+ "https://huggingface.co/settings/tokens (cần quyền **Write**).",
190
+ )
191
+ try: # best-effort: để các thư viện con dùng lại khi không truyền token
192
+ login(token=token, add_to_git_credential=False)
193
+ except Exception: # noqa: BLE001
194
+ pass
195
+ return token, f"✅ **Đã đăng nhập:** `{user}`"
196
+
197
+
198
+ def do_logout(session_token: Optional[str]):
199
+ if session_token:
200
+ try:
201
+ logout(token=session_token)
202
+ except Exception: # noqa: BLE001
203
+ pass
204
+ return None, "👋 Đã xoá token khỏi phiên làm việc này."
205
+
206
+
207
+ # ---------------------------------------------------------------------------
208
+ # Nút "Generate split name"
209
+ # ---------------------------------------------------------------------------
210
+
211
+
212
+ def generate_split_name(in_repo: str, in_subset: str, in_split: str, include_split: bool):
213
+ name = build_default_split_name(in_repo, in_subset, in_split, include_split)
214
+ if not SPLIT_RE.match(name):
215
+ name = "train"
216
+ return name, f"Đã sinh tên split: `{name}` — bạn có thể sửa lại tuỳ ý."
217
+
218
+
219
+ # ---------------------------------------------------------------------------
220
+ # Kiểm tra dataset nguồn
221
+ # ---------------------------------------------------------------------------
222
+
223
+
224
+ def inspect_source(
225
+ oauth_token: Optional[gr.OAuthToken],
226
+ session_token: Optional[str],
227
+ in_repo: str,
228
+ in_subset: str,
229
+ in_column: str,
230
+ in_revision: str,
231
+ ):
232
+ token = resolve_token(session_token, oauth_token)
233
+ repo = _clean(in_repo)
234
+ subset = _clean(in_subset) or None
235
+ column = _clean(in_column)
236
+ revision = _clean(in_revision) or None
237
+
238
+ if not repo:
239
+ return "❌ Chưa nhập **Input repo**."
240
+ if not token:
241
+ return "❌ Chưa đăng nhập. Hãy đăng nhập trước khi kiểm tra."
242
+
243
+ lines: list[str] = []
244
+ try:
245
+ configs = get_dataset_config_names(repo, token=token, revision=revision)
246
+ except Exception as exc: # noqa: BLE001
247
+ return f"❌ Không đọc được dataset `{repo}`.\n\n```\n{type(exc).__name__}: {exc}\n```"
248
+
249
+ lines.append(f"### 🔎 `{repo}`")
250
+ if len(configs) == 1 and configs[0] == "default":
251
+ lines.append("- **Subset (config):** `default` — dataset chỉ có 1 subset.")
252
+ else:
253
+ shown = ", ".join(f"`{c}`" for c in configs[:25])
254
+ if len(configs) > 25:
255
+ shown += f" … (+{len(configs) - 25})"
256
+ lines.append(f"- **Số subset:** {len(configs)} → {shown}")
257
+ if subset and subset not in configs:
258
+ lines.append(f"- ⚠️ Subset `{subset}` **không** nằm trong danh sách trên.")
259
+
260
+ try:
261
+ splits = get_dataset_split_names(repo, config_name=subset, token=token, revision=revision)
262
+ lines.append(f"- **Splits của `{subset or 'default'}`:** " + ", ".join(f"`{s}`" for s in splits))
263
+ except Exception as exc: # noqa: BLE001
264
+ lines.append(f"- ⚠️ Không đọc được splits: `{type(exc).__name__}: {exc}`")
265
+
266
+ try:
267
+ info = get_dataset_config_info(repo, config_name=subset, token=token, revision=revision)
268
+ columns = list(info.features.keys())
269
+ lines.append(f"- **Số cột:** {len(columns)}")
270
+ lines.append("- **Các cột:** " + ", ".join(f"`{c}`" for c in columns[:80]))
271
+ if column:
272
+ if column in columns:
273
+ lines.append(f"- ✅ Cột **`{column}`** tồn tại.")
274
+ else:
275
+ lines.append(f"- ❌ Cột **`{column}`** KHÔNG tồn tại trong dataset này.")
276
+ sizes = [
277
+ f"`{k}`: {v.num_examples:,}"
278
+ for k, v in (getattr(info, "splits", None) or {}).items()
279
+ if v.num_examples
280
+ ]
281
+ if sizes:
282
+ lines.append("- **Số dòng:** " + ", ".join(sizes))
283
+ except Exception as exc: # noqa: BLE001
284
+ lines.append(f"- ⚠️ Không đọc được thông tin cột: `{type(exc).__name__}: {exc}`")
285
+
286
+ return "\n".join(lines)
287
+
288
+
289
+ # ---------------------------------------------------------------------------
290
+ # Trích xuất & đẩy lên Hub (generator để log chạy trực tiếp)
291
+ # ---------------------------------------------------------------------------
292
+
293
+
294
+ def extract_and_push(
295
+ oauth_token: Optional[gr.OAuthToken],
296
+ session_token: Optional[str],
297
+ in_repo: str,
298
+ in_subset: str,
299
+ in_split: str,
300
+ in_column: str,
301
+ in_revision: str,
302
+ out_column: str,
303
+ max_rows: float,
304
+ num_proc: float,
305
+ streaming: bool,
306
+ out_repo: str,
307
+ out_config: str,
308
+ out_split: str,
309
+ out_private: bool,
310
+ push_mode_label: str,
311
+ set_default: bool,
312
+ max_shard_size: str,
313
+ commit_message: str,
314
+ progress=gr.Progress(),
315
+ ):
316
+ logs: list[str] = []
317
+
318
+ def snapshot(status: str = "⏳ Đang xử lý…"):
319
+ return status, "\n".join(logs), None
320
+
321
+ def log(message: str = "") -> tuple:
322
+ logs.append(str(message))
323
+ print(message, flush=True)
324
+ return snapshot()
325
+
326
+ def fail(message: str):
327
+ log("")
328
+ log(f"❌ LỖI: {message}")
329
+ return f"### ❌ Thất bại\n\n{message}", "\n".join(logs), None
330
+
331
+ try:
332
+ # ---------------- 0. Token & kiểm tra đầu vào ----------------
333
+ token = resolve_token(session_token, oauth_token)
334
+ if not token:
335
+ yield fail("Bạn cần **đăng nhập** trước (OAuth hoặc Access Token).")
336
+ return
337
+ user = token_user(token)
338
+ if not user:
339
+ yield fail("Token không hợp lệ hoặc đã hết hạn.")
340
+ return
341
+
342
+ repo = _clean(in_repo)
343
+ subset = _clean(in_subset) or None
344
+ split = _clean(in_split)
345
+ column = _clean(in_column)
346
+ revision = _clean(in_revision) or None
347
+ out_repo_id = _clean(out_repo)
348
+ out_cfg = _clean(out_config) or "default"
349
+ out_split_name = _clean(out_split)
350
+ target_column = _clean(out_column) or column
351
+ mode = PUSH_MODES.get(push_mode_label, "overwrite")
352
+ n_rows = int(max_rows or 0)
353
+ n_proc = int(num_proc or 0)
354
+ shard_size = _clean(max_shard_size) or None
355
+
356
+ if not repo:
357
+ yield fail("Chưa nhập **Input repo**.")
358
+ return
359
+ if not split:
360
+ yield fail("Chưa nhập **Input split**.")
361
+ return
362
+ if not column:
363
+ yield fail("Chưa nhập **tên cột text** cần trích xuất.")
364
+ return
365
+ if not out_repo_id:
366
+ yield fail("Chưa nhập **Output repo** (ví dụ: `ten-user/ten-dataset`).")
367
+ return
368
+ if not out_split_name:
369
+ yield fail("Chưa nhập **Output split**. Hãy bấm nút *Generate split name*.")
370
+ return
371
+ if not SPLIT_RE.match(out_split_name):
372
+ yield fail(
373
+ f"Tên split `{out_split_name}` không hợp lệ (chỉ cho phép chữ, số, `_`, "
374
+ f"và dấu `.` phân cấp). Gợi ý: `{sanitize_split_name(out_split_name)}`."
375
+ )
376
+ return
377
+ if out_cfg == "data":
378
+ yield fail("Tên config không được là `data`.")
379
+ return
380
+ if streaming and mode == "append":
381
+ yield fail("Chế độ **streaming** không hỗ trợ nối vào split đã có. Hãy tắt streaming.")
382
+ return
383
+
384
+ log(f"👤 Tài khoản: {user}")
385
+ log("─" * 62)
386
+ log("📥 NGUỒN")
387
+ log(f" repo : {repo}")
388
+ log(f" subset : {subset or '(default)'}")
389
+ log(f" split : {split}")
390
+ log(f" cột : {column}" + (f" → đổi tên thành '{target_column}'" if target_column != column else ""))
391
+ if revision:
392
+ log(f" revision: {revision}")
393
+ log("📤 ĐÍCH")
394
+ log(f" repo : {out_repo_id}")
395
+ log(f" config : {out_cfg}")
396
+ log(f" split : {out_split_name}")
397
+ log(f" mode : {mode}")
398
+ log("─" * 62)
399
+ yield snapshot("⏳ Đang tải dataset nguồn…")
400
+
401
+ # ---------------- 1. Tải dataset nguồn ----------------
402
+ progress(0.05, desc="Đang tải dataset nguồn…")
403
+ load_kwargs: dict = {
404
+ "split": split,
405
+ "token": token,
406
+ "revision": revision,
407
+ "streaming": bool(streaming),
408
+ }
409
+ if subset:
410
+ load_kwargs["name"] = subset
411
+ if n_proc > 1 and not streaming:
412
+ load_kwargs["num_proc"] = n_proc
413
+
414
+ source = load_dataset(repo, **load_kwargs)
415
+
416
+ columns = list(source.column_names or [])
417
+ if column not in columns:
418
+ yield fail(
419
+ f"Cột `{column}` không tồn tại trong `{repo}` / `{subset or 'default'}` / `{split}`.\n\n"
420
+ f"Các cột hiện có: {', '.join(f'`{c}`' for c in columns[:60])}"
421
+ )
422
+ return
423
+
424
+ # ---------------- 2. Trích xuất cột ----------------
425
+ progress(0.2, desc="Đang trích xuất cột…")
426
+ yield log("⏳ Đang trích xuất cột…")
427
+ extracted = source.select_columns([column])
428
+ del source
429
+ if target_column != column:
430
+ extracted = extracted.rename_column(column, target_column)
431
+ yield log(f"✏️ Đã đổi tên cột `{column}` → `{target_column}`.")
432
+
433
+ if n_rows > 0:
434
+ if streaming:
435
+ extracted = extracted.take(n_rows)
436
+ else:
437
+ extracted = extracted.select(range(min(n_rows, len(extracted))))
438
+ yield log(f"🔢 Giới hạn còn tối đa {n_rows:,} dòng.")
439
+
440
+ if not streaming:
441
+ yield log(f"✅ Đã trích xuất {len(extracted):,} dòng (cột `{target_column}`).")
442
+
443
+ # ---------------- 3. Xử lý repo/split đích ----------------
444
+ api = HfApi(token=token)
445
+ data_dir = data_dir_for(out_cfg)
446
+ prefixes = shard_prefixes(data_dir, out_split_name)
447
+
448
+ existing_files: list[str] = []
449
+ repo_files: list[str] = []
450
+ try:
451
+ repo_files = list(api.list_repo_files(out_repo_id, repo_type="dataset", token=token))
452
+ existing_files = [f for f in repo_files if any(f.startswith(p) for p in prefixes)]
453
+ yield log(
454
+ f"📦 Repo đích đã tồn tại — có {len(existing_files)} file thuộc split `{out_split_name}`."
455
+ )
456
+ except Exception as exc: # noqa: BLE001
457
+ yield log(f"ℹ️ Repo đích chưa tồn tại / chưa truy cập được ({type(exc).__name__}). Sẽ tạo mới.")
458
+
459
+ # `datasets` chỉ cho phép một config mặc định: nếu repo đã có config `default`
460
+ # (thư mục `data/`) thì không thể set_default cho config khác.
461
+ if out_cfg != "default" and set_default and any(f.startswith("data/") for f in repo_files):
462
+ set_default = False
463
+ yield log("ℹ️ Repo đã có config `default` → bỏ qua `set_default` cho config mới.")
464
+
465
+ if existing_files and mode == "error":
466
+ yield fail(
467
+ f"Split `{out_split_name}` đã tồn tại trong `{out_repo_id}` ({len(existing_files)} file). "
468
+ "Hãy chọn chế độ *Ghi đè* hoặc *Nối thêm*."
469
+ )
470
+ return
471
+
472
+ if mode == "append" and existing_files:
473
+ progress(0.35, desc="Đang đọc split cũ để nối…")
474
+ yield log("⏳ Đang đọc split đã tồn tại để nối thêm…")
475
+ try:
476
+ existing = load_dataset(
477
+ out_repo_id,
478
+ name=out_cfg if out_cfg != "default" else None,
479
+ split=out_split_name,
480
+ token=token,
481
+ )
482
+ except Exception as exc: # noqa: BLE001
483
+ yield fail(f"Không đọc được split cũ `{out_split_name}` để nối: {exc}")
484
+ return
485
+
486
+ if list(existing.column_names) != list(extracted.column_names):
487
+ yield fail(
488
+ "Không thể nối: cột của split cũ và dữ liệu mới khác nhau.\n\n"
489
+ f"- Split cũ : {existing.column_names}\n"
490
+ f"- Dữ liệu mới : {extracted.column_names}"
491
+ )
492
+ return
493
+ yield log(f"➕ Split cũ {len(existing):,} dòng + mới {len(extracted):,} dòng.")
494
+ extracted = concatenate_datasets([existing, extracted])
495
+ del existing
496
+
497
+ if existing_files and mode in ("overwrite", "append"):
498
+ progress(0.45, desc="Đang xoá dữ liệu cũ của split…")
499
+ yield log(f"🗑️ Xoá {len(existing_files)} file cũ của split `{out_split_name}`…")
500
+ api.create_commit(
501
+ repo_id=out_repo_id,
502
+ repo_type="dataset",
503
+ operations=[CommitOperationDelete(path_in_repo=f) for f in existing_files],
504
+ commit_message=f"Remove previous data of split '{out_split_name}'",
505
+ token=token,
506
+ )
507
+
508
+ # `datasets` yêu cầu mọi split trong CÙNG một config phải có cùng tên cột.
509
+ if repo_files:
510
+ try:
511
+ out_info = get_dataset_config_info(
512
+ out_repo_id,
513
+ config_name=out_cfg if out_cfg != "default" else None,
514
+ token=token,
515
+ )
516
+ existing_columns = list(out_info.features.keys())
517
+ except Exception: # noqa: BLE001
518
+ existing_columns = []
519
+ if existing_columns and existing_columns != list(extracted.column_names):
520
+ yield fail(
521
+ "Mọi split trong **cùng một config** phải có cùng tên cột "
522
+ "(ràng buộc của thư viện `datasets`).\n\n"
523
+ f"- Config `{out_cfg}` đang có cột: `{existing_columns}`\n"
524
+ f"- Dữ liệu mới có cột: `{list(extracted.column_names)}`\n\n"
525
+ "→ Hãy dùng **Output config** khác, hoặc giữ nguyên tên cột (bỏ đổi tên)."
526
+ )
527
+ return
528
+
529
+ # ---------------- 4. Push lên Hub ----------------
530
+ progress(0.6, desc="Đang đẩy lên Hugging Face Hub…")
531
+ yield log("🚀 Đang đẩy lên Hub (dataset lớn có thể mất vài phút)…")
532
+
533
+ push_kwargs: dict = {
534
+ "repo_id": out_repo_id,
535
+ "config_name": out_cfg,
536
+ "split": out_split_name,
537
+ "private": bool(out_private),
538
+ "token": token,
539
+ "commit_message": _clean(commit_message)
540
+ or f"Extract column '{column}' from {repo} ({subset or 'default'}/{split})",
541
+ }
542
+ if out_cfg != "default":
543
+ push_kwargs["set_default"] = bool(set_default)
544
+ if shard_size:
545
+ push_kwargs["max_shard_size"] = shard_size
546
+ if n_proc > 1:
547
+ push_kwargs["num_proc"] = n_proc
548
+
549
+ try:
550
+ extracted.push_to_hub(**push_kwargs)
551
+ except ValueError as exc:
552
+ # Vài phiên bản `datasets` báo lỗi khi đã tồn tại config `default`.
553
+ if push_kwargs.get("set_default") and "configuration named 'default'" in str(exc):
554
+ yield log("⚠️ Không thể set_default (repo đã có config `default`) → thử lại không set_default…")
555
+ push_kwargs.pop("set_default", None)
556
+ extracted.push_to_hub(**push_kwargs)
557
+ else:
558
+ raise
559
+
560
+ # ---------------- 5. Kiểm tra lại ----------------
561
+ progress(0.92, desc="Đang kiểm tra kết quả…")
562
+ final_files = [
563
+ f
564
+ for f in api.list_repo_files(out_repo_id, repo_type="dataset", token=token)
565
+ if any(f.startswith(p) for p in prefixes)
566
+ ]
567
+ total_bytes = 0
568
+ try:
569
+ for entry in api.list_repo_tree(out_repo_id, repo_type="dataset", token=token, recursive=True):
570
+ path = getattr(entry, "path", "")
571
+ size = getattr(entry, "size", None)
572
+ if path in final_files and size:
573
+ total_bytes += size
574
+ except Exception: # noqa: BLE001
575
+ pass
576
+
577
+ rows_txt = "streaming" if streaming else f"{len(extracted):,} dòng"
578
+ url = f"https://huggingface.co/datasets/{out_repo_id}"
579
+ log("")
580
+ log(f"✅ HOÀN TẤT — {rows_txt}")
581
+ for f in final_files:
582
+ log(f" • {f}")
583
+ if total_bytes:
584
+ log(f" tổng dung lượng: {total_bytes / 1e6:.2f} MB")
585
+
586
+ status = (
587
+ "### ✅ Đã đẩy thành công\n\n"
588
+ f"- **Dataset đích:** [{out_repo_id}]({url})\n"
589
+ f"- **Config / split:** `{out_cfg}` / `{out_split_name}`\n"
590
+ f"- **Cột:** `{target_column}` — {rows_txt}\n"
591
+ f"- **File dữ liệu:** {len(final_files)}\n\n"
592
+ "```python\n"
593
+ "from datasets import load_dataset\n"
594
+ f"ds = load_dataset({out_repo_id!r}"
595
+ + (f", name={out_cfg!r}" if out_cfg != "default" else "")
596
+ + f", split={out_split_name!r})\n"
597
+ "```"
598
+ )
599
+
600
+ preview = None
601
+ if not streaming:
602
+ try:
603
+ preview = extracted.select(range(min(5, len(extracted)))).to_pandas()
604
+ except Exception: # noqa: BLE001
605
+ preview = None
606
+ progress(1.0, desc="Xong!")
607
+ yield status, "\n".join(logs), preview
608
+
609
+ except Exception as exc: # noqa: BLE001
610
+ log("")
611
+ log(f"❌ {type(exc).__name__}: {exc}")
612
+ log(traceback.format_exc())
613
+ yield (
614
+ f"### ❌ Thất bại\n\n```\n{type(exc).__name__}: {exc}\n```\n\n"
615
+ "Xem chi tiết ở ô **Nhật ký**.",
616
+ "\n".join(logs),
617
+ None,
618
+ )
619
+
620
+
621
+ # ---------------------------------------------------------------------------
622
+ # Giao diện
623
+ # ---------------------------------------------------------------------------
624
+
625
+ with gr.Blocks(title="Text Column Extract", theme=gr.themes.Soft(), css=CSS) as demo:
626
+ gr.Markdown(
627
+ """
628
+ # 🧲 Text Column Extract
629
+ Trích xuất **một cột text** từ dataset nguồn và ghi thành **một split** của dataset đích
630
+ trên Hugging Face Hub — dùng thư viện `datasets`.
631
+ """
632
+ )
633
+
634
+ session_token = gr.State(None)
635
+
636
+ # ---------------------- 1. Đăng nhập ----------------------
637
+ with gr.Accordion("🔐 1. Đăng nhập Hugging Face", open=True):
638
+ with gr.Row():
639
+ with gr.Column(scale=1):
640
+ login_button = gr.LoginButton("Đăng nhập bằng tài khoản Hugging Face")
641
+ gr.Markdown(
642
+ "<sub>OAuth xin quyền <code>read-repos</code>, <code>write-repos</code>, "
643
+ "<code>manage-repos</code> để tạo repo và đẩy dữ liệu. "
644
+ "Khi đã đăng nhập, nút này trở thành nút đăng xuất.</sub>"
645
+ )
646
+ with gr.Column(scale=1):
647
+ token_box = gr.Textbox(
648
+ label="Hoặc dán Access Token",
649
+ placeholder="hf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
650
+ type="password",
651
+ info="Tạo tại https://huggingface.co/settings/tokens (quyền Write).",
652
+ )
653
+ with gr.Row():
654
+ login_btn = gr.Button("Đăng nhập bằng token", variant="primary")
655
+ logout_btn = gr.Button("Xoá token đã dán")
656
+ login_status = gr.Markdown(login_badge())
657
+
658
+ # ---------------------- 2. Nguồn ----------------------
659
+ with gr.Accordion("📥 2. Dataset nguồn", open=True):
660
+ with gr.Row():
661
+ in_repo = gr.Textbox(
662
+ label="Input repo",
663
+ placeholder="vd: giangndm/my-dataset (hoặc org/ten-dataset)",
664
+ scale=2,
665
+ )
666
+ in_subset = gr.Textbox(
667
+ label="Subset (config)",
668
+ placeholder="để trống nếu không có",
669
+ scale=1,
670
+ )
671
+ with gr.Row():
672
+ in_split = gr.Textbox(label="Input split", value="train", scale=1)
673
+ in_column = gr.Textbox(label="Tên cột text cần trích xuất", placeholder="vd: text", scale=2)
674
+ check_btn = gr.Button("🔎 Kiểm tra dataset nguồn")
675
+ check_out = gr.Markdown()
676
+
677
+ with gr.Accordion("⚙️ Tuỳ chọn nâng cao (nguồn)", open=False):
678
+ with gr.Row():
679
+ in_revision = gr.Textbox(label="Revision / branch (tuỳ chọn)", placeholder="main")
680
+ out_column = gr.Textbox(
681
+ label="Đổi tên cột ở output (tuỳ chọn)",
682
+ placeholder="để trống = giữ nguyên",
683
+ )
684
+ with gr.Row():
685
+ max_rows = gr.Number(label="Giới hạn số dòng (0 = tất cả)", value=0, precision=0)
686
+ num_proc = gr.Number(label="num_proc (0/1 = mặc định)", value=0, precision=0)
687
+ streaming = gr.Checkbox(
688
+ label="Streaming (dataset rất lớn)",
689
+ value=False,
690
+ info="Không nạp hết vào RAM; không hỗ trợ chế độ nối.",
691
+ )
692
+
693
+ # ---------------------- 3. Đích ----------------------
694
+ with gr.Accordion("📤 3. Dataset đích", open=True):
695
+ out_repo = gr.Textbox(
696
+ label="Output repo",
697
+ placeholder="vd: ten-user/ten-dataset-moi (tự do — không bắt buộc namespace nào)",
698
+ info="Repo của bạn hoặc của organization mà bạn có quyền ghi. Sẽ được tạo nếu chưa tồn tại.",
699
+ )
700
+ with gr.Row():
701
+ out_config = gr.Textbox(label="Output config (subset)", value="default", scale=1)
702
+ out_split = gr.Textbox(label="Output split", placeholder="bấm Generate split name →", scale=2)
703
+ with gr.Row():
704
+ gen_btn = gr.Button("✨ Generate split name", variant="secondary")
705
+ use_input_repo_btn = gr.Button("↩️ Dùng lại Input repo", variant="secondary")
706
+ include_split_in_name = gr.Checkbox(
707
+ label="Thêm cả tên split gốc vào tên sinh ra", value=False, scale=1
708
+ )
709
+ gen_hint = gr.Markdown(
710
+ "Tên split chỉ gồm chữ, số, `_` (và dấu `.` phân cấp). Ví dụ: `my_dataset_vi_v2`."
711
+ )
712
+
713
+ with gr.Accordion("⚙️ Tuỳ chọn nâng cao (đích)", open=False):
714
+ out_private = gr.Checkbox(label="Tạo repo ở chế độ private", value=False)
715
+ push_mode = gr.Radio(
716
+ choices=list(PUSH_MODES.keys()),
717
+ value="Ghi đè split đã tồn tại",
718
+ label="Nếu split đã tồn tại",
719
+ )
720
+ set_default = gr.Checkbox(
721
+ label="Đặt config này làm config mặc định (set_default)",
722
+ value=True,
723
+ info="Chỉ áp dụng khi Output config khác 'default'.",
724
+ )
725
+ max_shard_size = gr.Textbox(label="max_shard_size", value="500MB")
726
+ commit_message = gr.Textbox(
727
+ label="Commit message (tuỳ chọn)", placeholder="để trống = tự sinh"
728
+ )
729
+
730
+ # ---------------------- 4. Chạy ----------------------
731
+ extract_btn = gr.Button("🚀 4. Trích xuất & đẩy lên Hub", variant="primary", elem_id="extract-btn")
732
+ result_status = gr.Markdown()
733
+ with gr.Accordion("📜 Nhật ký", open=True):
734
+ result_log = gr.Textbox(
735
+ label="",
736
+ lines=18,
737
+ max_lines=32,
738
+ elem_id="log-box",
739
+ autoscroll=True,
740
+ **COPY_BUTTON_KWARGS,
741
+ )
742
+ with gr.Accordion("👀 Xem trước 5 dòng đầu của cột đã trích xuất", open=False):
743
+ result_preview = gr.Dataframe(wrap=True, interactive=False)
744
+
745
+ gr.Markdown(
746
+ """
747
+ ---
748
+ **Ghi chú**
749
+ - Dữ liệu được ghi dạng Parquet: `data/<split>-00000-of-00001.parquet`
750
+ (hoặc `<config>/<split>-....parquet` nếu Output config khác `default`).
751
+ - Chế độ *Ghi đè* / *Nối thêm* sẽ **xoá file cũ của đúng split đó** trước khi đẩy → không bị trùng dữ liệu.
752
+ - Mọi split trong **cùng một config** phải có **cùng tên cột** (ràng buộc của `datasets`).
753
+ Nếu muốn đổi tên cột, hãy dùng một Output config khác.
754
+ - Token chỉ được giữ trong phiên làm việc của trình duyệt, không ghi ra file của Space.
755
+ """
756
+ )
757
+
758
+ # ---------------------- Sự kiện ----------------------
759
+ # Lưu ý: các tham số kiểu `gr.OAuthProfile` / `gr.OAuthToken` được Gradio tự
760
+ # động inject theo type-hint, KHÔNG được khai báo trong `inputs`.
761
+ demo.load(
762
+ on_page_load,
763
+ inputs=[session_token],
764
+ outputs=[session_token, login_status],
765
+ )
766
+ login_btn.click(
767
+ do_token_login,
768
+ inputs=[token_box, session_token],
769
+ outputs=[session_token, login_status],
770
+ ).then(lambda: "", outputs=[token_box])
771
+ logout_btn.click(do_logout, inputs=[session_token], outputs=[session_token, login_status])
772
+ check_btn.click(
773
+ inspect_source,
774
+ inputs=[session_token, in_repo, in_subset, in_column, in_revision],
775
+ outputs=[check_out],
776
+ )
777
+ gen_btn.click(
778
+ generate_split_name,
779
+ inputs=[in_repo, in_subset, in_split, include_split_in_name],
780
+ outputs=[out_split, gen_hint],
781
+ )
782
+ use_input_repo_btn.click(lambda repo: repo, inputs=[in_repo], outputs=[out_repo])
783
+ extract_btn.click(
784
+ extract_and_push,
785
+ inputs=[
786
+ session_token,
787
+ in_repo,
788
+ in_subset,
789
+ in_split,
790
+ in_column,
791
+ in_revision,
792
+ out_column,
793
+ max_rows,
794
+ num_proc,
795
+ streaming,
796
+ out_repo,
797
+ out_config,
798
+ out_split,
799
+ out_private,
800
+ push_mode,
801
+ set_default,
802
+ max_shard_size,
803
+ commit_message,
804
+ ],
805
+ outputs=[result_status, result_log, result_preview],
806
+ )
807
+
808
+
809
+ if __name__ == "__main__":
810
+ demo.queue(max_size=16, default_concurrency_limit=1).launch(show_error=True)
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # Thư viện đọc/ghi dataset trên Hugging Face Hub.
2
+ datasets>=3.0
3
+ huggingface_hub>=0.34
4
+
5
+ # Phụ thuộc cho OAuth (tương đương `gradio[oauth]`; gradio đã có sẵn trong image của Space).
6
+ authlib>=1.3
7
+ itsdangerous>=2.1