Spaces:
Running on Zero
Running on Zero
Add Text Column Extract Gradio app
Browse files- README.md +79 -5
- app.py +810 -0
- requirements.txt +7 -0
README.md
CHANGED
|
@@ -1,13 +1,87 @@
|
|
| 1 |
---
|
| 2 |
title: Text Column Extract
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|