Spaces:
Sleeping
Sleeping
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}") |