# 在練習程式上加入輔助工具
---
待改進清單
04/03 生成過的可以在更新一次生成內容(已解決)
04/04 例句範圍限定成多益常考邏輯(已解決)
04/17 API設計需要一次大量
04/19 語音模擬多益題目(Gemini Multi-modal Live)
---
這是屬於第二章
這邊的基礎是建立於上一章節的內容
要記得
---
## BUG大集合:
- 1. 當出現這Bug 代表你的vs code資料出現錯誤,先嘗試把舊的***vocab_app.db*** 資料庫檔案刪除


一樣關閉原始cmd再重新開一個cmd
輸入啟動指令
==streamlit run app.py==
終端機中可以按 ctrl+c來重新跳出輸入行
---
- 2. 出現停用指令碼執行的問題,基本上是因為Windows 為了保護系統,預設禁止執行任何腳本(.ps1 檔案),所以虛擬環境(venv)啟動腳本被擋下了。

```
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可以先不用管這問題。

---
重點:
==自己的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/

---
==Usage==這頁面負責後台管理,選定的ai版本用了多少資源跟次數,在偵錯時候能直觀看到到底用了哪一個版本跟次數,也就是前文說到的
這在管理跟開發上會需要特別注意,


常見錯誤表:
強烈推薦到官方文件中搜尋 **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 如何處理重複單字。

2. AI 教學內容生成邏輯 (快取與 API 調度)
這部分描述了程式如何自動選擇模型,並利用資料庫作為 Cache (快取) 來節省 API 額度。

3. 循環測驗與單字池管理 (防止重複抽題)
這部分解釋了 q_pool 如何運作,確保在單元測完前不重複,測完後自動重啟。

## 二、 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 檔案。")
```

# 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