# 在練習程式上加入輔助工具 --- 待改進清單 04/03 生成過的可以在更新一次生成內容(已解決) 04/04 例句範圍限定成多益常考邏輯(已解決) 04/17 API設計需要一次大量 04/19 語音模擬多益題目(Gemini Multi-modal Live) --- 這是屬於第二章 這邊的基礎是建立於上一章節的內容 要記得 --- ## BUG大集合: - 1. 當出現這Bug 代表你的vs code資料出現錯誤,先嘗試把舊的***vocab_app.db*** 資料庫檔案刪除 ![image](https://hackmd.io/_uploads/SkxvhjAcZx.png) ![螢幕擷取畫面 2026-03-23 200700](https://hackmd.io/_uploads/SJYZTsCqWe.png) 一樣關閉原始cmd再重新開一個cmd 輸入啟動指令 ==streamlit run app.py== 終端機中可以按 ctrl+c來重新跳出輸入行 --- - 2. 出現停用指令碼執行的問題,基本上是因為Windows 為了保護系統,預設禁止執行任何腳本(.ps1 檔案),所以虛擬環境(venv)啟動腳本被擋下了。 ![image](https://hackmd.io/_uploads/r1VIDsU3Wg.png) ``` PS D:\pye> & d:\pye\.venv\Scripts\Activate.ps1 & : 因為這個系統上已停用指令碼執行,所以無法載入 D:\pye\.venv\Scripts\Activate.ps1 檔案。如需詳細資訊,請參閱 about_Execution_Policies,網址為 https:/go.microsoft.com/fwlink/?LinkID=135170。 位於 線路:1 字元:3 + & d:\pye\.venv\Scripts\Activate.ps1 + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : SecurityError: (:) [], PSSecurityException + FullyQualifiedErrorId : UnauthorizedAccess PS D:\pye> ``` 但這是因為我們的命令提示字元切換到PoewrShell,切換回Command Promt可以先不用管這問題。 ![image](https://hackmd.io/_uploads/HkFzYoIhbe.png) --- 重點: ==自己的api晶鑰請小心,絕對不可以公開,避免惡意盜用去做高耗運算讓你帳單起飛== ## h3**學習資源參考:** 開始使用 Google AI Studio https://steam.oxxostudio.tw/category/aigc/2025/google-ai-studio-1.html 官方資料 https://ai.google.dev/gemini-api/docs?hl=zh-tw 筆記軟體 MarkDown語法 https://hackmd.io/@eMP9zQQ0Qt6I8Uqp2Vqy6w/SyiOheL5N/%2FBVqowKshRH246Q7UDyodFA 費率限制 https://ai.google.dev/gemini-api/docs/rate-limits?hl=zh-tw ## 前置作業 申請Gemini的API晶鑰 進入官方網站申請,理論不需要使用付費方案就能使用 這邊會有新的專有名詞需要注意,之後都會使用到 * `RPM` 每分鐘請求數(requests per minute) * `TPM` 每分鐘 Token 數(tokens per minute) * `RPD` 每日請求數(requests per day) --- 進入官網,點選 ==Get API Key==來申請(或是他創辦帳號完就已自動升成一個能使用的API晶鑰) https://aistudio.google.com/ ![image](https://hackmd.io/_uploads/ryEkAV6sbe.png) --- ==Usage==這頁面負責後台管理,選定的ai版本用了多少資源跟次數,在偵錯時候能直觀看到到底用了哪一個版本跟次數,也就是前文說到的 這在管理跟開發上會需要特別注意, ![image](https://hackmd.io/_uploads/H1TnfBps-x.png) ![image](https://hackmd.io/_uploads/B1fVErasWe.png) 常見錯誤表: 強烈推薦到官方文件中搜尋 **Gemini API 後端服務錯誤代碼** 我目前有遇過在串接API會出現404錯誤,後來發現是搜尋不到所設定的Gemini AI工具版本,所以語法上更改成使用詢問方式解決,程式解說部份再詳解。 https://ai.google.dev/gemini-api/docs/troubleshooting?hl=zh-tw#error-codes # 1 先練習加入Gemini API 在vs code的cmd中輸入 ``` pip install google-generativeai``` 更新```pip install --upgrade google-generativeai``` ## 一、 系統架構流程圖 1. 系統初始化與 Excel 匯入流程 (去重複邏輯) 這部分描述了 init_db 的欄位補齊功能,以及 save_to_db_upsert 如何處理重複單字。 ![Untitled diagram-2026-04-02-181608](https://hackmd.io/_uploads/rkPFGNniZl.png =70%x) 2. AI 教學內容生成邏輯 (快取與 API 調度) 這部分描述了程式如何自動選擇模型,並利用資料庫作為 Cache (快取) 來節省 API 額度。 ![Untitled diagram-2026-04-02-182031](https://hackmd.io/_uploads/SyZt743oZx.png =70%x) 3. 循環測驗與單字池管理 (防止重複抽題) 這部分解釋了 q_pool 如何運作,確保在單元測完前不重複,測完後自動重啟。 ![Untitled diagram-2026-04-02-182145](https://hackmd.io/_uploads/SJJJ4N3sZl.png =70%x) ## 二、 Imports `streamlit`建立 Web 介面、按鈕、側邊欄及分頁。全域:負責所有畫面的呈現。 `pandas`處理 Excel 匯入及資料表的篩選與操作。檔案處理:上傳 Excel 並轉為 DataFrame。 `gtts`,`Google Text-to-Speech`將文字轉為語音檔。發音功能:點擊喇叭按鈕時生成音訊。 `sqlite3`輕量級資料庫,存放單字、錯誤次數與 AI 內容。資料持久化:確保關閉程式後資料不遺失。 `random`,隨機挑選單字與生成測驗選項。測驗模式:洗牌選項與隨機抽題。 `base64`,將音訊二進位資料轉為編碼字串。自動播放:讓語音不需下載即可在瀏覽器播放。 `json`處理 AI 生成的多個例句 (List/Dict) 轉換。AI 儲存:將複雜資料轉為字串存入資料庫。 `google.generativeai`串接 Google Gemini 1.5 大模型。AI 教學:生成字根分析、記憶法與例句。 ## 三、 核心函式 (Functions) 的功能解說 - `init_db():` 用途:資料庫初始化與升級。 環節:程式一啟動即執行,確保資料表結構正確。 - `save_to_db_upsert():` 用途:存入單字並「去重複」。若單字已存在則更新 Unit 而非新增。 環節:側邊欄上傳 Excel 後執行。 - `load_full_db():` 用途:從 SQLite 讀取全部單字回 Pandas DataFrame。 環節:主畫面渲染前,確保抓到最新數據。 - `get_ai_learning_content():` 用途:核心 AI 請求邏輯。包含自動選擇模型(Flash/Pro)與清理 JSON 字串。 環節:點擊生成 AI 教學內容按鈕時觸發。 - `save_ai_content():` 用途:將 AI 跑完的內容回填至資料庫。 環節:AI 生成成功後立即執行。 - `autoplay_audio():` 用途:將文字轉為語音並利用 `HTML5 <audio>` 標籤自動播放。 環節:點擊任何喇叭按鈕時。 - `update_error_count():` 用途:當使用者測驗答錯時,SQL 指令將該單字錯誤次數 +1。 環節:測驗模式點選錯誤選項時。 ## 四、 程式執行生命週期 (Step-by-Step) - Step 1: 環境檢測與資料載入 當 app.p 執行,系統第一時間呼叫 `init_db()`。隨後執行 `load_full_db()`,將硬碟資料載入 `st.session_state` 作為全局緩存。 - Step 2: 側邊欄檔案監視 監控 file_uploader。一旦有檔案上傳,立即補齊 AI 欄位的空值,寫入 SQL,並觸發 `st.rerun()`。這會讓上傳器清空,並讓新檔名出現在「已上傳清單」。 - Step 3: 單元切換與狀態初始化 當使用者切換 selectbox: 系統觸發「狀態守衛」。 重置 `active_unit` 標籤。 清除舊單元的題目快取,重新複製一份該單元所有單字的 Index 形成新的 Pool。 - Step 4: 教學模式與「懶加載」內容 當進入 Tab 1: 系統根據 SQL 中的 AI 欄位狀態決定 UI。 若欄位為空,顯示「生成」按鈕;若已有資料,直接顯示字根與例句。 點擊生成時,啟動 `st.spinner` 動態提示,直到 AI 回傳並更新資料庫。 - Step 5: 循環測驗流程 當進入 Tab 2跟3: 偵測 Pool 是否為空。若空則從 DB 重新填充(Circular logic)。 隨機抽取題目,並從 Pool 移除該 ID 以防重複。 利用 `st.columns` 動態佈局生成選項按鈕,並執行答錯次數的 SQL 更新。 ## 五、 系統架構流程圖 ## 六、 把Gemini API加入本地部屬的程式(單純單字教學頁面) ```python= import streamlit as st import pandas as pd from gtts import gTTS import io import sqlite3 import os import random import base64 import json import google.generativeai as genai # --- 1. 資料庫層 (DAO) ------------------------------------------------------- DB_NAME = "vocab_app.db" def init_db(): conn = sqlite3.connect(DB_NAME) c = conn.cursor() # 建立完整表格 (含 AI 欄位) c.execute('''CREATE TABLE IF NOT EXISTS vocab (unit INTEGER, english TEXT UNIQUE, chinese TEXT, source_file TEXT, error_count INTEGER DEFAULT 0, root_analysis TEXT, mnemonic TEXT, sentences_json TEXT)''') # 欄位自動檢查 (Schema Migration) c.execute("PRAGMA table_info(vocab)") existing_cols = [col[1] for col in c.fetchall()] for col in ["root_analysis", "mnemonic", "sentences_json"]: if col not in existing_cols: c.execute(f"ALTER TABLE vocab ADD COLUMN {col} TEXT") conn.commit() conn.close() def save_ai_content(word, root, mnem, sents): conn = sqlite3.connect(DB_NAME) c = conn.cursor() c.execute('UPDATE vocab SET root_analysis=?, mnemonic=?, sentences_json=? WHERE english=?', (root, mnem, json.dumps(sents), word)) conn.commit() conn.close() # --- 2. AI 核心邏輯 (Gemini API) ---------------------------------------------- def get_ai_learning_content(word, chinese): try: # 1. API 配置 API_KEY = "api整串直接貼過來 需要注意安全性" genai.configure(api_key=API_KEY) # 2. 自動選擇可用模型 (解決 404 的核心邏輯) available_models = [m.name for m in genai.list_models() if 'generateContent' in m.supported_generation_methods] # 優先順序:1.5-flash -> 1.0-pro -> 第一個可用的模型 target_model = "" if 'models/gemini-1.5-flash' in available_models: target_model = 'models/gemini-1.5-flash' elif 'models/gemini-pro' in available_models: target_model = 'models/gemini-pro' else: target_model = available_models[0] if available_models else "" if not target_model: st.error("找不到任何可用的 Gemini 模型,請檢查 API Key 權限。") return None model = genai.GenerativeModel(target_model) # 3. 設定 Prompt prompt = f""" 你是一位專業英語老師。請針對單字 "{word}" (中文意思: {chinese}) 提供教學。 請直接回傳 JSON 字串,不要包含任何 Markdown 標籤(如 ```json )。 格式必須精確如下: {{ "root": "字根字首拆解", "mnemonic": "記憶法(包含諧音)", "sentences": [ {{"en": "TOEIC sentence 1", "zh": "翻譯1"}}, {{"en": "TOEIC sentence 2", "zh": "翻譯2"}}, {{"en": "TOEIC sentence 3", "zh": "翻譯3"}} ] }} """ # 4. 呼叫與強制清理 (防止 AI 碎碎念) response = model.generate_content(prompt) raw_text = response.text.strip() # 強力清理 Markdown 殘留 if "```" in raw_text: # 抓取第一對括號之間的內容 import re json_match = re.search(r'\{.*\}', raw_text, re.DOTALL) if json_match: raw_text = json_match.group() return json.loads(raw_text) except Exception as e: st.error(f"AI 請求失敗: {e}") # 如果發生 404,印出所有可用模型供 debug try: models = [m.name for m in genai.list_models()] st.info(f"當前可用模型列表: {models}") except: pass return None # --- 3. 工具與發音 ----------------------------------------------------------- def autoplay_audio(text): if not text: return try: tts = gTTS(text=text, lang='en') fp = io.BytesIO() tts.write_to_fp(fp) b64 = base64.b64encode(fp.getvalue()).decode() md = f'<audio autoplay="true"><source src="data:audio/mp3;base64,{b64}" type="audio/mp3"></audio>' st.markdown(md, unsafe_allow_html=True) except: pass # --- 4. 初始化 ------------------------------------------------------------- st.set_page_config(page_title="本地 AI 單字系統", layout="wide") init_db() # --- 5. 側邊欄 ------------------------------------------------------------- with st.sidebar: st.header("⚙️ 本地管理") if "up_key" not in st.session_state: st.session_state.up_key = 0 up_file = st.file_uploader("匯入 Excel", type=["xlsx"], key=f"up_{st.session_state.up_key}") if up_file: df = pd.read_excel(up_file, header=None) df.columns = ['unit', 'english', 'chinese'] conn = sqlite3.connect(DB_NAME); c = conn.cursor() for _, r in df.iterrows(): c.execute('INSERT INTO vocab (unit,english,chinese,source_file) VALUES (?,?,?,?) ON CONFLICT(english) DO UPDATE SET unit=excluded.unit, chinese=excluded.chinese', (int(r['unit']), str(r['english']), str(r['chinese']), up_file.name)) conn.commit(); conn.close() st.session_state.up_key += 1 st.rerun() st.divider() st.subheader("📄 已上傳清單") conn = sqlite3.connect(DB_NAME); c = conn.cursor() c.execute('SELECT DISTINCT source_file FROM vocab'); files = [r[0] for r in c.fetchall() if r[0]]; conn.close() for i, f in enumerate(files): c1, c2 = st.columns([4, 1]) with c1: st.write(f"· {f}") with c2: if st.button("🗑️", key=f"del_{i}"): conn = sqlite3.connect(DB_NAME); conn.execute('DELETE FROM vocab WHERE source_file=?', (f,)); conn.commit(); conn.close() st.rerun() # --- 6. 主畫面展現 ----------------------------------------------------------- conn = sqlite3.connect(DB_NAME); db = pd.read_sql('SELECT * FROM vocab', conn); conn.close() if not db.empty: unit_list = sorted(db['unit'].unique()) selected_unit = st.selectbox("選擇單元:", unit_list) # Unit 切換初始化 Pool if "active_unit" not in st.session_state or st.session_state.active_unit != selected_unit: st.session_state.active_unit = selected_unit u_idx = db[db['unit'] == selected_unit].index.tolist() st.session_state.q1_pool, st.session_state.q2_pool = u_idx.copy(), u_idx.copy() st.rerun() curr_df = db[db['unit'] == selected_unit].reset_index(drop=True) tab1, tab2, tab3 = st.tabs(["📖 AI 教學模式", "📝 測驗一", "📝 測驗二"]) with tab1: for i, row in curr_df.iterrows(): word, ch = str(row['english']), str(row['chinese']) with st.expander(f"#{i+1} - {word} ({ch})", expanded=False): cL, cR = st.columns([1, 1]) with cL: st.header(word) st.write(f"❌ 錯誤次數:{row['error_count']}") if st.button(f"🔊 播放單字", key=f"t1_s_{i}"): autoplay_audio(word) if not row['root_analysis']: if st.button("✨ 生成 AI 教學內容", key=f"gen_{i}"): with st.spinner(f'AI 正在分析 {word}...'): ai_res = get_ai_learning_content(word, ch) if ai_res: save_ai_content(word, ai_res['root'], ai_res['mnemonic'], ai_res['sentences']) st.rerun() else: st.subheader("🔍 字根字首拆解") st.info(row['root_analysis']) st.subheader("💡 記憶法") st.warning(row['mnemonic']) with cR: st.subheader("📝 多益實戰例句") if row['sentences_json']: s_list = json.loads(row['sentences_json']) for s_i, s in enumerate(s_list): st.write(f"**{s_i+1}.** {s['en']}") st.caption(f"翻譯:{s['zh']}") if st.button(f"🔊 播放例句 {s_i+1}", key=f"spk_s_{i}_{s_i}"): autoplay_audio(s['en']) st.divider() # --- 測驗功能 (維持 v1.5.2 穩定邏輯) --- # ... (此處可填入之前完成的 get_next_q 與測驗 Tab 2/3 代碼) else: st.info("請先匯入單字 Excel 檔案。") ``` ![螢幕擷取畫面 2026-04-03 013244](https://hackmd.io/_uploads/BksO2_Aj-x.png) # 2 加入測驗(中選英 英選中) 經典題目練習 算是初步測試你對這回合單字有多少了解(成為無情刷題機器) 一樣要在程式第50行的 API_KEY =輸入你的api 晶鑰 ==拜託必須記得請好好保管晶鑰== ==拜託必須記得請好好保管晶鑰== ==拜託必須記得請好好保管晶鑰== ```python= import streamlit as st import pandas as pd from gtts import gTTS import io import sqlite3 import os import random import base64 import json import google.generativeai as genai # --- 1. 資料庫層 (DAO) ------------------------------------------------------- DB_NAME = "vocab_app.db" def init_db(): conn = sqlite3.connect(DB_NAME) c = conn.cursor() c.execute('''CREATE TABLE IF NOT EXISTS vocab (unit INTEGER, english TEXT UNIQUE, chinese TEXT, source_file TEXT, error_count INTEGER DEFAULT 0, root_analysis TEXT, mnemonic TEXT, sentences_json TEXT)''') c.execute("PRAGMA table_info(vocab)") existing_cols = [col[1] for col in c.fetchall()] for col in ["root_analysis", "mnemonic", "sentences_json"]: if col not in existing_cols: c.execute(f"ALTER TABLE vocab ADD COLUMN {col} TEXT") conn.commit() conn.close() def save_ai_content(word, root, mnem, sents): conn = sqlite3.connect(DB_NAME) c = conn.cursor() c.execute('UPDATE vocab SET root_analysis=?, mnemonic=?, sentences_json=? WHERE english=?', (root, mnem, json.dumps(sents), word)) conn.commit() conn.close() def update_error_count(english_word): conn = sqlite3.connect(DB_NAME) c = conn.cursor() c.execute('UPDATE vocab SET error_count = error_count + 1 WHERE english = ?', (english_word,)) conn.commit() conn.close() # --- 2. AI 核心邏輯 ---------------------------------------------------------- def get_ai_learning_content(word, chinese): try: # 請確保此 API KEY 正確且無前後空格 API_KEY = "API KEY!!!!!!!!!!!!!!!!!!!".strip() genai.configure(api_key=API_KEY) available_models = [m.name for m in genai.list_models() if 'generateContent' in m.supported_generation_methods] target_model = "" if 'models/gemini-1.5-flash' in available_models: target_model = 'models/gemini-1.5-flash' elif 'models/gemini-pro' in available_models: target_model = 'models/gemini-pro' else: target_model = available_models[0] if available_models else "" if not target_model: st.error("找不到可用模型") return None model = genai.GenerativeModel(target_model) prompt = f""" 你是一位專業多益TOEIC英語老師。請針對單字 "{word}" (中文意思: {chinese}) 提供多益TOEIC考試題目常出的教學。 請直接回傳 JSON 字串,不要包含任何 Markdown 標籤。 格式必須精確如下: {{ "root": "字根字首拆解", "mnemonic": "記憶法,近似詞,常用短句", "sentences": [ {{"en": "TOEIC sentence 1", "zh": "翻譯1"}}, {{"en": "TOEIC sentence 2", "zh": "翻譯2"}}, {{"en": "TOEIC sentence 3", "zh": "翻譯3"}} ] }} """ response = model.generate_content(prompt) raw_text = response.text.strip() if "```" in raw_text: import re json_match = re.search(r'\{.*\}', raw_text, re.DOTALL) if json_match: raw_text = json_match.group() return json.loads(raw_text) except Exception as e: st.error(f"AI 請求失敗: {e}") return None # --- 3. 工具與發音 ----------------------------------------------------------- def autoplay_audio(text): if not text: return try: tts = gTTS(text=text, lang='en') fp = io.BytesIO() tts.write_to_fp(fp) b64 = base64.b64encode(fp.getvalue()).decode() md = f'<audio autoplay="true"><source src="data:audio/mp3;base64,{b64}" type="audio/mp3"></audio>' st.markdown(md, unsafe_allow_html=True) except: pass # --- 4. 初始化 ------------------------------------------------------------- st.set_page_config(page_title="本地 AI 多益學習系統", layout="wide") init_db() # --- 5. 側邊欄 ------------------------------------------------------------- with st.sidebar: st.header("⚙️ 本地管理") if "up_key" not in st.session_state: st.session_state.up_key = 0 up_file = st.file_uploader("匯入 Excel", type=["xlsx"], key=f"up_{st.session_state.up_key}") if up_file: df = pd.read_excel(up_file, header=None) df.columns = ['unit', 'english', 'chinese'] conn = sqlite3.connect(DB_NAME); c = conn.cursor() for _, r in df.iterrows(): c.execute('''INSERT INTO vocab (unit,english,chinese,source_file) VALUES (?,?,?,?) ON CONFLICT(english) DO UPDATE SET unit=excluded.unit, chinese=excluded.chinese''', (int(r['unit']), str(r['english']), str(r['chinese']), up_file.name)) conn.commit(); conn.close() st.session_state.up_key += 1 st.rerun() st.divider() st.subheader("📄 已上傳清單") conn = sqlite3.connect(DB_NAME); c = conn.cursor() c.execute('SELECT DISTINCT source_file FROM vocab'); files = [r[0] for r in c.fetchall() if r[0]]; conn.close() for i, f in enumerate(files): c1, c2 = st.columns([4, 1]) with c1: st.write(f"· {f}") with c2: if st.button("🗑️", key=f"del_{i}"): conn = sqlite3.connect(DB_NAME); conn.execute('DELETE FROM vocab WHERE source_file=?', (f,)); conn.commit(); conn.close() st.rerun() # --- 6. 主畫面展現 ----------------------------------------------------------- conn = sqlite3.connect(DB_NAME); db = pd.read_sql('SELECT * FROM vocab', conn); conn.close() if not db.empty: unit_list = sorted(db['unit'].unique()) selected_unit = st.selectbox("選擇單元:", unit_list) if "active_unit" not in st.session_state or st.session_state.active_unit != selected_unit: st.session_state.active_unit = selected_unit u_idx = db[db['unit'] == selected_unit].index.tolist() st.session_state.q1_pool = u_idx.copy() st.session_state.q2_pool = u_idx.copy() if 'q1_data' in st.session_state: del st.session_state.q1_data if 'q2_data' in st.session_state: del st.session_state.q2_data st.rerun() curr_df = db[db['unit'] == selected_unit].reset_index(drop=True) t_unit_len = len(curr_df) tab1, tab2, tab3 = st.tabs(["📖 AI 教學模式", "📝 測驗一 (中選英)", "📝 測驗二 (英選中)"]) # --- Tab 1: 教學模式 --- with tab1: for i, row in curr_df.iterrows(): word, ch = str(row['english']), str(row['chinese']) with st.expander(f"#{i+1} - {word} ({ch})"): cL, cR = st.columns([1, 1]) with cL: st.header(word) st.write(f"❌ 錯誤次數:{row['error_count']}") b1, b2 = st.columns(2) with b1: if st.button(f"🔊 播放單字", key=f"t1_s_{i}"): autoplay_audio(word) with b2: label = "✨ 生成 AI 教學" if not row['root_analysis'] else "🔄 重新生成內容" if st.button(label, key=f"gen_{i}"): with st.spinner(f'AI 分析中...'): res = get_ai_learning_content(word, ch) if res: save_ai_content(word, res['root'], res['mnemonic'], res['sentences']) st.rerun() if row['root_analysis']: st.subheader("🔍 字根字首") st.info(row['root_analysis']) st.subheader("💡 記憶法") st.warning(row['mnemonic']) with cR: st.subheader("📝 多益實戰例句") if row['sentences_json']: s_list = json.loads(row['sentences_json']) for s_i, s in enumerate(s_list): st.write(f"**{s_i+1}.** {s['en']}") st.caption(f"翻譯:{s['zh']}") if st.button(f"🔊 播放例句 {s_i+1}", key=f"spk_s_{i}_{s_i}"): autoplay_audio(s['en']) st.divider() # --- 測驗抽取核心函式 --- def get_next_q(pool_key, q_type): if not st.session_state[pool_key]: st.session_state[pool_key] = db[db['unit'] == selected_unit].index.tolist() st.toast("🔄 測驗已自動循環!") pool = st.session_state[pool_key] idx = random.choice(pool) pool.remove(idx) st.session_state[pool_key] = pool row = db.loc[idx] q_text = row['chinese'] if q_type == 'c2e' else row['english'] ans = row['english'] if q_type == 'c2e' else row['chinese'] # 抓取干擾項 all_opts = db[db['unit'] == selected_unit]['english' if q_type == 'c2e' else 'chinese'].unique().tolist() others = [o for o in all_opts if str(o) != str(ans)] distractors = random.sample(others, min(len(others), 3)) opts = distractors + [ans] random.shuffle(opts) return {"q": q_text, "ans": ans, "opts": opts, "raw": row} # --- Tab 2 & 3: 測驗循環展示 (加入語音功能) --- quiz_tabs = [(tab2, 'q1_pool', 'q1_data', 'c2e', "中選英"), (tab3, 'q2_pool', 'q2_data', 'e2c', "英選中")] for t, p_key, q_key, q_type, t_name in quiz_tabs: with t: rem = len(st.session_state.get(p_key, [])) st.write(f"📊 {t_name} 進度:**{t_unit_len - rem}** / {t_unit_len}") st.progress((t_unit_len - rem) / t_unit_len if t_unit_len > 0 else 0) if st.button("下一題", key=f"btn_next_{q_key}") or q_key not in st.session_state: st.session_state[q_key] = get_next_q(p_key, q_type) st.rerun() curr_q = st.session_state[q_key] # 題目顯示區 st.subheader(f"題目:{curr_q['q']}") # --- 語音功能邏輯 --- if q_type == 'e2c': # 英選中:題目本身就是英文,提供「聽題」按鈕 if st.button("🔊 聽題目發音", key=f"aud_q_{q_key}"): autoplay_audio(curr_q['q']) else: # 中選英:題目是中文,提供「聽答案(單字)發音」按鈕 # 這樣可以幫助使用者在選出英文前,先透過聲音輔助記憶 if st.button("🔊 聽答案(英文單字)發音", key=f"aud_ans_{q_key}"): autoplay_audio(curr_q['raw']['english']) st.divider() # 選項區 cols = st.columns(2) for idx, opt in enumerate(curr_q['opts']): with cols[idx % 2]: if st.button(opt, key=f"opt_{q_key}_{idx}", use_container_width=True): if opt == curr_q['ans']: st.success("✅ 正確!") # 答對時自動播放該英文單字發音 (強化記憶) # 不論中選英還是英選中,都唸出英文 autoplay_audio(curr_q['raw']['english']) else: update_error_count(curr_q['raw']['english']) st.error(f"❌ 錯誤!正解:{curr_q['ans']}") else: st.info("👋 歡迎!請先在左側上傳 Excel 檔案。") ``` # 3 # 4 # 5 # 6 # 7