# live_engine — 即時聊天室訊息疊加畫面 + 表情 + TTS 朗讀
透明穿透的直播聊天室 Overlay,接收 TCP 訊息後以 OpenGL 渲染在畫面上,支援內嵌圖片 URL 解析、系統字型選擇、觀眾人數、直播計時器,並可透過 edge-tts 朗讀訊息。
---
## 功能
- **透明疊層視窗** — 無邊框、支援點擊穿透/拖曳調整位置
- **頭像 + 用戶名 + 內嵌表情 + 禮物圖示** 渲染
- **文字自動換行**,可選系統字型
- **訊息平滑動畫** — 進場、滾動、淡出
- **超出視窗自動滾動** — 最新訊息保持在可見區域
- **內嵌圖片 URL 解析** — 自動下載並渲染訊息中的 HTTP(S) 圖片;也接受本機解碼的 png/jpeg/webp/gif data URI(單張上限 256 KiB),不發出 HTTP
- **非阻塞圖片載入** — 頭像/禮物/表情的網路下載與解碼都在背景下載執行緒(同時最多 4 個),繪製執行緒只負責上傳 GL 紋理(每幀最多 4 張),不會卡住畫面
- **紋理 LRU 快取** — 頭像/禮物/表情共用最多 300 張的 LRU,長時間直播不會無限成長;表情另有**本地磁碟快取**(`cache/emojis/`)跨啟動復用,同 URL 不下載第二次
- **表情跟隨字體** — 大小比例跟隨字體設定(font_size × 1.6),不再寫死
- **觀眾人數顯示** — 從 `userNum` / `userList` 欄位讀取,顯示於疊層頂部
- **直播計時器** — Socket 連線自動啟動、斷線停止,可手動控制
- **系統托盤** — 懸浮圖示,滑鼠右鍵選單管理疊層與設定視窗
- **設定視窗縮小到托盤** — 主程式內關閉按鈕(X)改為隱藏到托盤而非結束程式(獨立 `--settings` 模式則關閉即結束)
- **自訂全域快捷鍵** — 預設 `Ctrl+Alt+F9`,可在 疊加層設定→基本 修改或清除;使用 Win32 `RegisterHotKey`(無鍵盤鉤子,不觸發防作弊誤判),開啟 TTS 設定視窗(與托盤共用同一視窗,不另開進程)
- **防作弊自動讓路(選用,預設關閉)** — 偵測到 ACE / EasyAntiCheat / Vanguard 等防作弊程式**且有全螢幕遊戲在前景**時,自動隱藏疊層並停用全域快捷鍵;結束後自動恢復,並以托盤通知提示
- **TTS 朗讀** — 使用 Microsoft Edge 雲端語音(edge-tts)
- **語音過濾器** — 排除/替換關鍵字、移除 URL、Emoji、純數字
- **佇列管理** — 佇列上限、滿載策略(跳過/停止舊/清空)
- **可調參數** — 語速、音量、語音、最小/最大字數
- **疊加層設定 GUI** — 五頁分頁:基本(字型、大小、快捷鍵)、間距、位置(拖曳)、計時器、贊助橫幅(樣式自訂)
- **贊助橫幅 (AdOverlay)** — 接收 `AdOverlay` 訊息顯示個性化贊助橫幅:圓形頭像 + 用戶名 + 內嵌表情與自動換行文字,可自訂背景/邊框/文字顏色、頭像大小與垂直微調、顯示時長;`iconURL` 與文字圖片也接受 data URI;顯示期間聊天訊息自動讓路並暫停清理,結束後滑回原位
- **字型選擇** — GDI EnumFontFamiliesW 列舉系統所有已安裝字型
- **pytest 測試** — 核心邏輯 62 項單元測試
---
## 目錄結構
```text
live_engine/
├── main.py # 進入點
├── config.py # 預設設定
├── README.md # 本文件
├── requirements.txt # 依賴套件
├── shaders/
│ ├── sdf_font.vert # SDF 字型頂點著色器
│ └── sdf_font.frag # SDF 字型片段著色器
├── renderer/
│ ├── overlay.py # 透明疊層視窗 (QOpenGLWidget)
│ ├── font_system.py # 文字 → OpenGL 紋理
│ ├── texture_loader.py # 非阻塞圖片下載/解碼 + GL 紋理 LRU 快取
│ ├── gl_renderer.py # 背景方塊繪製
│ └── shader.py # GLSL 著色器編譯
├── core/
│ ├── engine.py # 主更新迴圈、佈局計算、計時器、觀眾人數
│ ├── scene.py # ChatNode 資料模型(含分段解析)
│ ├── ad_overlay.py # AdOverlayNode 贊助橫幅資料模型與排版
│ ├── emoji_parser.py # 訊息中圖片 URL 擷取與分段
│ ├── tts.py # edge-tts 語音朗讀服務
│ ├── speech_filter.py # 語音過濾器(關鍵字、URL、Emoji)
│ ├── hotkey.py # 全域快捷鍵(RegisterHotKey,無鉤子)
│ └── debug_log.py # 日誌系統
├── gui/
│ ├── system_tray.py # 系統托盤(右鍵選單、雙擊開啟設定)
│ ├── overlay_settings.py # 疊加層設定(大小/位置拖曳/計時器)
│ ├── tts_settings_window.py # TTS 設定視窗
│ ├── filter_settings.py # 過濾器管理視窗
│ └── settings_manager.py # JSON 設定讀寫
├── network/
│ └── socket_server.py # TCP Server (port 9322:keepalive/閒置斷線/連線上限)
├── tests/
│ ├── conftest.py # QApplication fixture(供 QFontMetrics 測試)
│ ├── test_emoji_parser.py
│ ├── test_scene.py
│ ├── test_overlay_config.py
│ ├── test_speech_filter.py
│ ├── test_texture_loader.py # data URI 本機解碼與大小限制
│ └── test_ad_overlay.py # 贊助橫幅節點與引擎讓路/暫停清理測試
├── config/
│ ├── tts_settings.json # TTS 設定(執行期自動產生)
│ ├── tts_filter.json # 過濾規則(執行期自動產生)
│ ├── overlay_settings.json # 疊加層位置/大小(執行期自動產生)
│ └── overlay_settings.example.json # 預設值範本(版控內)
└── logs/
└── live_engine.log # 執行日誌(自動產生)
```
---
## 安裝
```bash
pip install -r requirements.txt
```
---
## 執行
### 疊層視窗 + 訊息接收
```bash
python main.py
```
### 僅開啟 TTS 設定
```bash
python main.py --settings
```
此為獨立進程模式:視窗按 **X** 關閉時會**真正結束進程**(而非隱藏),不會殘留背景 python 程序。
---
## 系統托盤
啟動後工作列通知區域會出現藍色 **L** 圖示:
| 操作 | 行為 |
| ------------- | --------------------- |
| 雙擊圖示 | 開啟疊加層設定視窗 |
| 右鍵 → 隱藏/顯示疊加層 | 切換疊層可見性 |
| 右鍵 → 疊加層設定 | 基本、間距、位置、計時器、贊助橫幅五頁設定 |
| 右鍵 → TTS 朗讀設定 | 語音參數與佇列設定 |
| 右鍵 → 過濾器設定 | 關鍵字過濾與排除規則 |
| 右鍵 → 關閉 | 結束整個應用程式 |
在完整應用程式(`python main.py`)內,所有設定視窗(疊加層、TTS、過濾器)點擊 **X** 按鈕時會縮小到系統托盤而非關閉,可從托盤選單重新開啟;若以獨立模式(`--settings`)啟動,關閉即結束程式。
---
## 疊加層設定 GUI
可從系統托盤 → 疊加層設定 開啟,分為五個頁籤:
### **基本**
- **寬度/高度** — 視窗尺寸調整(立即生效,自動儲存)
- **字型** — 系統所有已安裝字型(可自訂輸入),生效時清除字型快取
- **字體大小** — 訊息字體大小 10~30px
- **全域快捷鍵** — 點擊後按下組合鍵即可錄製(需含 Ctrl/Alt/Shift/Win;Esc 取消、Del 清除),預設 Ctrl+Alt+F9
- **防作弊自動讓路** — 勾選後,偵測到防作弊程式且有全螢幕遊戲在前景時,自動隱藏疊層並停用全域快捷鍵(預設關閉)
- **[開啟 TTS 朗讀設定]** — 直接跳轉 TTS 設定頁面,無需熱鍵或托盤
### **間距**
- **行間距** — 節點與節點之間的垂直間距 (0~30px)
- **內容間距** — 用戶名與訊息文字之間的垂直間距 (0~20px)
### **位置**
- **啟用拖曳調整位置** — 勾選後關閉點擊穿透,可用滑鼠拖動疊層視窗,顯示藍色邊框
- **儲存目前位置 / 重設預設位置** — 手動管理視窗座標
### **計時器**
- 顯示目前計時(HH:MM:SS),可手動開始/停止/重設
### **贊助橫幅**
- **基本** — 顯示時長(3~120 秒,預設 10)、字體大小(10~30px,預設 16)、頭像大小(20~80px,預設 40)、顯示頭像開關、頭像垂直微調(-20~+20px,正值下移)
- **樣式** — 背景顏色、邊框/強調色、用戶名顏色、訊息文字顏色(顏色選擇器即時預覽)、背景不透明度(0~100%)、恢復預設樣式按鈕
- 所有設定即時套用並存入 `config/overlay_settings.json`
---
## 通訊協定
Socket port **9322**,每行一個 JSON:
```json
{"user":"觀眾名","message":"哈囉","img":"頭像網址","giftImg":"禮物網址","type":"StreamMessage","isMain":true,"userNum":0,"userList":[]}
```
> **Socket 綁定位址**:預設綁定 `127.0.0.1`(僅本機)。需要讓其他裝置連入時,可透過環境變數
> `SOCKET_HOST=0.0.0.0`(所有介面)或指定特定 IP,例如 `SOCKET_HOST=192.168.1.50`;
> 埠號可用 `SOCKET_PORT` 覆寫(預設 9322)。
>
> **Socket Server 行為**(`network/socket_server.py`):
>
> - 綁定使用 `SO_REUSEADDR`;若埠被佔用,會寫入錯誤日誌並每 5 秒重試(同時送出 `SystemEvent` `socket_error`,引擎會記錄),不會靜默失效。
> - 每 20 秒對連線送出 `{"type":"keepalive"}`;超過 60 秒未收到任何資料的連線會被主動關閉(半開連線不會讓連線計數卡住)。
> - 同時最多接受 8 條連線,超出者直接拒絕並記錄。
> - 收到的訊息放入上限 200 筆的佇列,滿載時淘汰最舊資料,並以每 10 秒節流記錄一次。
>
| 欄位 | 說明 |
| ------ | ------ |
| `user` | 用戶名稱 |
| `message` | 訊息內容(含圖片 URL 時自動解析為內嵌表情) |
| `img` | 頭像圖片 URL |
| `giftImg` | 禮物圖片 URL |
| `type` | `"StreamMessage"` 才會觸發 TTS |
| `isMain` | 是否為主訊息 |
| `userNum` | 觀眾人數(顯示於疊層頂部) |
| `userList` | 觀眾列表(有提供時顯示其長度為人數) |
### 贊助橫幅 (AdOverlay)
獨立於聊天訊息之外的贊助橫幅:
```json
{"type":"AdOverlay","user":"贊助者","text":"歡迎光臨 https://…/emoji.png","iconURL":"https://…/avatar.png","useTTS":true}
```
| 欄位 | 說明 |
| ------ | ------ |
| `user` | 橫幅顯示的用戶名稱 |
| `text` | 橫幅文字(內嵌圖片 URL 自動解析為表情、自動換行;獨立 token `useTTS` 會被過濾不顯示) |
| `iconURL` | 橫幅頭像圖片 URL(圓形裁切) |
| `useTTS` | 是否朗讀此句(true 時朗讀,跳過最短長度限制) |
收到時橫幅顯示於疊層頂部(header 下方),聊天訊息自動下移讓路並**暫停自動清理**(訊息 TTL 不計時、不因溢出被清除);橫幅時間到後訊息滑回原位並恢復自動清理。設定於 設定 → 疊加層設定 → **贊助橫幅** 分頁。
### 圖片 URL 解析
`message` 欄位中的 `png`/`jpg`/`jpeg`/`gif`/`webp` 圖片 URL 會被自動偵測,下載後內嵌顯示於對話中(大小跟隨字體設定)。所有圖片共用最多 300 張的 LRU 快取,避免重複下載與無限成長。
### 系統事件(自動計時)
Socket 連線/斷線時引擎會自動觸發計時器啟停,無需手動操作。
---
## 全域快捷鍵
預設 **Ctrl+Alt+F9** → 開啟 TTS 設定視窗。可在 疊加層設定 → **基本** 分頁點擊「全域快捷鍵」按鈕後,按下想要的組合鍵來修改;按 `Del` 可清除(停用全域快捷鍵),`Esc` 取消錄製。組合鍵需包含 Ctrl/Alt/Shift/Win 其中至少一個(例如 `Ctrl+Shift+8`、`Ctrl+Alt+R`)。
> **為什麼不能用「R 再按 8」這種連續鍵或純雙鍵和弦?** 本程式使用 Windows 內建的 `RegisterHotKey` 註冊全域快捷鍵,**不安裝鍵盤鉤子**。鉤子會攔截全機所有按鍵,正是防作弊(ACE 等)判定為鍵盤側錄/駭客工具的主因;`RegisterHotKey` 只會回報你註冊的那一組組合,因此不會被誤判。代價是它只支援「修飾鍵 + 單一主鍵」的形式。
熱鍵與系統托盤共用同一個設定視窗(in-process),重複按下只會重新顯示同一視窗,**不會**另外 spawn 新的 python 進程;關閉視窗(主程式內)只是隱藏,可隨時從熱鍵或托盤再度開啟。若註冊失敗(與其他程式衝突),會跳出提示,托盤圖示仍可開啟設定。
---
## 防作弊自動讓路(選用)
疊加層若與遊戲畫面重疊,可能被防作弊(ACE、EasyAntiCheat、Vanguard…)判定為外掛疊層。開啟此選項後,程式會**只讀取行程清單與前景視窗**偵測防作弊:
- 偵測到已知防作弊程式,**且**有全螢幕遊戲在前景時 → 自動**隱藏疊層**並**停用全域快捷鍵**(`UnregisterHotKey`)。
- 遊戲結束、不再是全螢幕前景時 → 自動**恢復**疊層與快捷鍵。
- 狀態切換時會以**系統托盤通知**提示一次,托盤 tooltip 也會反映目前狀態。
- 額外的「全螢幕」條件是為了避免像 Vanguard 的 `vgtray.exe` 這種常駐程式讓疊層永久隱藏。
此機制**不裝鉤子、不注入遊戲、不開遊戲行程的 handle**,純粹是「看到防作弊+全螢幕遊戲時自己閃開」。設定位置:疊加層設定 → **基本** 分頁。**預設關閉**。
---
## TTS 設定
可在 GUI 中調整:
| 頁籤 | 項目 |
| --- | --- |
| 一般 | 啟用開關、只讀主訊息、朗讀用戶名、中間詞、打斷 |
| 聲音 | 語音選擇、語速 (-100~+100%)、音量 (-100~+100%) |
| 限制 | 最小字數、最大字數、佇列上限、滿載動作 |
### 佇列滿載三種行為
1. **跳過新訊息** — 直接丟棄
2. **停止舊的,朗讀最新的** — 打斷當前朗讀,新的接上
3. **清空全部佇列** — 停止播放 + 清除計數 + 朗讀新的
### 記憶體播放(不寫入磁碟)
edge-tts 合成的音訊透過 `Communicate.stream()` 串流累積在記憶體(`io.BytesIO`),再由 pygame 直接從記憶體播放,**全程不產生任何臨時檔案**。
> **舊版本提醒**:早期版本會將語音暫存為 `%TEMP%\tmp*.mp3` 再播放,且 Windows 上 pygame 播放期間鎖住檔案導致清理失敗,會在 `C:\Users\<你>\AppData\Local\Temp` 留下大量殘留 mp3(約 10~35KB 一個)。請檢查該資料夾,如見 `tmp*.mp3` 可放心刪除,此後新版本已不再產生。
---
## 過濾器
可在 GUI 中管理:
- 排除關鍵字(完全移除)
- 替換關鍵字(取代為指定文字)
- 移除 URL
- 移除 Emoji
- 移除純數字
---
## 設定檔
| 檔案 | 說明 |
| ------ | ------ |
| `config/tts_settings.json` | TTS 設定(GUI 修改後自動儲存) |
| `config/tts_filter.json` | 過濾規則(GUI 修改後自動儲存) |
| `config/overlay_settings.json` | 疊加層位置、大小、字型、間距、贊助橫幅時長與樣式(修改立即儲存) |
| `config/overlay_settings.example.json` | 預設值範本(版控內)。程式預設值唯一來源是 `gui/overlay_settings.py` 的 `DEFAULT_OVERLAY_CONFIG`,此範本與其一致;`overlay_settings.json` 缺檔時即以此為基準 |
設定修改後引擎會自動偵測並同步,無需重啟。
---
## 日誌
所有事件寫入 `logs/live_engine.log`,包含時間戳。除一般事件外,圖片下載失敗、socket 綁定失敗(含重試)、佇列滿載與連線進出都會記錄,方便事後追查。可搭配 `--settings` GUI 進行除錯。
---
## 測試
```bash
python -m pytest tests/
```
執行 62 項單元測試,涵蓋:
| 模組 | 測試內容 |
| ------ | ---------- |
| `test_emoji_parser` | 圖片 URL/data URI 解析、文字分段、`strip_image_urls` |
| `test_texture_loader` | data URI 本機解碼、SVG 與過大資料拒絕 |
| `test_speech_filter` | URL/Emoji 過濾、關鍵字封鎖與取代、數字過濾 |
| `test_scene` | ChatNode 資料欄位、生命週期(alpha/dead) |
| `test_overlay_config` | JSON 讀寫、預設值合併、遺漏鍵值處理 |
| `test_ad_overlay` | 贊助橫幅欄位/表情解析/`useTTS` 過濾、佈局(讓路偏移、頭像隱藏、自訂顏色)、引擎 TTL 暫停與恢復 |
### 拖曳保護
拖曳前獨立掃描已知反作弊及遊戲程序(目前遊戲名包含 `endfield.exe`、`arknightsendfield.exe`,需依實際執行檔維護)。即使程序在背景、尚未全螢幕,或未勾選「反作弊自動隱藏」,仍禁止進入拖曳。程序掃描失敗也不允許拖曳;拖曳期間每秒檢查,偵測到風險後自動退出,恢復後不自動重新進入。
位置頁會顯示紅色提示與阻擋原因。請在啟動遊戲前完成視窗位置設定;輪詢與程序名稱清單無法涵蓋所有遊戲,也不能保證消除驅動、GPU 或反作弊造成的系統問題。此變更沒有證明任何一次死機的根因。
視窗樣式存取已補上 ctypes 的 HWND/LONG_PTR 型別與錯誤檢查,只操作自身視窗;恢復點擊穿透失敗時隱藏疊層,避免留下可攔截輸入的置頂視窗。