File size: 14,104 Bytes
1703267
 
 
 
 
 
 
 
 
 
 
 
9ba4393
1703267
9ba4393
1703267
 
 
 
 
 
 
 
 
9ba4393
1703267
 
 
 
 
 
 
 
 
6121bfa
1703267
 
 
 
6121bfa
 
1703267
 
 
 
6121bfa
1703267
 
 
 
6121bfa
1703267
 
6121bfa
 
1703267
6121bfa
 
1703267
 
 
 
6121bfa
 
 
 
 
1703267
 
6121bfa
 
 
 
1703267
 
 
6121bfa
1703267
 
 
6121bfa
 
 
 
 
 
1703267
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6121bfa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
760a2df
6121bfa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1703267
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
760a2df
1703267
 
 
760a2df
 
1703267
760a2df
 
 
 
 
 
 
 
 
 
1703267
760a2df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
import streamlit as st
import json
from io import StringIO
import requests
from PyPDF2 import PdfReader
from datetime import datetime, timezone, timedelta

st.set_page_config(page_title="資料上傳與檢查工具", layout="wide")

BACKEND_URL = st.secrets.get("BACKEND_URL", None)

st.title("🌟 協助貢獻繁體中文資料")
st.markdown("專案路徑: [tw-sharegpt](https://huggingface.co/datasets/lianghsun/tw-sharegpt)")
st.markdown("""
歡迎加入我們,一起建立高品質的 **繁體中文語言資料集**!你提供的每一份資料,都能幫助未來的繁中模型更準確、更理解本地語境。我們非常感謝你的協助,你的貢獻將直接推動繁體中文 AI 生態的發展!🌱
""")

st.info("⚠️ 請勿上傳真實個資或敏感商業資料。")

# ---- 本次上傳的共同設定(兩個 tab 共用) ----
st.markdown("### 本次上傳設定")
contributor_email = st.text_input("聯絡 email(選填)", placeholder="example@email.com")
share_permission = st.checkbox(
    "我同意將本次上傳的資料,未來在去識別化後以開源形式提供研究與模型訓練使用。",
    value=True,
)

# 產生 UTC+8 的上傳時間(每次互動當下)
tz_utc8 = timezone(timedelta(hours=8))
uploaded_at = datetime.now(tz_utc8).isoformat()

tab_jsonl, tab_pdf = st.tabs(["對話資料 (.jsonl)", "預訓練 PDF"])


# ---------- Tab 1: JSONL ----------
# ---------- Tab 1: JSONL ----------
with tab_jsonl:
    st.subheader("上傳對話資料")

    sample_prompt = """
    請將我們上述對話的內容(但不包含本問題),整理成 OpenAI Messages Format,輸出格式必須是 .jsonl。

格式要求:

- 每一行是一個獨立的 JSON 物件。
- 每個 JSON 物件必須包含一個 messages 欄位。
- 不要在檔案中加入註解或說明文字,每一行只能是 JSON。

範例(僅供格式參考):

[{"messages": [
  {"role": "system", "content": "你是一個友善的客服人員。"},
  {"role": "user", "content": "請問我要如何申請退貨?"},
  {"role": "assistant", "content": "您好,若您要申請退貨,請先登入會員中心,在「訂單管理」中選擇欲退貨的訂單,點選「申請退貨」,依指示填寫原因並送出。"},  {"role": "user", "content": "..."},
  {"role": "assistant", "content": "..."},]

請依照以上規格輸出一筆 .jsonl 對話資料(保持為一列,好讓我可以方便貼上),並用 markdown 表示。
    """

    st.markdown("##### 請將以下的 prompt 貼到你的對話生成模型中,產生符合格式的對話資料:")
    st.code(sample_prompt, language="markdown")

    st.markdown("#### 選擇輸入方式")
    input_mode = st.radio(
        "選擇要如何提供 `.jsonl` 內容",
        ["上傳檔案", "貼上文字"],
        horizontal=True,
    )

    # 共用的檢查函式:給「檔案模式」和「貼上模式」共用
    def validate_jsonl_lines(lines):
        """lines: list[str] (每一行一個 JSON) → 回傳 (parsed_objs, errors)"""
        parsed = []
        errors = []
        allowed_roles = {"system", "user", "assistant"}

        for idx, line in enumerate(lines, start=1):
            line = line.strip()
            if not line:
                continue
            # 如果使用者是從 ChatGPT 貼出來的,有可能含 ``` 之類的標記,先跳過
            if line.startswith("```") and line.endswith("```"):
                continue
            if line.startswith("```") or line == "```":
                continue

            try:
                obj = json.loads(line)
            except json.JSONDecodeError as e:
                errors.append(f"第 {idx} 行不是合法 JSON:{e}")
                continue

            if "messages" not in obj or not isinstance(obj["messages"], list):
                errors.append(f"第 {idx} 行缺少 messages 欄位或型態錯誤。")
                continue

            for m_idx, msg in enumerate(obj["messages"]):
                if not isinstance(msg, dict):
                    errors.append(f"第 {idx} 行第 {m_idx+1} 則訊息不是物件。")
                    continue
                role = msg.get("role")
                msg_content = msg.get("content")
                if role not in allowed_roles:
                    errors.append(f"第 {idx} 行第 {m_idx+1} 則 role 非預期:{role}")
                if not isinstance(msg_content, str):
                    errors.append(f"第 {idx} 行第 {m_idx+1} 則 content 需為字串。")

            parsed.append(obj)

        return parsed, errors

    # ---------- 模式 A:上傳檔案 ----------
    if input_mode == "上傳檔案":
        jsonl_file = st.file_uploader(
            "上傳對話資料 `.jsonl` 檔",
            type=["jsonl"],
            accept_multiple_files=False
        )

        file_jsonl_valid = False
        file_parsed_lines = []

        if jsonl_file is not None:
            st.markdown("#### 檔案檢查結果")
            content = jsonl_file.read().decode("utf-8")
            lines = content.splitlines()

            file_parsed_lines, errors = validate_jsonl_lines(lines)

            if errors:
                st.error("格式檢查失敗,請修正後重新上傳:")
                for e in errors[:20]:
                    st.write("- " + e)
                if len(errors) > 20:
                    st.write(f"... 還有 {len(errors) - 20} 筆錯誤未顯示")
                st.info("若多次調整仍無法通過檢查,建議先在本地編輯好 `.jsonl` 檔,再重新上傳。")
            else:
                file_jsonl_valid = True
                st.success(f"檢查通過!共 {len(file_parsed_lines)} 筆對話。")
                st.markdown("#### 範例預覽(前 2 筆)")
                for i, obj in enumerate(file_parsed_lines[:2], start=1):
                    st.json(obj)

        if st.button("上傳對話資料(檔案)", disabled=not (BACKEND_URL and file_jsonl_valid)):
            if BACKEND_URL is None:
                st.warning("尚未設定 BACKEND_URL,無法實際送出,請在 `st.secrets` 中配置。")
            else:
                with st.spinner("正在上傳對話資料並檢查,請稍候…"):
                    meta = {
                        "uploaded_at": uploaded_at,  # UTC+8 ISO 字串
                        "contributor_email": contributor_email if contributor_email.strip() else None,
                        "share_permission": bool(share_permission),
                    }

                    enriched_lines = []
                    for obj in file_parsed_lines:
                        obj_with_meta = {**obj, "metadata": meta}
                        enriched_lines.append(json.dumps(obj_with_meta, ensure_ascii=False))

                    payload = "\n".join(enriched_lines).encode("utf-8")
                    files = {"file": ("contrib.jsonl", payload, "application/jsonl")}

                    try:
                        resp = requests.post(f"{BACKEND_URL}/upload-jsonl", files=files)
                        if resp.ok:
                            st.success("已成功送交後端伺服器,等待後端進一步檢查與處理。")
                        else:
                            st.error(f"後端回傳錯誤:{resp.status_code} {resp.text}")
                    except Exception as e:
                        st.error(f"送出時發生錯誤:{e}")

    # ---------- 模式 B:貼上文字 ----------
    else:
        st.markdown("請將 `.jsonl` 內容貼在下方,每一行必須是一個 JSON 物件:")
        pasted_text = st.text_area(
            "貼上 `.jsonl` 內容",
            placeholder='例如:\n{"messages": [...]}',
            height=240,
        )

        pasted_jsonl_valid = False
        pasted_parsed_lines = []

        if pasted_text.strip():
            st.markdown("#### 貼上內容檢查結果")
            lines = pasted_text.splitlines()
            pasted_parsed_lines, errors = validate_jsonl_lines(lines)

            if errors:
                st.error("格式檢查失敗,請依錯誤訊息調整貼上的內容:")
                for e in errors[:20]:
                    st.write("- " + e)
                if len(errors) > 20:
                    st.write(f"... 還有 {len(errors) - 20} 筆錯誤未顯示")
                st.info("若多次調整仍無法通過檢查,建議先在本地編輯好 `.jsonl` 檔案,再使用「上傳檔案」模式上傳。")
            else:
                pasted_jsonl_valid = True
                st.success(f"檢查通過!共 {len(pasted_parsed_lines)} 筆對話。")
                st.markdown("#### 範例預覽(前 2 筆)")
                for i, obj in enumerate(pasted_parsed_lines[:2], start=1):
                    st.json(obj)

        if st.button("上傳對話資料(貼上內容)", disabled=not (BACKEND_URL and pasted_jsonl_valid)):
            if BACKEND_URL is None:
                st.warning("尚未設定 BACKEND_URL,無法實際送出,請在 `st.secrets` 中配置。")
            else:
                with st.spinner("正在上傳貼上內容並檢查,請稍候…"):
                    meta = {
                        "uploaded_at": uploaded_at,  # UTC+8 ISO 字串
                        "contributor_email": contributor_email if contributor_email.strip() else None,
                        "share_permission": bool(share_permission),
                    }

                    enriched_lines = []
                    for obj in pasted_parsed_lines:
                        obj_with_meta = {**obj, "metadata": meta}
                        enriched_lines.append(json.dumps(obj_with_meta, ensure_ascii=False))

                    payload = "\n".join(enriched_lines).encode("utf-8")
                    files = {"file": ("contrib_pasted.jsonl", payload, "application/jsonl")}

                    try:
                        resp = requests.post(f"{BACKEND_URL}/upload-jsonl", files=files)
                        if resp.ok:
                            st.success("已成功送交後端伺服器,等待後端進一步檢查與處理。")
                        else:
                            st.error(f"後端回傳錯誤:{resp.status_code} {resp.text}")
                    except Exception as e:
                        st.error(f"送出時發生錯誤:{e}")


# ---------- Tab 2: PDF ----------
with tab_pdf:
    st.subheader("上傳預訓練 PDF(純文字型)")
    st.markdown("""
**格式說明**

- 檔案副檔名:`.pdf`
- 內容須為可擷取文字的 PDF(非掃描圖片)。
- 系統會抽樣頁面檢查是否能讀取到足夠文字內容。
""")

    pdf_files = st.file_uploader(
        "上傳一個或多個 PDF 檔",
        type=["pdf"],
        accept_multiple_files=True
    )

    pdf_results = []

    if pdf_files:
        for pdf in pdf_files:
            st.markdown(f"#### 檢查檔案:`{pdf.name}`")
            try:
                reader = PdfReader(pdf)
                num_pages = len(reader.pages)
                sample_pages = [0, 2, 4]  # 第 1,3,5 頁(若存在)
                text_snippets = []

                for p in sample_pages:
                    if p < num_pages:
                        page = reader.pages[p]
                        text = page.extract_text() or ""
                        text_snippets.append(text)

                total_text = "".join(text_snippets)
                text_len = len(total_text)

                if text_len < 100:
                    st.warning(f"未擷取到足夠文字內容(抽樣字數 {text_len})。此 PDF 可能是掃描型,建議先做 OCR。")
                else:
                    st.success(f"檢查通過!頁數:{num_pages},抽樣字數:{text_len}")
                    st.markdown("**文字預覽(前 300 字)**")
                    st.text(total_text[:300])

                pdf_results.append((pdf, text_len))

            except Exception as e:
                st.error(f"讀取 PDF 時發生錯誤:{e}")

    any_valid_pdf = any(tlen >= 100 for _, tlen in pdf_results) if pdf_results else False

    if st.button("上傳 PDF 檔案", disabled=not (pdf_files and any_valid_pdf or BACKEND_URL is None)):
        if BACKEND_URL is None:
            st.warning("尚未設定 BACKEND_URL,無法實際送出,請在 `st.secrets` 中配置。")
        else:
            if not pdf_results:
                st.warning("沒有可上傳的 PDF 檔案。")
            else:
                with st.spinner("正在上傳 PDF 並進行檢查,請稍候…"):
                    files = []
                    for pdf, text_len in pdf_results:
                        if text_len < 100:
                            continue  # 跳過疑似掃描檔
                        pdf.seek(0)
                        files.append(("files", (pdf.name, pdf.getvalue(), "application/pdf")))

                    if not files:
                        st.warning("沒有通過文字檢查的 PDF 檔案可送出。")
                    else:
                        # PDF 部分的 metadata 用 form data 一起送出,讓後端可以記錄
                        meta = {
                            "uploaded_at": uploaded_at,
                            "contributor_email": contributor_email if contributor_email.strip() else "",
                            "share_permission": json.dumps(bool(share_permission)),
                        }

                        try:
                            resp = requests.post(f"{BACKEND_URL}/upload-pdf", files=files, data=meta)
                            if resp.ok:
                                st.success("已成功送交後端伺服器,等待後端進一步檢查與處理。")
                            else:
                                st.error(f"後端回傳錯誤:{resp.status_code} {resp.text}")
                        except Exception as e:
                            st.error(f"送出時發生錯誤:{e}")