chuho commited on
Commit
43b8331
·
verified ·
1 Parent(s): 5dac453
Files changed (1) hide show
  1. app.py +719 -103
app.py CHANGED
@@ -1,127 +1,743 @@
1
- import gradio as gr
2
- import json
3
- import requests
4
- import time
5
- import datetime
6
- import pytz
7
- import csv
8
- import traceback
9
- import threading
10
  import os
 
11
 
12
- OUTPUT_FILE = "/data/data3.csv"
13
- status_logs = ["⏳ 系統啟動,等待第一次抓取資料..."]
 
 
14
 
15
- def update_log(message):
16
- global status_logs
17
- status_logs.append(message)
18
- if len(status_logs) > 15:
19
- status_logs = status_logs[-15:]
20
-
21
- def fetch_and_append_data():
22
- url = "https://www.chinamoney.com.cn/r/cms/www/chinamoney/data/fx/rfx-sp-quot.json"
23
- beijing_tz = pytz.timezone('Asia/Shanghai')
24
- current_time = datetime.datetime.now(beijing_tz).strftime('%Y-%m-%d %H:%M:%S')
25
-
26
- try:
27
- dir_name = os.path.dirname(OUTPUT_FILE)
28
- if dir_name and not os.path.exists(dir_name):
29
- os.makedirs(dir_name, exist_ok=True)
30
 
31
- headers = {
32
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  }
34
- response = requests.get(url, headers=headers)
35
- response.raise_for_status()
36
- data = response.json()
37
 
38
- items = data.get('records', [])
39
- if not items:
40
- update_log(f"[{current_time}] ⚠️ 抓取成功,但找不到 records 資料。")
41
- return
 
 
 
 
 
 
 
 
 
 
 
42
 
43
- headers_csv = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  try:
45
- with open(OUTPUT_FILE, 'r', newline='', encoding='utf-8') as f:
46
- reader = csv.reader(f)
47
- first_row = next(reader, None)
48
- if first_row:
49
- headers_csv = first_row
50
- except (FileNotFoundError, StopIteration):
51
- pass
52
-
53
- with open(OUTPUT_FILE, 'a', newline='', encoding='utf-8') as csvfile:
54
- writer = csv.writer(csvfile)
55
-
56
- if not headers_csv:
57
- if items and isinstance(items[0], dict):
58
- headers_csv = list(items[0].keys()) + ['timestamp']
59
- writer.writerow(headers_csv)
 
 
 
 
60
  else:
61
- update_log(f"[{current_time}] ❌ 無法判斷資料結構,跳過寫入。")
62
- return
63
 
64
- for item in items:
65
- if isinstance(item, dict):
66
- row = [item.get(header, '') for header in headers_csv[:-1]]
67
- row.append(current_time)
68
- writer.writerow(row)
69
 
70
- update_log(f"[{current_time}] ✅ 成功抓取並寫入 {len(items)} 筆資料!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
- except requests.exceptions.RequestException as e:
73
- update_log(f"[{current_time}] ❌ 網路請求錯誤: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  except Exception as e:
75
- update_log(f"[{current_time}] ❌ 寫入或執行失敗: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
- def background_task():
78
- while True:
79
- fetch_and_append_data()
80
- time.sleep(300)
 
 
 
 
81
 
82
- thread = threading.Thread(target=background_task, daemon=True)
83
- thread.start()
 
 
 
 
 
 
84
 
85
- def refresh_all_data():
86
- logs = "\n".join(status_logs)
87
- file_exists = os.path.exists(OUTPUT_FILE) and os.path.isfile(OUTPUT_FILE)
 
 
 
 
 
88
 
89
- # 優化:如果檔案不存在回傳 None,避免 Gradio 讀取空值崩潰
90
- file_output = OUTPUT_FILE if file_exists else None
 
 
 
 
91
 
92
- preview = "檔案尚未建立或尚無資料..."
93
- if file_exists:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  try:
95
- with open(OUTPUT_FILE, 'r', encoding='utf-8') as f:
96
- lines = f.readlines()
97
- if len(lines) <= 10:
98
- preview = "".join(lines)
 
 
 
 
 
 
 
99
  else:
100
- preview = lines[0] + "...\n(中間省略)\n...\n" + "".join(lines[-10:])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  except Exception as e:
102
- preview = f"讀取預覽檔案時失敗: {e}"
103
-
104
- return logs, file_output, preview
105
 
106
- with gr.Blocks(title="資料自動抓取工具") as demo:
107
- gr.Markdown("# 📈 外匯報價自動抓取系統")
108
- gr.Markdown("此程式會在背景 **每 5 分鐘** 自動向目標網站請求資料,並寫入 CSV。")
 
 
109
 
110
- with gr.Row():
111
- with gr.Column():
112
- log_box = gr.Textbox(label="後台執行狀態", lines=10, value="\n".join(status_logs), interactive=False)
113
- refresh_btn = gr.Button("🔄 刷新狀態與資料", variant="primary")
 
 
 
 
 
114
 
115
- with gr.Column():
116
- download_btn = gr.File(label="下載當前的 CSV 檔案")
117
- preview_box = gr.Textbox(label="資料預覽 (顯示最後幾筆)", lines=10, value="點擊刷新按鈕載入預覽...", interactive=False)
118
-
119
- refresh_btn.click(
120
- fn=refresh_all_data,
121
- inputs=None,
122
- outputs=[log_box, download_btn, preview_box]
123
- )
124
-
125
- if __name__ == "__main__":
126
- # 關鍵修復:加入 allowed_paths=["/data"],允許 Gradio 提供 Storage Bucket 裡的檔案給使用者下載
127
- demo.launch(allowed_paths=["/data"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
+ import sys
3
 
4
+ if "STREAMLIT_IS_RUNNING" not in os.environ:
5
+ import subprocess
6
+ import locale
7
+ import streamlit.web.cli as stcli
8
 
9
+ def get_locale_lang():
10
+ try:
11
+ import ctypes
12
+ windll = ctypes.windll.kernel32
13
+ loc = locale.windows_locale.get(windll.GetUserDefaultUILanguage(), 'zh-TW')
14
+ if not loc: return 'zh-TW'
15
+ loc = loc.lower()
16
+ if 'zh_cn' in loc or 'zh_sg' in loc or 'zh-cn' in loc: return 'zh-CN'
17
+ elif 'en' in loc: return 'en'
18
+ return 'zh-TW'
19
+ except:
20
+ return 'zh-TW'
 
 
 
21
 
22
+ PROMPTS = {
23
+ 'zh-TW': {
24
+ "ask": "請選擇操作 (輸入數字並按 Enter):\n1: 運行網頁應用程式 (啟動 Streamlit)\n2: 編譯成 EXE 執行檔\n請選擇 (1/2): ",
25
+ "err": "無效輸入,請輸入 1 或 2。",
26
+ "compiling": "開始編譯成 EXE...",
27
+ "success": "編譯完成!您可以在 'dist' 資料夾中找到您的應用程式。"
28
+ },
29
+ 'zh-CN': {
30
+ "ask": "请选择操作 (输入数字并按 Enter):\n1: 运行网页应用程序 (启动 Streamlit)\n2: 编译成 EXE 可执行文件\n请选择 (1/2): ",
31
+ "err": "无效输入,请输入 1 或 2。",
32
+ "compiling": "开始编译成 EXE...",
33
+ "success": "编译完成!您可以在 'dist' 文件夹中找到您的应用程序。"
34
+ },
35
+ 'en': {
36
+ "ask": "Please select an action (Enter a number):\n1: Run Application (Start Streamlit)\n2: Compile to EXE\nSelect (1/2): ",
37
+ "err": "Invalid input. Please enter 1 or 2.",
38
+ "compiling": "Starting compilation to EXE...",
39
+ "success": "Compilation complete! You can find your app in the 'dist' folder."
40
  }
41
+ }
 
 
42
 
43
+ def run_app():
44
+ os.environ["STREAMLIT_IS_RUNNING"] = "1"
45
+ script_path = os.path.abspath(__file__)
46
+ if getattr(sys, 'frozen', False):
47
+ script_path = os.path.join(sys._MEIPASS, os.path.basename(__file__))
48
+
49
+ sys.argv = [
50
+ "streamlit",
51
+ "run",
52
+ script_path,
53
+ "--server.headless=false",
54
+ "--browser.gatherUsageStats=false",
55
+ "--server.maxUploadSize=2000"
56
+ ]
57
+ sys.exit(stcli.main())
58
 
59
+ def compile_exe(lang_code):
60
+ import streamlit
61
+ p = PROMPTS[lang_code]
62
+ print(f"\n{p['compiling']}\n")
63
+
64
+ st_dir = os.path.dirname(streamlit.__file__)
65
+ sep = ';' if os.name == 'nt' else ':'
66
+ script_path = os.path.abspath(__file__)
67
+
68
+ cmd = [
69
+ sys.executable, "-m", "PyInstaller",
70
+ "--noconfirm",
71
+ "--onedir",
72
+ "--windowed",
73
+ f"--add-data={st_dir}{sep}streamlit",
74
+ f"--add-data={script_path}{sep}.",
75
+ "--hidden-import=chromadb",
76
+ "--hidden-import=faiss",
77
+ "--hidden-import=pandas",
78
+ "--hidden-import=numpy",
79
+ "--hidden-import=streamlit_javascript",
80
+ "--hidden-import=sqlalchemy",
81
+ "--hidden-import=pymysql",
82
+ "--hidden-import=psycopg2",
83
+ "--hidden-import=sentence_transformers",
84
+ script_path
85
+ ]
86
+
87
  try:
88
+ subprocess.run(cmd, check=True)
89
+ print(f"\n{p['success']}\n")
90
+ except subprocess.CalledProcessError as e:
91
+ print(f"\nError during compilation: {e}\n")
92
+
93
+ if __name__ == "__main__":
94
+ if getattr(sys, 'frozen', False):
95
+ run_app()
96
+ else:
97
+ lang = get_locale_lang()
98
+ p = PROMPTS[lang]
99
+ while True:
100
+ choice = input(p["ask"]).strip()
101
+ if choice == '1':
102
+ run_app()
103
+ break
104
+ elif choice == '2':
105
+ compile_exe(lang)
106
+ break
107
  else:
108
+ print(f"\n{p['err']}\n")
109
+ sys.exit(0)
110
 
111
+ # ====================================================================
112
+ # STREAMLIT APPLICATION CODE BELOW
113
+ # ====================================================================
 
 
114
 
115
+ import streamlit as st
116
+
117
+ st.set_page_config(page_title="VectorDB Converter", layout="wide")
118
+
119
+ st.markdown('''
120
+ <style>
121
+ /* Pull the main container up to eliminate the huge blank space */
122
+ div[data-testid="stAppViewBlockContainer"] {
123
+ padding-top: 2rem !important;
124
+ }
125
+ .block-container {
126
+ padding-top: 2rem !important;
127
+ }
128
+
129
+ /* Prevent the 'Deploy' header from covering our content */
130
+ header[data-testid="stHeader"] {
131
+ background-color: transparent;
132
+ z-index: 998;
133
+ }
134
+
135
+ /* Make the Tabs sticky at the top */
136
+ div[data-testid="stTabs"] > div[data-baseweb="tab-list"] {
137
+ position: sticky;
138
+ top: 2rem;
139
+ background-color: white;
140
+ z-index: 999;
141
+ padding-top: 15px;
142
+ padding-bottom: 10px;
143
+ border-bottom: 1px solid #f0f2f6;
144
+ }
145
+
146
+ /* Dark mode background fix for sticky tabs */
147
+ @media (prefers-color-scheme: dark) {
148
+ div[data-testid="stTabs"] > div[data-baseweb="tab-list"] {
149
+ background-color: #0e1117;
150
+ border-bottom: 1px solid #333;
151
+ }
152
+ }
153
+ </style>
154
+ ''', unsafe_allow_html=True)
155
 
156
+ import pandas as pd
157
+ import numpy as np
158
+ import chromadb
159
+ import faiss
160
+ import os
161
+ import io
162
+ import shutil
163
+ import zipfile
164
+ import uuid
165
+ from chromadb.utils import embedding_functions
166
+ from streamlit_javascript import st_javascript
167
+
168
+ TRANSLATIONS = {
169
+ "zh-TW": {
170
+ "page_title": "向量資料庫轉換器",
171
+ "title": "向量資料庫轉換器:表格 ↔ 向量庫",
172
+ "tab_table_to_db": "表格轉向量庫",
173
+ "tab_db_to_table": "向量庫轉表格",
174
+ "header_tab1": "將 CSV/Excel 轉換為向量資料庫",
175
+ "upload_file": "上傳 CSV 或 Excel 檔案",
176
+ "select_sheet": "選擇 Excel 工作表:",
177
+ "data_preview": "資料預覽",
178
+ "total_rows": "總行數",
179
+ "total_cols": "總欄數",
180
+ "scroll_down": "在表格內向下捲動以檢視所有資料。",
181
+ "select_columns": "選擇一個或多個要轉換為向量的文字欄位:",
182
+ "select_db": "選擇目標向量資料庫:",
183
+ "btn_convert": "轉換為向量資料庫",
184
+ "warn_select_col": "請至少選擇一個要轉換的欄位。",
185
+ "generating": "正在產生向量並儲存至",
186
+ "initializing": "初始化中...",
187
+ "saving_chroma": "正在儲存至 ChromaDB...",
188
+ "saving_faiss": "正在儲存索引與元數據至 FAISS...",
189
+ "zipping": "正在壓縮輸出檔案...",
190
+ "download_chroma": "下載 ChromaDB (ZIP)",
191
+ "download_faiss": "下載 FAISS DB (ZIP)",
192
+ "success_chroma": "成功轉換為 ChromaDB!",
193
+ "success_faiss": "成功轉換為 FAISS!",
194
+ "header_tab2": "將向量資料庫轉換為 CSV/Excel",
195
+ "source_db": "選擇來源向量資料庫:",
196
+ "upload_zip_info": "上傳包含您的 {} 檔案的 ZIP 壓縮包。",
197
+ "upload_zip_btn": "上傳資料庫 ZIP 檔案",
198
+ "no_collections": "在此 ChromaDB 中找不到任何集合。",
199
+ "select_collection": "選擇集合:",
200
+ "extract_data": "提取資料",
201
+ "fail_read": "無法讀取 ChromaDB:",
202
+ "no_index": "在上傳的 ZIP 中找不到 .index 檔案。",
203
+ "select_index": "選擇 FAISS 索引:",
204
+ "extracted_preview": "提取資料預覽",
205
+ "download_csv": "下載為 CSV",
206
+ "download_excel": "下載為 Excel",
207
+ "error_processing": "處理向量資料庫時發生錯誤:",
208
+ "generating_progress": "正在產生向量:已處理 {} / {} 行",
209
+ "error_read_convert": "讀取檔案或轉換時發生錯誤:",
210
+ "data_source": "資料來源:",
211
+ "file_upload": "檔案上傳 (CSV/Excel)",
212
+ "sql_db": "SQL 資料庫",
213
+ "db_uri": "資料庫連線字串 (URI)",
214
+ "db_uri_help": "範例:sqlite:///my_database.db 或 mysql+pymysql://user:pass@localhost/dbname",
215
+ "connect_db": "連線",
216
+ "select_table": "選擇資料表:",
217
+ "export_to_sql": "匯出至 SQL 資料庫",
218
+ "target_table": "目標資料表名稱",
219
+ "save_to_db": "儲存至資料庫",
220
+ "db_success": "成功儲存至資料庫!"
221
+ },
222
+ "zh-CN": {
223
+ "page_title": "向量数据库转换器",
224
+ "title": "向量数据库转换器:表格 ↔ 向量库",
225
+ "tab_table_to_db": "表格转向量库",
226
+ "tab_db_to_table": "向量库转表格",
227
+ "header_tab1": "将 CSV/Excel 转换为向量数据库",
228
+ "upload_file": "上传 CSV 或 Excel 文件",
229
+ "select_sheet": "选择 Excel 工作表:",
230
+ "data_preview": "数据预览",
231
+ "total_rows": "总行数",
232
+ "total_cols": "总列数",
233
+ "scroll_down": "在表格内向下滚动以查看所有数据。",
234
+ "select_columns": "选择一个或多个要转换为向量的文本字段:",
235
+ "select_db": "选择目标向量数据库:",
236
+ "btn_convert": "转换为向量数据库",
237
+ "warn_select_col": "请至少选择一个要转换的字段。",
238
+ "generating": "正在生成向量并保存至",
239
+ "initializing": "初始化中...",
240
+ "saving_chroma": "正在保存至 ChromaDB...",
241
+ "saving_faiss": "正在保存索引与元数据至 FAISS...",
242
+ "zipping": "正在压缩输出文件...",
243
+ "download_chroma": "下载 ChromaDB (ZIP)",
244
+ "download_faiss": "下载 FAISS DB (ZIP)",
245
+ "success_chroma": "成功转换为 ChromaDB!",
246
+ "success_faiss": "成功转换为 FAISS!",
247
+ "header_tab2": "将向量数据库转换为 CSV/Excel",
248
+ "source_db": "选择来源向量数据库:",
249
+ "upload_zip_info": "上传包含您的 {} 文件的 ZIP 压缩包。",
250
+ "upload_zip_btn": "上传数据库 ZIP 文件",
251
+ "no_collections": "在此 ChromaDB 中找不到任何集合。",
252
+ "select_collection": "选择集合:",
253
+ "extract_data": "提取数据",
254
+ "fail_read": "无法读取 ChromaDB:",
255
+ "no_index": "在上传的 ZIP 中找不到 .index 文件。",
256
+ "select_index": "选择 FAISS 索引:",
257
+ "extracted_preview": "提取数据预览",
258
+ "download_csv": "下载为 CSV",
259
+ "download_excel": "下载为 Excel",
260
+ "error_processing": "处理向量数据库时发生错误:",
261
+ "generating_progress": "正在生成向量:已处理 {} / {} 行",
262
+ "error_read_convert": "读取文件或转换时发生错误:",
263
+ "data_source": "数据来源:",
264
+ "file_upload": "文件上传 (CSV/Excel)",
265
+ "sql_db": "SQL 数据库",
266
+ "db_uri": "数据库连接字符串 (URI)",
267
+ "db_uri_help": "示例:sqlite:///my_database.db 或 mysql+pymysql://user:pass@localhost/dbname",
268
+ "connect_db": "连接",
269
+ "select_table": "选择数据表:",
270
+ "export_to_sql": "导出至 SQL 数据库",
271
+ "target_table": "目标数据表名称",
272
+ "save_to_db": "保存至数据库",
273
+ "db_success": "成功保存至数据库!"
274
+ },
275
+ "en": {
276
+ "page_title": "VectorDB Converter",
277
+ "title": "VectorDB Converter: Table ↔ Vector Database",
278
+ "tab_table_to_db": "Table to Vector DB",
279
+ "tab_db_to_table": "Vector DB to Table",
280
+ "header_tab1": "Convert CSV/Excel to Vector Database",
281
+ "upload_file": "Upload CSV or Excel file",
282
+ "select_sheet": "Select Excel Sheet:",
283
+ "data_preview": "Data Preview",
284
+ "total_rows": "Total rows",
285
+ "total_cols": "Total columns",
286
+ "scroll_down": "Scroll down inside the table to see all rows.",
287
+ "select_columns": "Select one or more columns containing text to embed:",
288
+ "select_db": "Select Target Vector Database:",
289
+ "btn_convert": "Convert to Vector DB",
290
+ "warn_select_col": "Please select at least one column to embed.",
291
+ "generating": "Generating embeddings and saving to",
292
+ "initializing": "Initializing...",
293
+ "saving_chroma": "Saving to ChromaDB...",
294
+ "saving_faiss": "Saving index and metadata to FAISS...",
295
+ "zipping": "Zipping output...",
296
+ "download_chroma": "Download ChromaDB (ZIP)",
297
+ "download_faiss": "Download FAISS DB (ZIP)",
298
+ "success_chroma": "Successfully converted to ChromaDB!",
299
+ "success_faiss": "Successfully converted to FAISS!",
300
+ "header_tab2": "Convert Vector Database to CSV/Excel",
301
+ "source_db": "Select Source Vector Database:",
302
+ "upload_zip_info": "Upload the ZIP file containing your {} files.",
303
+ "upload_zip_btn": "Upload DB ZIP file",
304
+ "no_collections": "No collections found in this ChromaDB.",
305
+ "select_collection": "Select Collection:",
306
+ "extract_data": "Extract Data",
307
+ "fail_read": "Failed to read ChromaDB:",
308
+ "no_index": "No .index file found in the uploaded ZIP.",
309
+ "select_index": "Select FAISS index:",
310
+ "extracted_preview": "Extracted Data Preview",
311
+ "download_csv": "Download as CSV",
312
+ "download_excel": "Download as Excel",
313
+ "error_processing": "Error processing the Vector DB:",
314
+ "generating_progress": "Generating embeddings: {} / {} rows processed",
315
+ "error_read_convert": "Error reading file or converting:",
316
+ "data_source": "Data Source:",
317
+ "file_upload": "File Upload (CSV/Excel)",
318
+ "sql_db": "SQL Database",
319
+ "db_uri": "Database Connection URI",
320
+ "db_uri_help": "Example: sqlite:///my_database.db or mysql+pymysql://user:pass@localhost/dbname",
321
+ "connect_db": "Connect",
322
+ "select_table": "Select Table:",
323
+ "export_to_sql": "Export to SQL Database",
324
+ "target_table": "Target Table Name",
325
+ "save_to_db": "Save to Database",
326
+ "db_success": "Successfully saved to Database!"
327
+ }
328
+ }
329
+
330
+ # Determine language
331
+ try:
332
+ lang_str = st_javascript("window.navigator.userLanguage || window.navigator.language;")
333
+ if not lang_str or lang_str == 0:
334
+ lang_str = "zh-TW"
335
+ except:
336
+ lang_str = "zh-TW"
337
+
338
+ lang_str = str(lang_str).lower()
339
+ if "zh-cn" in lang_str or "zh-hans" in lang_str:
340
+ lang_code = "zh-CN"
341
+ elif "en" in lang_str:
342
+ lang_code = "en"
343
+ else:
344
+ lang_code = "zh-TW"
345
+
346
+ t = TRANSLATIONS[lang_code]
347
+
348
+ # Initialize embedding model globally
349
+ default_ef = embedding_functions.DefaultEmbeddingFunction()
350
+
351
+ def get_embeddings_batched(texts, progress_bar=None, log_func=None):
352
+ batch_size = 32
353
+ all_embeddings = []
354
+ total = len(texts)
355
+ if log_func: log_func(f"Starting batch embedding generation (Total rows: {total}, Batch size: {batch_size})")
356
+ for i in range(0, total, batch_size):
357
+ batch = texts[i:i+batch_size]
358
+ emb = default_ef(batch)
359
+ all_embeddings.extend(emb)
360
+ if progress_bar is not None:
361
+ progress = min(1.0, (i + len(batch)) / total)
362
+ progress_bar.progress(progress, text=t.get("generating_progress", "Progress: {}/{}").format(i + len(batch), total))
363
+ if log_func: log_func(f"Processed embeddings: {min(i + len(batch), total)} / {total}")
364
+ if log_func: log_func("Embeddings generation completed successfully.")
365
+ return np.array(all_embeddings, dtype=np.float32)
366
+
367
+ def table_to_chroma(df, db_path, collection_name, text_columns, progress_bar=None, log_func=None):
368
+ if log_func: log_func(f"Initializing ChromaDB client at: {db_path}")
369
+ try:
370
+ client = chromadb.PersistentClient(path=db_path)
371
+ collection = client.get_or_create_collection(name=collection_name)
372
  except Exception as e:
373
+ if log_func: log_func(f"ERROR initializing ChromaDB: {e}")
374
+ raise e
375
+
376
+ if log_func: log_func("Preparing text data for embedding...")
377
+ texts = []
378
+ for _, row in df.iterrows():
379
+ parts = [f"{col}: {row[col]}" for col in text_columns if pd.notna(row[col])]
380
+ texts.append("\n".join(parts) if parts else "")
381
+
382
+ embeddings = get_embeddings_batched(texts, progress_bar, log_func)
383
+
384
+ if log_func: log_func("Preparing metadata payload...")
385
+ if progress_bar is not None:
386
+ progress_bar.progress(1.0, text=t.get("saving_chroma", "Saving to ChromaDB..."))
387
+
388
+ metadatas = []
389
+ for _, row in df.iterrows():
390
+ meta = {}
391
+ for col in df.columns:
392
+ val = row[col]
393
+ if pd.isna(val): val = ""
394
+ elif not isinstance(val, (str, int, float, bool)): val = str(val)
395
+ meta[col] = val
396
+ metadatas.append(meta)
397
+ ids = [str(i) for i in range(len(df))]
398
+
399
+ batch_size = 200
400
+ total = len(df)
401
+ if log_func: log_func(f"Adding data to ChromaDB (Batch size: {batch_size})...")
402
+ for i in range(0, total, batch_size):
403
+ collection.add(
404
+ embeddings=embeddings[i:i+batch_size].tolist(),
405
+ documents=texts[i:i+batch_size],
406
+ metadatas=metadatas[i:i+batch_size],
407
+ ids=ids[i:i+batch_size]
408
+ )
409
+ if log_func: log_func(f"Saved to DB: {min(i + batch_size, total)} / {total}")
410
+ if log_func: log_func("Successfully finished ChromaDB conversion.")
411
+ return True
412
 
413
+ def chroma_to_table(db_path, collection_name):
414
+ client = chromadb.PersistentClient(path=db_path)
415
+ collection = client.get_collection(name=collection_name)
416
+ data = collection.get(include=["documents", "metadatas"])
417
+ documents = data.get("documents", [])
418
+ metadatas = data.get("metadatas", [])
419
+ if not metadatas: return pd.DataFrame({"text": documents})
420
+ return pd.DataFrame(metadatas)
421
 
422
+ def faiss_to_table(index_path):
423
+ import pandas as pd
424
+ import os
425
+ meta_path = index_path + ".meta.pkl"
426
+ if os.path.exists(meta_path):
427
+ return pd.read_pickle(meta_path)
428
+ else:
429
+ raise Exception(f"Metadata file not found: {meta_path} - Ensure the .meta.pkl file is in the same directory as the .index file.")
430
 
431
+ def table_to_faiss(df, index_path, text_columns, progress_bar=None, log_func=None):
432
+ if log_func: log_func("Preparing text data for embedding...")
433
+ texts = []
434
+ for _, row in df.iterrows():
435
+ parts = [f"{col}: {row[col]}" for col in text_columns if pd.notna(row[col])]
436
+ texts.append("\n".join(parts) if parts else "")
437
+
438
+ embeddings = get_embeddings_batched(texts, progress_bar, log_func)
439
 
440
+ if progress_bar is not None:
441
+ progress_bar.progress(1.0, text=t.get("saving_faiss", "Saving to FAISS..."))
442
+
443
+ if log_func: log_func("Creating FAISS index...")
444
+ dimension = embeddings.shape[1]
445
+ index = faiss.IndexFlatL2(dimension)
446
 
447
+ if log_func: log_func(f"Adding {len(embeddings)} vectors to index...")
448
+ index.add(embeddings)
449
+
450
+ if log_func: log_func(f"Writing index to {index_path}...")
451
+ faiss.write_index(index, index_path)
452
+
453
+ meta_path = index_path + ".meta.pkl"
454
+ if log_func: log_func(f"Saving metadata to {meta_path}...")
455
+ df.to_pickle(meta_path)
456
+
457
+ if log_func: log_func("Successfully finished FAISS conversion.")
458
+ return True
459
+
460
+ st.title(t["title"])
461
+
462
+ tab1, tab2 = st.tabs([t["tab_table_to_db"], t["tab_db_to_table"]])
463
+
464
+ # --- TAB 1: Table to Vector DB ---
465
+ with tab1:
466
+ st.header(t["header_tab1"])
467
+
468
+ data_source = st.radio(t.get("data_source", "Data Source:"), ["📁 " + t.get("file_upload", "File Upload"), "🗄️ SQLite File", "🌐 Remote SQL"], horizontal=True)
469
+ df = None
470
+
471
+ if data_source.startswith("📁"):
472
+ uploaded_file = st.file_uploader(t["upload_file"], type=["csv", "xlsx", "xls"], key="upload_table")
473
+ if uploaded_file is not None:
474
+ try:
475
+ if uploaded_file.name.endswith('.csv'):
476
+ df = pd.read_csv(uploaded_file)
477
+ else:
478
+ xl = pd.ExcelFile(uploaded_file)
479
+ sheet = st.selectbox(t["select_sheet"], xl.sheet_names)
480
+ df = xl.parse(sheet)
481
+ except Exception as e:
482
+ st.error(f"{t.get('error_read_convert', 'Error')} {str(e)}")
483
+
484
+ elif data_source.startswith("🗄️"):
485
+ sqlite_file = st.file_uploader("Upload SQLite Database (.db, .sqlite) / 上傳 SQLite 檔案", type=["db", "sqlite"], key="upload_sqlite")
486
+ if sqlite_file is not None:
487
+ temp_db_path = os.path.join("/data/temp_extract", f"temp_{hash(sqlite_file.name)}.db")
488
+ os.makedirs("/data/temp_extract", exist_ok=True)
489
+ with open(temp_db_path, "wb") as f:
490
+ f.write(sqlite_file.getvalue())
491
+ db_uri = f"sqlite:///{temp_db_path}"
492
+
493
+ try:
494
+ from sqlalchemy import create_engine, inspect
495
+ engine = create_engine(db_uri)
496
+ inspector = inspect(engine)
497
+ tables = inspector.get_table_names()
498
+ if tables:
499
+ selected_table = st.selectbox(t.get("select_table", "Select Table:"), tables)
500
+ if selected_table:
501
+ df = pd.read_sql_table(selected_table, engine)
502
+ else:
503
+ st.warning("No tables found in this database.")
504
+ except Exception as e:
505
+ st.error(f"Database error: {e}")
506
+
507
+ else: # Remote SQL
508
+ with st.container():
509
+ col1, col2 = st.columns(2)
510
+ sql_type = col1.selectbox("DB Type / 資料庫類型", ["MySQL", "PostgreSQL"])
511
+ host = col2.text_input("Host / 伺服器", value="localhost")
512
+ port = col1.text_input("Port / 埠號", value="3306" if sql_type=="MySQL" else "5432")
513
+ db_name = col2.text_input("Database Name / 資料庫名稱")
514
+ user = col1.text_input("Username / 帳號")
515
+ password = col2.text_input("Password / 密碼", type="password")
516
+
517
+ if host and db_name and user:
518
+ if sql_type == "MySQL":
519
+ db_uri = f"mysql+pymysql://{user}:{password}@{host}:{port}/{db_name}"
520
+ else:
521
+ db_uri = f"postgresql://{user}:{password}@{host}:{port}/{db_name}"
522
+
523
+ try:
524
+ from sqlalchemy import create_engine, inspect
525
+ engine = create_engine(db_uri)
526
+ inspector = inspect(engine)
527
+ tables = inspector.get_table_names()
528
+ if tables:
529
+ selected_table = st.selectbox(t.get("select_table", "Select Table:"), tables)
530
+ if selected_table:
531
+ df = pd.read_sql_table(selected_table, engine)
532
+ else:
533
+ st.warning("No tables found in this database.")
534
+ except Exception as e:
535
+ st.error(f"Database error: {e}")
536
+
537
+ if df is not None:
538
  try:
539
+
540
+ st.write(f"### {t['data_preview']} ({t['total_rows']}: {df.shape[0]}, {t['total_cols']}: {df.shape[1]})")
541
+ st.write(t["scroll_down"])
542
+ st.dataframe(df, height=600, use_container_width=True)
543
+
544
+ text_columns = st.multiselect(t["select_columns"], df.columns, default=[df.columns[0]])
545
+ db_type = st.radio(t["select_db"], ["ChromaDB", "FAISS"])
546
+
547
+ if st.button(t["btn_convert"]):
548
+ if not text_columns:
549
+ st.warning(t["warn_select_col"])
550
  else:
551
+ with st.spinner(f"{t['generating']} {db_type}..."):
552
+ output_dir = "/data/output_db"
553
+ os.makedirs(output_dir, exist_ok=True)
554
+
555
+ progress_bar = st.progress(0, text=t["initializing"])
556
+
557
+ st.write(f"### Execution Logs / 執行日誌")
558
+ log_placeholder = st.empty()
559
+ log_msgs = []
560
+ def app_logger(msg):
561
+ import datetime
562
+ import html
563
+ ts = datetime.datetime.now().strftime("%H:%M:%S")
564
+ log_msgs.append(f"[{ts}] {msg}")
565
+ safe_logs = html.escape("\n".join(log_msgs))
566
+
567
+ log_html = f"""
568
+ <div style="
569
+ height: 220px;
570
+ overflow-y: auto;
571
+ background-color: #1e1e1e;
572
+ color: #4af626;
573
+ padding: 10px;
574
+ border-radius: 5px;
575
+ font-family: 'Courier New', monospace;
576
+ font-size: 13px;
577
+ white-space: pre-wrap;
578
+ border: 1px solid #444;
579
+ display: flex;
580
+ flex-direction: column-reverse;
581
+ ">
582
+ <div>{safe_logs}</div>
583
+ </div>
584
+ """
585
+ log_placeholder.markdown(log_html, unsafe_allow_html=True)
586
+
587
+ session_id = uuid.uuid4().hex
588
+
589
+ if db_type == "ChromaDB":
590
+ db_path = os.path.join(output_dir, f"chroma_db_{session_id}")
591
+ os.makedirs(db_path, exist_ok=True)
592
+
593
+ collection_name = "my_collection"
594
+ table_to_chroma(df, db_path, collection_name, text_columns, progress_bar, app_logger)
595
+
596
+ progress_bar.progress(1.0, text=t["zipping"])
597
+ # Zip the output for download
598
+ base_name = os.path.join(output_dir, f"chroma_db_{session_id}")
599
+ shutil.make_archive(base_name, 'zip', db_path)
600
+ zip_path = base_name + ".zip"
601
+
602
+ with open(zip_path, "rb") as f:
603
+ st.download_button(
604
+ label=t["download_chroma"],
605
+ data=f,
606
+ file_name="chroma_db.zip",
607
+ mime="application/zip"
608
+ )
609
+ progress_bar.empty()
610
+ st.success(t["success_chroma"])
611
+
612
+ elif db_type == "FAISS":
613
+ faiss_dir = os.path.join(output_dir, f"faiss_db_{session_id}")
614
+ os.makedirs(faiss_dir, exist_ok=True)
615
+
616
+ index_path = os.path.join(faiss_dir, "vector.index")
617
+ table_to_faiss(df, index_path, text_columns, progress_bar, app_logger)
618
+
619
+ progress_bar.progress(1.0, text=t["zipping"])
620
+ # Zip the output for download
621
+ base_name = os.path.join(output_dir, f"faiss_db_{session_id}")
622
+ shutil.make_archive(base_name, 'zip', faiss_dir)
623
+ zip_path = base_name + ".zip"
624
+
625
+ with open(zip_path, "rb") as f:
626
+ st.download_button(
627
+ label=t["download_faiss"],
628
+ data=f,
629
+ file_name="faiss_db.zip",
630
+ mime="application/zip"
631
+ )
632
+ progress_bar.empty()
633
+ st.success(t["success_faiss"])
634
+
635
  except Exception as e:
636
+ st.error(f"{t['error_read_convert']} {str(e)}")
 
 
637
 
638
+ # --- TAB 2: Vector DB to Table ---
639
+ with tab2:
640
+ st.header(t["header_tab2"])
641
+
642
+ db_type_reverse = st.radio(t["source_db"], ["ChromaDB", "FAISS"], key="reverse_db")
643
 
644
+ st.info(t.get("upload_zip_info", "Upload ZIP").format(db_type_reverse))
645
+ uploaded_zip = st.file_uploader(t.get("upload_zip_btn", "Upload ZIP"), type=["zip"], key="upload_zip")
646
+
647
+ if uploaded_zip is not None:
648
+ extract_dir = os.path.join("/data/temp_extract", f"zip_{hash(uploaded_zip.name)}")
649
+ if not os.path.exists(extract_dir):
650
+ os.makedirs(extract_dir, exist_ok=True)
651
+ with zipfile.ZipFile(uploaded_zip, 'r') as zip_ref:
652
+ zip_ref.extractall(extract_dir)
653
 
654
+ try:
655
+ if db_type_reverse == "ChromaDB":
656
+ try:
657
+ client = chromadb.PersistentClient(path=extract_dir)
658
+ collections = client.list_collections()
659
+ if not collections:
660
+ st.error(t.get("no_collections", "No collections found."))
661
+ else:
662
+ col_names = [c if isinstance(c, str) else c.name for c in collections]
663
+ selected_col = st.selectbox(t.get("select_collection", "Select Collection"), col_names)
664
+
665
+ if st.button(t.get("extract_data", "Extract Data")):
666
+ st.session_state['df_extracted'] = chroma_to_table(extract_dir, selected_col)
667
+ except Exception as e:
668
+ st.error(f"{t.get('fail_read', 'Failed to read')} {e}")
669
+
670
+ elif db_type_reverse == "FAISS":
671
+ index_files = [f for f in os.listdir(extract_dir) if f.endswith(".index")]
672
+ if not index_files:
673
+ st.error(t.get("no_index", "No .index found"))
674
+ else:
675
+ selected_index = st.selectbox(t.get("select_index", "Select Index"), index_files)
676
+ if st.button(t.get("extract_data", "Extract Data")):
677
+ index_path = os.path.join(extract_dir, selected_index)
678
+ st.session_state['df_extracted'] = faiss_to_table(index_path)
679
+
680
+ if 'df_extracted' in st.session_state and st.session_state['df_extracted'] is not None:
681
+ df_to_show = st.session_state['df_extracted']
682
+ st.write(f"### {t.get('extracted_preview', 'Extracted Data Preview')}")
683
+ st.dataframe(df_to_show, height=300)
684
+
685
+ col1, col2, col3, _ = st.columns([1.2, 1.2, 1.5, 3])
686
+
687
+ csv_data = df_to_show.to_csv(index=False).encode('utf-8')
688
+ col1.download_button(
689
+ label=t.get("download_csv", "Download CSV"),
690
+ data=csv_data,
691
+ file_name="extracted_data.csv",
692
+ mime="text/csv"
693
+ )
694
+
695
+ excel_buffer = io.BytesIO()
696
+ df_to_show.to_excel(excel_buffer, index=False)
697
+ excel_buffer.seek(0)
698
+ col2.download_button(
699
+ label=t.get("download_excel", "Download Excel"),
700
+ data=excel_buffer,
701
+ file_name="extracted_data.xlsx",
702
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
703
+ )
704
+
705
+ # Generate SQLite DB for download
706
+ temp_db = os.path.join("/data/temp_extract", f"export_{uuid.uuid4().hex}.db")
707
+ from sqlalchemy import create_engine
708
+ engine_sqlite = create_engine(f"sqlite:///{temp_db}")
709
+ df_to_show.to_sql("extracted_data", con=engine_sqlite, if_exists='replace', index=False)
710
+ with open(temp_db, "rb") as f_db:
711
+ sqlite_data = f_db.read()
712
+
713
+ col3.download_button(
714
+ label="Download SQLite DB",
715
+ data=sqlite_data,
716
+ file_name="extracted_data.db",
717
+ mime="application/x-sqlite3"
718
+ )
719
+
720
+ st.write("---")
721
+ st.write(f"### Push to Remote SQL Server / 推送至遠端資料庫")
722
+ st.info("Push data directly to a Remote SQL Server (MySQL / PostgreSQL)")
723
+ col3, col4 = st.columns(2)
724
+ exp_host = col3.text_input("Host / 伺服器", value="localhost", key="exp_host")
725
+ exp_port = col4.text_input("Port / 埠號", value="3306", key="exp_port")
726
+ exp_user = col3.text_input("Username / 帳號", key="exp_user")
727
+ exp_pass = col4.text_input("Password / 密碼", type="password", key="exp_pass")
728
+ exp_db = col3.text_input("Database / 資料庫名稱", key="exp_db")
729
+ exp_table = col4.text_input("Target Table / 匯出目標表", value="extracted_data", key="exp_table")
730
+
731
+ if st.button(t.get("save_to_db", "Save to Database")):
732
+ if exp_host and exp_user and exp_db and exp_table:
733
+ try:
734
+ db_uri_export = f"mysql+pymysql://{exp_user}:{exp_pass}@{exp_host}:{exp_port}/{exp_db}"
735
+ from sqlalchemy import create_engine
736
+ engine = create_engine(db_uri_export)
737
+ df_to_show.to_sql(name=exp_table, con=engine, if_exists='replace', index=False)
738
+ st.success(t.get("db_success", "Successfully saved to Database!"))
739
+ except Exception as e:
740
+ st.error(f"Database error: {e}")
741
+
742
+ except Exception as e:
743
+ st.error(f"{t.get('error_processing', 'Error')} {str(e)}")