File size: 17,296 Bytes
bc7bdb7 | 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 316 317 318 319 320 321 322 323 324 325 326 327 328 | # deep_analyzer.py
import google.generativeai as genai
from supabase import create_client
import config
import json
import logging
import re
import requests # 👈 確保檔案最上面有 import requests
logger = logging.getLogger(__name__)
if config.GEMINI_KEY:
try:
genai.configure(api_key=config.GEMINI_KEY)
except Exception as e:
logger.warning(f"Gemini 初始化提醒: {e}")
# 💡 Supabase 防呆連線檢查
url = config.SUPABASE_URL
key = config.SUPABASE_KEY
if not url or not key:
logger.warning("⚠️ 警告:SUPABASE_URL 或 KEY 尚未設定,將無法使用 Supabase 知識庫功能!")
supabase = None
else:
try:
supabase = create_client(url, key)
except Exception as e:
logger.warning(f"Supabase 初始化失敗: {e}")
supabase = None
# ----------------------------------------------------------------
# 📝 根據官方規範建立的資料庫全稱對齊映射(超前部署未來 15 族資料)
# ----------------------------------------------------------------
TRIBE_MAPPING = {
"阿美": "海岸阿美語",
"泰雅": "賽考利克泰雅語",
"賽夏": "賽夏語",
"邵": "邵語",
"賽德克": "德固達雅賽德克語",
"布農": "郡群布農語",
"排灣": "北排灣語",
"魯凱": "霧台魯凱語",
"太魯閣": "太魯閣語",
"噶瑪蘭": "噶瑪蘭語",
"鄒": "阿里山鄒語",
"卑南": "南王卑南語",
"雅美": "雅美語",
"撒奇萊雅": "撒奇萊雅語",
"卡那卡那富": "卡那卡那富語",
"拉阿魯哇": "拉阿魯哇語"
}
# ----------------------------------------------------------------
# 1. 滿血型 RAG 檢索邏輯 (智慧文字雷達版 - 速度極快且 100% 穩定)
# ----------------------------------------------------------------
def get_context(query, tribe):
if not supabase:
return "尚無相關權威辭典與語法參考資料。"
try:
db_tribe_clean = tribe
db_tribe_full = TRIBE_MAPPING.get(tribe, tribe)
# 🎯 步驟 1:直接從 Supabase 撈取該族群的所有黃金語料 (不走 RPC 向量,改走純文字精準撈取)
response = supabase.table("lang_knowledge")\
.select("category, content")\
.in_("tribe", [db_tribe_clean, db_tribe_full])\
.execute()
records = response.data if response.data else []
if not records:
logger.info(f"[{tribe}語] 文字雷達:資料庫中目前無該族群的任何原始語料。")
return "尚無相關權威辭典與語法參考資料。"
# 🎯 步驟 2:核心文字雷達演算法(剝離標點符號,計算關鍵字元重疊比例)
clean_query = re.sub(r'[\s,。!?、??!\-_=\.<>]', '', query)
query_chars = set(clean_query)
scored_records = []
for item in records:
content = item.get('content', '')
# 計算使用者輸入的漢字/字元出現在此條語料中的數量
match_count = sum(1 for char in query_chars if char in content)
if match_count > 0:
score = match_count / len(query_chars) if query_chars else 0
scored_records.append((score, item))
# 依重疊分數從高到低進行語言學權重排序
scored_records.sort(key=lambda x: x[0], reverse=True)
# 僅篩選出重疊率大於 15% 的高價值精準小抄,最多取 10 筆
top_records = [item for score, item in scored_records if score >= 0.15][:10]
if not top_records:
logger.info(f"[{tribe}語] 文字雷達掃描完畢,但輸入語意與現存語料的特徵重疊度過低。")
return "尚無相關權威辭典與語法參考資料。"
academic_data = []
dictionary_data = []
for item in top_records:
cat = str(item.get('category', '')).strip()
content = item['content']
if "學術" in cat or "四行" in cat:
academic_data.append(f"【學術語法規範】: {content}")
else:
dictionary_data.append(f"【辭典詞彙參考】: {content}")
# 混合戰術:學術上限 5 句,剩餘由辭典補齊
final_context = academic_data[:5] + dictionary_data[:(10 - len(academic_data[:5]))]
logger.info(f"🎉 [文字雷達 RAG 大通車] 成功繞過 Google API,精擺打撈出 {len(final_context)} 筆核心對齊語料!")
return "\n\n".join(final_context)
except Exception as e:
logger.error(f"文字雷達 RAG 檢索徹底失敗: {e}")
return ""
# ----------------------------------------------------------------
# 2. 構詞語法分析引擎 (支援長短句智慧分流與 16 族文法隔離)
# ----------------------------------------------------------------
def get_structured_analysis(query, tribe):
context = get_context(query, tribe)
word_count = len(re.findall(r'\w+', query))
is_long = word_count > 15
# 💡 語言學防線分流:將太魯閣語特定的書寫規範進行隔離,避免干擾其他 15 族
if tribe == "太魯閣":
LINGUISTIC_RULES = """
【🛑 核心語言學與數詞規範】(最高優先級):
1. 構詞拆解:綴詞必須使用 '-' (例如 m-usa),代名詞連綴必須使用 '=' (例如 bubu=mu),完成貌必須使用 '<en>' (例如 q<en>pah)。
2. 數詞系統必須遵循族語習慣:40 必須為 mspatul,10 為 maxal。嚴禁使用生硬的拼湊(如 spac kmxalan)。
3. 遵守書寫規範 2.0 (單字中 o 轉 u)。
"""
else:
LINGUISTIC_RULES = f"""
【🛑 核心語言學與數詞規範】(最高優先級):
1. 構詞拆解:必須嚴格符合當前「{tribe}語」的官方現代構詞、標點與連字號書寫規範。
2. 語法結構分析與中文翻譯必須 100% 精準對齊「{tribe}語」的傳統生活語感與文化脈絡,絕對不准套用太魯閣語、泰雅語或其他無關族語的文法規則!
"""
if is_long:
# 長句模式:純文字標籤輸出(防 JSON 破碎)
model = genai.GenerativeModel('models/gemini-3.5-flash')
prompt = f"""
你是一位{tribe}語權威語言學家。請針對這段長難句進行深度解析。
原文:「{query}」
【🛑 嚴格限制】:
1. 絕對禁止在說明中提到其他無關族語,一律以「{tribe}語」進行解析與說明。
2. 翻譯與分析必須絕對忠實,禁止修改原文語感。
3. 🛑【禁止擅自推翻小抄】進行深度解析時,語意核心「必須」與【參考資料】(小抄)完全一致!絕對禁止自行腦補出與原句核心題意(如:刺自己)毫不相干的華語解釋(如:獨立、現代等)!
{LINGUISTIC_RULES}
請嚴格按照以下標籤格式輸出:
【分析結果】:(這裡放條列式解釋,必須包含原文、構詞、語法、翻譯的分析)
【中文原文】:{query}
參考資料:
{context}
"""
try:
response = model.generate_content(prompt, generation_config={"temperature": 0.0})
text = response.text
explanation = text.split("【分析結果】:")[-1].split("【中文原文】:")[0].strip()
explanation = explanation.replace("**", "").replace("'", "")
return {"is_long": True, "explanation": explanation, "translation": query}
except:
return {"is_long": True, "explanation": "⚠️ 解析過於精密,請嘗試拆分句子。", "translation": query}
else:
model = genai.GenerativeModel(
'models/gemini-3.5-flash',
generation_config={"temperature": 0.0, "response_mime_type": "application/json"}
)
prompt = f"""
你是一位{tribe}語權威語言學家。針對使用者輸入產出符合學術「四行分析(Interlinear Glossing)」規範的精確 JSON。
使用者輸入:「{query}」
【🛑 嚴格處理步驟與限制】:
1. 必須完全、唯一使用「{tribe}語」進行解析,回覆內容中絕對不准夾帶或提到其他族語。
2. 若輸入中文,請務必先將其精準翻譯為「{tribe}語」後再進行拆解。
3. 🛑【字義與解說防叛變鐵律】在拆解 line3(華語解釋)與填寫 explanation(綜合重點解說)時,所有字詞含意與意譯方向「必須」嚴格忠實於【參考資料】(小抄)的核心題意!絕對禁止自作聰明推翻小抄,或盲目腦補出與原句(如:刺自己)毫無關聯的離譜華語字面意思(如:獨立、現代、各做各的等)!
【🛑 四行分析欄位鋼鐵定義】:
- line1 (第一行):【族語書寫文字】。請填入該{tribe}語句子的單字原形拆解。
- line2 (第二行):【族語構詞語法分析 (仍為族語文字)】。請保留族語文字,並精準標出前綴、中綴、連字號等構詞結構。
- line3 (第三行):【構詞語法分析 (華語解釋)】。必須完全使用「繁體中文」說明該單字或綴詞的語法功能或字義(如:焦點標記、主格、過去貌、喜歡、家)。【絕對禁止出現英文或國際語言學縮寫(如 FOC, NOM, AF)】!
- translation (第四行):【句子華語翻譯】。請將整句話翻譯成最自然流暢的完整華語/中文句子。
3. 必須包含 'glossing' 結構,且 line1, line2, line3 必須是長度完全一致的陣列。
{LINGUISTIC_RULES}
【JSON 標準格式範例】:
{{
"is_long": false,
"glossing": {{
"line1": ["單字1", "單字2"],
"line2": ["構詞1", "構詞2"],
"line3": ["華語解釋1", "華語解釋2"]
}},
"explanation": "針對此句子語法的綜合重點解說內容",
"translation": "整句句子的華語完整翻譯"
}}
參考資料:
{context}
"""
try:
response = model.generate_content(prompt)
clean_text = response.text.replace("```json", "").replace("```", "").strip()
data = json.loads(clean_text)
if "glossing" not in data:
data["glossing"] = {
"line1": data.get("original", []),
"line2": data.get("morphemes", []),
"line3": data.get("gloss", [])
}
lines = ["line1", "line2", "line3"]
max_l = max([len(data["glossing"].get(l, [])) if isinstance(data["glossing"].get(l), list) else 0 for l in lines] + [1])
for l in lines:
if l not in data["glossing"] or not isinstance(data["glossing"][l], list):
data["glossing"][l] = []
while len(data["glossing"][l]) < max_l:
data["glossing"][l].append("")
if 'explanation' in data:
data['explanation'] = data['explanation'].replace("**", "").replace("'", "")
# 🎯【核心修正】如果使用者輸入的句子本身就包含華語(中文),強制將第四行校正為該中文!
# 這樣不論 Gemini 怎麼腦補,第四行都絕對會是整齊的華語翻譯。
if re.search(r'[\u4e00-\u9fff]', query):
data['translation'] = query
data['is_long'] = False
return data
except Exception as e:
logger.error(f"JSON 解析錯誤: {e}")
return {"is_long": True, "explanation": "自動轉為文字模式解析。", "translation": query}
# ----------------------------------------------------------------
# 3. 深度文化翻譯潤飾引擎 (經典三大防線 + 思考消音咒)
# ----------------------------------------------------------------
def deep_translation(source_text, target_text, tribe, user_api_key=None):
# 🎯 優先採用傳入的 user_api_key,否則備退至系統環境變數 config.GEMINI_KEY
api_key_to_use = user_api_key.strip() if (user_api_key and isinstance(user_api_key, str) and user_api_key.strip()) else config.GEMINI_KEY
if not api_key_to_use:
return (
"🔑【需要輸入 API Key】\n\n"
"「深度文化潤飾」功能需使用 Google Gemini AI 進行分析。\n"
"請點擊按鈕輸入您的 Gemini API Key 即可開啟服務。"
)
context = get_context(source_text + " " + target_text, tribe)
try:
genai.configure(api_key=api_key_to_use)
model = genai.GenerativeModel(
'models/gemini-3.5-flash',
generation_config={"temperature": 0.0, "max_output_tokens": 4000}
)
prompt = f"""
你是一位平易近人的{tribe}語翻譯專家。請參考資料對初稿進行深度潤飾與校正。
【🛑 第一防線:語意與防腦補絕對限制】(最高優先級)
1. 絕對忠實原文:翻譯內容必須 100% 貼合【原翻譯(華語)】的語意(如:這個學生自己刺自己)。原文沒有的主詞、時間或情境,【絕對禁止】自行添加!
2. 🚨【強制摧毀一般翻譯的詞彙幻覺】🚨:一般翻譯(初稿)極度不可靠,經常出現嚴重的字詞幻覺(例如將『刺』錯譯為『迷路 mataohtaoh』)。你「絕對不准」盲目保留一般翻譯的動詞結構!
3. 🚨【鋼鐵文法套用鐵律】🚨:你必須強制採用【參考資料】(小抄)中提供的高階權威反身文法骨架(如:Kis’anak’anak a ... a kislaupa)!請保持這個神級文法公式,僅根據使用者輸入的中文,將主語智慧替換為學生,並將指示代詞精準修正為「這個」(布農語中『這個學生』請使用一般翻譯對齊正確的 Maza andii isnanavaan 或相關正確詞彙),但動詞核心絕不准被一般翻譯帶偏!
【🛑 第二防線:RAG 詞彙與領域限制】:
1. 你的潤飾與選詞「必須」以底下的【參考資料】為依據進行事實查核。
2. 嚴禁混入其他不相干族語的任何詞彙!當前為{tribe}語模式,回覆內容中【絕對不准】出現 any 太魯閣語、泰雅語或其他無關語別的詞彙、文法或說明!
【🛑 第三防線:格式與品質禁令】:
1. 嚴禁使用 ** 或 ` 或 ' 符號。
2. 必須遵守{tribe}語的官方書寫規範。
3. 說明必須白話、簡短,只要指出差異與簡單原因即可。
4. 🛑【極重要(消音咒)】嚴禁在最終回覆中夾帶 any 英語、內部碎碎念、或自我辯證過程。你必須直接且唯一輸出規定的中文與族語標籤模板!
請嚴格依照以下模板輸出(直接填寫內容,嚴禁修改標題):
【原翻譯(華語)】
{source_text}
【一般翻譯】
{target_text}
【深度翻譯】
(填入以小抄 Kis’anak’anak 為骨架,並精準將指示詞校正為『這個』的權威{tribe}語)
【潤飾說明】
1. (明確指出將一般翻譯的錯誤動詞,修正為源自學術小抄的正宗反身結構與動詞。)
2. (說明如何精準對齊『這個學生』的指示代詞。)
參考資料:
{context}
"""
response = model.generate_content(prompt)
res = response.text.strip().replace("**", "").replace("`", "").replace("'", "")
return res
except Exception as e:
err_msg = str(e)
logger.error(f"Gemini API 深度潤飾失敗: {err_msg}")
# 🎯 人性化中文攔截,不讓生硬英文嚇到使用者
if "API_KEY_INVALID" in err_msg or "400" in err_msg or "not valid" in err_msg.lower():
return (
"⚠️【API Key 驗證失敗】\n\n"
"您所輸入的 Google Gemini API Key 無效或已過期。\n"
"請點擊下方「🔑 更換 API Key」按鈕重新輸入有效的金鑰。"
)
elif "QUOTA_EXCEEDED" in err_msg or "429" in err_msg or "RESOURCE_EXHAUSTED" in err_msg:
return (
"⚠️【API 使用額度已達上限】\n\n"
"當前 API Key 的調用配額已用盡或請求太過頻繁。\n"
"請稍後再試,或點擊下方「🔑 更換 API Key」切換為其他金鑰。"
)
else:
return (
f"【原翻譯(華語)】\n{source_text}\n\n"
f"【一般翻譯】\n{target_text}\n\n"
f"💡【話意說明】:服務連線暫時無回應,已為您保留初稿結果。(錯誤簡訊: {err_msg[:80]})"
) |