Spaces:
Sleeping
Sleeping
feat: Use is_api_key_error flag from backend + onclick-based button for reliable change key UX
bc7bdb7 | # visual_renderer.py (滿血防豆腐塊、雙字型分流完全體) | |
| from PIL import Image, ImageDraw, ImageFont | |
| import os | |
| def draw_glossing_card(data, output_path): | |
| """ | |
| 將四行分析畫在畫布上 | |
| 包含動態寬度計算,防止長句被截斷(支援雙字型分流防亂碼) | |
| """ | |
| font_size = 32 | |
| font_zh_path = "NotoSansTC-Regular.ttf" | |
| # 💡 核心改動:定義高階拉丁符號(族語專用)的字型候補清單 | |
| # Linux 伺服器通常內建 DejaVuSans 或 LiberationSans,地端 Windows 則有 arial | |
| font_ind_candidates = [ | |
| "DejaVuSans.ttf", | |
| "LiberationSans-Regular.ttf", | |
| "arial.ttf", | |
| "Arial.ttf", | |
| "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" | |
| ] | |
| # 1. 載入中文大腦 | |
| try: | |
| font_zh = ImageFont.truetype(font_zh_path, font_size) | |
| except Exception as e: | |
| print(f"⚠️ 找不到中文字型或載入失敗: {e}") | |
| font_zh = ImageFont.load_default() | |
| # 2. 載入族語拉丁大腦(尋找認得 ʉ 和 ’ 的字型) | |
| font_ind = None | |
| for f_path in font_ind_candidates: | |
| try: | |
| font_ind = ImageFont.truetype(f_path, font_size) | |
| print(f"✅ 族語圖卡成功啟用特殊符號防彈字型: {f_path}") | |
| break | |
| except: | |
| continue | |
| if font_ind is None: | |
| print("⚠️ 未找到特殊拉丁字型,自動降級使用中文字型(可能出現豆腐塊)") | |
| font_ind = font_zh | |
| # 建立一個臨時畫布來精準測量文字寬度 (相容不同版本的 Pillow) | |
| temp_img = Image.new('RGB', (1, 1)) | |
| temp_draw = ImageDraw.Draw(temp_img) | |
| def get_text_width(text, font_obj): | |
| if hasattr(temp_draw, 'textlength'): | |
| return temp_draw.textlength(text, font=font_obj) | |
| elif hasattr(temp_draw, 'textbbox'): | |
| return temp_draw.textbbox((0,0), text, font=font_obj)[2] | |
| else: | |
| return font_obj.getsize(text)[0] | |
| # 💡 3. 動態計算畫布所需的總寬度(依據各自使用的字型精準測量) | |
| x_offset = 80 | |
| for i in range(len(data['glossing']['line1'])): | |
| w1_len = get_text_width(data['glossing']['line1'][i], font_ind) # 族語用拉丁字型量 | |
| w2_len = get_text_width(data['glossing']['line2'][i], font_ind) # 構詞用拉丁字型量 | |
| w3_len = get_text_width(data['glossing']['line3'][i], font_zh) # 中文用中文字型量 | |
| x_offset += max(w1_len, w2_len, w3_len) + 50 # 每個單詞群組保留 50px 間距 | |
| # 計算第四行(翻譯)的寬度 | |
| trans_w = get_text_width(data['translation'], font_zh) | |
| # 決定最終畫布寬度 | |
| canvas_width = int(max(1200, x_offset + 80, trans_w + 160)) | |
| canvas_height = 500 # 固定高度 | |
| bg_color = (253, 248, 241) # 米白色紙張感 | |
| img = Image.new('RGB', (canvas_width, canvas_height), color=bg_color) | |
| draw = ImageDraw.Draw(img) | |
| # 💡 4. 開始分流繪製 | |
| curr_x = 80 | |
| y_start = 100 | |
| line_spacing = 80 # 四行等距 | |
| for i in range(len(data['glossing']['line1'])): | |
| w1 = data['glossing']['line1'][i] | |
| w2 = data['glossing']['line2'][i] | |
| w3 = data['glossing']['line3'][i] | |
| # 🚀 第一、二行強行注入 font_ind,徹底封殺 ʉ 與 ’ 的豆腐塊 | |
| draw.text((curr_x, y_start), w1, fill=(0,0,0), font=font_ind) | |
| draw.text((curr_x, y_start + line_spacing), w2, fill=(139,0,0), font=font_ind) | |
| # 🚀 第三行回歸 font_zh,確保中文解說完美顯示 | |
| draw.text((curr_x, y_start + line_spacing * 2), w3, fill=(85,85,85), font=font_zh) | |
| # 推進 X 座標到下一個單詞的起點 | |
| bbox_w1 = get_text_width(w1, font_ind) | |
| bbox_w2 = get_text_width(w2, font_ind) | |
| bbox_w3 = get_text_width(w3, font_zh) | |
| curr_x += max(bbox_w1, bbox_w2, bbox_w3) + 50 | |
| # 🚀 第四行回歸 font_zh 畫出整句意譯 | |
| draw.text((80, y_start + line_spacing * 3.5), data['translation'], fill=(0,0,0), font=font_zh) | |
| img.save(output_path) | |
| return output_path |