data-contribute / app.py
lianghsun's picture
Update app.py
9ba4393
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}")