From 9f981a0621f6aecdd9885486594b093998a89c9b Mon Sep 17 00:00:00 2001 From: z060142 Date: Fri, 18 Apr 2025 21:38:24 +0800 Subject: [PATCH 1/7] Added the function of dismissing positions. Improved the stability of chat bubble detection. --- .gitignore | 3 +- ClaudeCode.md | 29 +- config.py | 34 +- llm_interaction.py | 12 +- main.py | 37 +- templates/base.png | Bin 0 -> 13349 bytes templates/capitol/black_arrow_down.png | Bin 0 -> 1059 bytes templates/capitol/capitol_#11.png | Bin 0 -> 4734 bytes templates/capitol/close_button.png | Bin 0 -> 418 bytes templates/capitol/confirm.png | Bin 0 -> 6368 bytes templates/capitol/dismiss.png | Bin 0 -> 5198 bytes templates/capitol/page_DEVELOPMENT.png | Bin 0 -> 11335 bytes templates/capitol/page_INTERIOR.png | Bin 0 -> 10125 bytes templates/capitol/page_SCIENCE.png | Bin 0 -> 9810 bytes templates/capitol/page_SECURITY.png | Bin 0 -> 10221 bytes templates/capitol/page_STRATEGY.png | Bin 0 -> 10860 bytes templates/capitol/position_development.png | Bin 0 -> 4344 bytes templates/capitol/position_interior.png | Bin 0 -> 3907 bytes templates/capitol/position_science.png | Bin 0 -> 4181 bytes templates/capitol/position_security.png | Bin 0 -> 4329 bytes templates/capitol/position_strategy.png | Bin 0 -> 3771 bytes templates/capitol/president_title.png | Bin 0 -> 10004 bytes templates/capitol/president_title1.png | Bin 0 -> 3095 bytes templates/capitol/president_title2.png | Bin 0 -> 67381 bytes templates/corner_br_type2.png | Bin 0 -> 2251 bytes templates/corner_br_type3.png | Bin 0 -> 2631 bytes templates/corner_tl.png | Bin 1486 -> 250 bytes templates/corner_tl_type2.png | Bin 0 -> 1456 bytes templates/corner_tl_type3.png | Bin 0 -> 2172 bytes templates/keyword_wolf_lower_type3.png | Bin 0 -> 1031 bytes templates/keyword_wolf_upper_type3.png | Bin 0 -> 2096 bytes templates/positions/development.png | Bin 0 -> 2034 bytes templates/positions/interior.png | Bin 0 -> 1795 bytes templates/positions/science.png | Bin 0 -> 1573 bytes templates/positions/security.png | Bin 0 -> 2003 bytes templates/positions/strategy.png | Bin 0 -> 1545 bytes ui_interaction.py | 594 +++++++++++++++++---- 37 files changed, 584 insertions(+), 125 deletions(-) create mode 100644 templates/base.png create mode 100644 templates/capitol/black_arrow_down.png create mode 100644 templates/capitol/capitol_#11.png create mode 100644 templates/capitol/close_button.png create mode 100644 templates/capitol/confirm.png create mode 100644 templates/capitol/dismiss.png create mode 100644 templates/capitol/page_DEVELOPMENT.png create mode 100644 templates/capitol/page_INTERIOR.png create mode 100644 templates/capitol/page_SCIENCE.png create mode 100644 templates/capitol/page_SECURITY.png create mode 100644 templates/capitol/page_STRATEGY.png create mode 100644 templates/capitol/position_development.png create mode 100644 templates/capitol/position_interior.png create mode 100644 templates/capitol/position_science.png create mode 100644 templates/capitol/position_security.png create mode 100644 templates/capitol/position_strategy.png create mode 100644 templates/capitol/president_title.png create mode 100644 templates/capitol/president_title1.png create mode 100644 templates/capitol/president_title2.png create mode 100644 templates/corner_br_type2.png create mode 100644 templates/corner_br_type3.png create mode 100644 templates/corner_tl_type2.png create mode 100644 templates/corner_tl_type3.png create mode 100644 templates/keyword_wolf_lower_type3.png create mode 100644 templates/keyword_wolf_upper_type3.png create mode 100644 templates/positions/development.png create mode 100644 templates/positions/interior.png create mode 100644 templates/positions/science.png create mode 100644 templates/positions/security.png create mode 100644 templates/positions/strategy.png diff --git a/.gitignore b/.gitignore index 4d9ae04..d550f45 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .env llm_debug.log -__pycache__/ \ No newline at end of file +__pycache__/ +debug_screenshots/ \ No newline at end of file diff --git a/ClaudeCode.md b/ClaudeCode.md index 00322f3..42bb7a4 100644 --- a/ClaudeCode.md +++ b/ClaudeCode.md @@ -75,10 +75,10 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機 系統使用基於圖像辨識的方法監控遊戲聊天界面: -1. **泡泡檢測**:通過辨識聊天泡泡的角落圖案定位聊天訊息,區分一般用戶與機器人 +1. **泡泡檢測**:通過辨識聊天泡泡的左上角 (TL) 和右下角 (BR) 角落圖案定位聊天訊息。系統能區分一般用戶泡泡和機器人泡泡。**為了適應玩家可能使用的不同聊天泡泡外觀 (skin),一般用戶泡泡的偵測機制已被擴充,可以同時尋找多組不同的角落模板 (例如 `corner_tl_type2.png`, `corner_br_type2.png` 等),提高了對自訂外觀的兼容性。機器人泡泡目前僅偵測預設的角落模板。** 2. **關鍵字檢測**:在泡泡區域內搜尋 "wolf" 或 "Wolf" 關鍵字圖像 3. **內容獲取**:點擊關鍵字位置,使用剪貼板複製聊天內容 -4. **發送者識別**:通過點擊頭像,導航菜單,複製用戶名稱 +4. **發送者識別**:**關鍵步驟** - 系統會根據**偵測到關鍵字的那個特定聊天泡泡**的左上角座標,計算出頭像的點擊位置(目前水平偏移量為 -55 像素)。這確保了點擊的是觸發訊息的發送者頭像,而不是其他位置的頭像。接著通過點擊計算出的頭像位置,導航菜單,最終複製用戶名稱。 5. **防重複處理**:使用位置比較和內容歷史記錄防止重複回應 #### LLM 整合 @@ -167,6 +167,31 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機 這些優化確保了即使在複雜工具調用後,Wolfhart 也能保持角色一致性,並提供合適的回應。無效回應不再發送到遊戲,提高了用戶體驗。 +## 最近改進(2025-04-18) + +### 支援多種一般聊天泡泡外觀,並修正先前錯誤配置 + +- **UI 互動模塊 (`ui_interaction.py`)**: + - **修正**:先前錯誤地將多外觀支援應用於機器人泡泡。現已修正 `find_dialogue_bubbles` 函數,使其能夠載入並搜尋多組**一般用戶**泡泡的角落模板(例如 `corner_tl_type2.png`, `corner_br_type2.png` 等)。 + - 允許任何類型的一般用戶左上角與任何類型的一般用戶右下角進行配對,只要符合幾何條件。 + - 機器人泡泡的偵測恢復為僅使用預設的 `bot_corner_tl.png` 和 `bot_corner_br.png` 模板。 + - 這提高了對使用了自訂聊天泡泡外觀的**一般玩家**訊息的偵測能力。 +- **模板文件**: + - 在 `ui_interaction.py` 中為一般角落定義了新類型模板的路徑(`_type2`, `_type3`)。 + - **注意:** 需要在 `templates` 資料夾中實際添加對應的 `corner_tl_type2.png`, `corner_br_type2.png` 等圖片檔案才能生效。 +- **文件更新 (`ClaudeCode.md`)**: + - 在「技術實現」部分更新了泡泡檢測的說明。 + - 添加了此「最近改進」條目,並修正了先前的描述。 + +### 頭像點擊偏移量調整 + +- **UI 互動模塊 (`ui_interaction.py`)**: + - 將 `AVATAR_OFFSET_X` 常數的值從 `-50` 調整為 `-55`。 + - 這統一了常規關鍵字觸發流程和 `remove_user_position` 功能中計算頭像點擊位置時使用的水平偏移量。 +- **文件更新 (`ClaudeCode.md`)**: + - 在「技術實現」的「發送者識別」部分強調了點擊位置是相對於觸發泡泡計算的,並註明了新的偏移量。 + - 添加了此「最近改進」條目。 + ## 開發建議 ### 優化方向 diff --git a/config.py b/config.py index 13e410f..4242b66 100644 --- a/config.py +++ b/config.py @@ -27,24 +27,24 @@ exa_config_dict = {"exaApiKey": EXA_API_KEY if EXA_API_KEY else "YOUR_EXA_KEY_MI # For cmd /c on Windows, embedding escaped JSON often works like this: exa_config_arg_string = json.dumps(json.dumps(exa_config_dict)) # Double dump for cmd escaping? Or just one? Test needed. # Let's try single dump first, often sufficient if passed correctly by subprocess -exa_config_arg_string_single_dump = json.dumps(exa_config_dict) +exa_config_arg_string_single_dump = json.dumps(exa_config_dict) # Use this one # --- MCP Server Configuration --- MCP_SERVERS = { - "exa": { - "command": "cmd", - "args": [ - "/c", - "npx", - "-y", - "@smithery/cli@latest", - "run", - "exa", - "--config", - # Pass the dynamically created config string with the environment variable key - exa_config_arg_string # Use the properly escaped variable - ], - }, + # "exa": { # Temporarily commented out to prevent blocking startup + # "command": "cmd", + # "args": [ + # "/c", + # "npx", + # "-y", + # "@smithery/cli@latest", + # "run", + # "exa", + # "--config", + # # Pass the dynamically created config string with the environment variable key + # exa_config_arg_string_single_dump # Use the single dump variable + # ], + # }, "servers": { "command": "npx", "args": [ @@ -71,5 +71,5 @@ WINDOW_TITLE = "Last War-Survival Game" # --- Print loaded keys for verification (Optional - BE CAREFUL!) --- # print(f"DEBUG: Loaded OPENAI_API_KEY: {'*' * (len(OPENAI_API_KEY) - 4) + OPENAI_API_KEY[-4:] if OPENAI_API_KEY else 'Not Found'}") -# print(f"DEBUG: Loaded EXA_API_KEY: {'*' * (len(EXA_API_KEY) - 4) + EXA_API_KEY[-4:] if EXA_API_KEY else 'Not Found'}") -# print(f"DEBUG: Exa args: {MCP_SERVERS['exa']['args']}") \ No newline at end of file +print(f"DEBUG: Loaded EXA_API_KEY: {'*' * (len(EXA_API_KEY) - 4) + EXA_API_KEY[-4:] if EXA_API_KEY else 'Not Found'}") # Uncommented Exa key check +# print(f"DEBUG: Exa args: {MCP_SERVERS['exa']['args']}") diff --git a/llm_interaction.py b/llm_interaction.py index d147720..7e18ff3 100644 --- a/llm_interaction.py +++ b/llm_interaction.py @@ -165,10 +165,15 @@ You MUST respond in the following JSON format: Parameters: `names` (array of strings) Usage: Access specific entities you know exist in the graph. + **Game Actions:** + - `remove_position`: Initiate the process to remove a user's assigned position/role. + Parameters: (none) - The context (triggering message) is handled separately. + Usage: Use ONLY when the user explicitly requests a position removal AND you, as Wolfhart, decide to grant the request based on the interaction's tone, politeness, and perceived intent (e.g., not malicious or a prank). Your decision should reflect Wolfhart's personality (calm, strategic, potentially dismissive of rudeness or foolishness). If you decide to remove the position, include this command alongside your dialogue response. + 3. `thoughts` (OPTIONAL): Your internal analysis that won't be shown to users. Use this for your reasoning process. - - Think about whether you need to use memory tools or web search - - Analyze the user's question and determine what information is needed - - Plan your approach before responding + - Think about whether you need to use memory tools or web search. + - Analyze the user's message: Is it a request to remove a position? If so, evaluate its politeness and intent from Wolfhart's perspective. Decide whether to issue the `remove_position` command. + - Plan your approach before responding. **VERY IMPORTANT Instructions:** @@ -691,4 +696,3 @@ async def _execute_single_tool_call(tool_call, mcp_sessions, available_mcp_tools f"Tool: {function_name}\nFormatted Response: {json.dumps(response, ensure_ascii=False, indent=2)}") return response - diff --git a/main.py b/main.py index b7cbc1d..395b8f7 100644 --- a/main.py +++ b/main.py @@ -227,12 +227,15 @@ async def run_main_with_exit_stack(): sender_name = trigger_data.get('sender') bubble_text = trigger_data.get('text') + bubble_region = trigger_data.get('bubble_region') # <-- Extract bubble_region print(f"\n--- Received trigger from UI ---") print(f" Sender: {sender_name}") print(f" Content: {bubble_text[:100]}...") + if bubble_region: + print(f" Bubble Region: {bubble_region}") # <-- Log bubble_region - if not sender_name or not bubble_text: - print("Warning: Received incomplete trigger data, skipping.") + if not sender_name or not bubble_text: # bubble_region is optional context, don't fail if missing + print("Warning: Received incomplete trigger data (missing sender or text), skipping.") # No task_done needed for standard queue continue @@ -257,11 +260,31 @@ async def run_main_with_exit_stack(): print(f"Processing {len(commands)} command(s)...") for cmd in commands: cmd_type = cmd.get("type", "") - cmd_params = cmd.get("parameters", {}) - # 預留位置:在這裡添加命令處理邏輯 - print(f"Command type: {cmd_type}, parameters: {cmd_params}") - # TODO: 實現各類命令的處理邏輯 - + cmd_params = cmd.get("parameters", {}) # Parameters might be empty for remove_position + + # --- Command Processing --- + if cmd_type == "remove_position": + if bubble_region: # Check if we have the context + print("Sending 'remove_position' command to UI thread...") + command_to_send = { + 'action': 'remove_position', + 'trigger_bubble_region': bubble_region # Pass the region + } + try: + await loop.run_in_executor(None, command_queue.put, command_to_send) + print("Command placed in queue.") + except Exception as q_err: + print(f"Error putting remove_position command in queue: {q_err}") + else: + print("Error: Cannot process 'remove_position' command without bubble_region context.") + # Add other command handling here if needed + # elif cmd_type == "some_other_command": + # # Handle other commands + # pass + else: + print(f"Received unhandled command type: {cmd_type}, parameters: {cmd_params}") + # --- End Command Processing --- + # 記錄思考過程 (如果有的話) thoughts = bot_response_data.get("thoughts", "") if thoughts: diff --git a/templates/base.png b/templates/base.png new file mode 100644 index 0000000000000000000000000000000000000000..7fb93fa2e33b92f9cd7b316b4406dfa95ec3e849 GIT binary patch literal 13349 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 literal 0 HcmV?d00001 diff --git a/templates/capitol/black_arrow_down.png b/templates/capitol/black_arrow_down.png new file mode 100644 index 0000000000000000000000000000000000000000..824abd1f4dc6a60456596c233cfcd663e1f254a7 GIT binary patch literal 1059 zcmeAS@N?(olHy`uVBq!ia0vp^N|6k~CayA#8@b22YMV(E^)jtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!hW5;mh>{3jAFJg2T)o7U{G?R9irfOAY6b=y z`-+0Zwic-?7f?V97Du6s&rHqo20xNy} z^73-Ma$~*xqI7*jOG`_A10#JSBVC{h-Qvo;lEez#ykcdj0WPV<$wiq3C7Jno3Lpa$ zlk!VTY?Vq&GgGY664OkRQc_HHlM;bI$DBcci@Z53sG$ zJHZyRF@^QK;6Y=BifJ|s-1ivFLb#?zgfNAPdhK4(qQL2@%UdW^9l_#y>PFw>fGK)E zRPz5BOVwM=GM;wyTkk3(mws2}uL*%Ml7bs|evII}dHr*}S4>K0=aqXx`wry0v)L^5Z$N_b;uk zKU$(R@5s)Kz}im}{{FHSN@Hrs+kSMt-|g=^YTfTEPI^&&>f zKi{>Y?#H&Y4v#NCjAS0JKWoL1ZhtPKi{E^+j(4<0%GMtYDI0vE*0If-{L*C#Q|gDc zq3)H;e*5N3*#0@?jp(FlWrw*Mr=D;=v1DngWaIv4;oOhTFI#%3Ht3}9wd>bzR_|`l zRNElr`c|{W%O`19U(Z5`FIHRp`ET60arEU`vwM<}a&-qym%TnA!DKvbQ3z0wO@7bG yAF4C_Q#uTMCR_|Q(Jk(dG5haYawPQU(?7f$FU?`MY%%Bh~NelF{r5}E)Gm8!4+ literal 0 HcmV?d00001 diff --git a/templates/capitol/capitol_#11.png b/templates/capitol/capitol_#11.png new file mode 100644 index 0000000000000000000000000000000000000000..1b2c19391d7d624f8c231ee6d54279b64795c9ba GIT binary patch literal 4734 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 M07*qoM6N<$f}UVCp8x;= literal 0 HcmV?d00001 diff --git a/templates/capitol/close_button.png b/templates/capitol/close_button.png new file mode 100644 index 0000000000000000000000000000000000000000..38fcdd86fd208bb35c4f1f0425cfc0303917d153 GIT binary patch literal 418 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 i-sYuN6)Tj-as2|N@$r-+ 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 IM6N<$f-&9b#{d8T literal 0 HcmV?d00001 diff --git a/templates/capitol/page_DEVELOPMENT.png b/templates/capitol/page_DEVELOPMENT.png new file mode 100644 index 0000000000000000000000000000000000000000..927db88385903a22da05420d8124d29acd777801 GIT binary patch literal 11335 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 literal 0 HcmV?d00001 diff --git a/templates/capitol/page_SCIENCE.png b/templates/capitol/page_SCIENCE.png new file mode 100644 index 0000000000000000000000000000000000000000..5daf20759538f0ef21a50b45204b43b336b8dafa GIT binary patch literal 9810 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 literal 0 HcmV?d00001 diff --git a/templates/capitol/page_SECURITY.png b/templates/capitol/page_SECURITY.png new file mode 100644 index 0000000000000000000000000000000000000000..661740931c2e6de4d3ebc1ef65cccc2027a36a9f GIT binary patch literal 10221 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 literal 0 HcmV?d00001 diff --git a/templates/capitol/page_STRATEGY.png b/templates/capitol/page_STRATEGY.png new file mode 100644 index 0000000000000000000000000000000000000000..3e43e4ec5e67f0e60a93aa38118e92d0c6ff991a GIT binary patch literal 10860 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 RXIB6K002ovPDHLkV1gmiv%~-Z literal 0 HcmV?d00001 diff --git a/templates/capitol/position_science.png b/templates/capitol/position_science.png new file mode 100644 index 0000000000000000000000000000000000000000..e6d354d74e2f3e2fefadba5930b346e51d691ab9 GIT binary patch literal 4181 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 literal 0 HcmV?d00001 diff --git a/templates/capitol/president_title.png b/templates/capitol/president_title.png new file mode 100644 index 0000000000000000000000000000000000000000..41a8e041b0184882d1f7c04071f9579b4085b792 GIT binary patch literal 10004 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 z zqqG97in5Ju5Xuzf<9KYw`Sg~i^X#%g^8ioU6m>GV zr^OI{Tluv_T<1{o-6&A3C-h?8&uPwK&meEt#vy-c?>mwT)xb}*8O#(rH_&k0TTEl` z48Cz~e00s7X#;KB_z29`jtDqlmSTLk5^S%ktcZNiTA|(o8XR)oRUer2z)a-L^+w8v z)>1}dgW%u6$VJj}^z>Y?0aRa1Is!vv7u^twZRWya|BQ<_zHWZn?>~}qvK4X_v~+_{ z;dpVkku5o7AA$82W&1HWyDFr=EQ<#r6BV4r8J?5y%!yZ-w#p_S?zJ;oVll(C)Ec_lFeASlFIX`$^_eO$97Q-)&{#cG ze~2i>d;z9a))tazJ}P^whg{guAcem}_;>re5J$N2;WWWPsWx!XIYv}ZXpyTUw-8x6 zl#Zpy3|J*?>#g+y-_CvSH3+2dL1w|yakCVAB~68VFx|=(1D;63q{C?CWor3(1F&#c zWK~(B%&Kx3_EN(pH)ehL^5rztbOvhgPCQkdmk!{{C0+>v3yI{Ivg%mJ=F7O+qGd_1#0nopR3mM9WK@lKs1>)nX+_pX+&4`Erc~rPaU6*r}v^FujkbMdul-_=E>5wqd!%}Uf(Tj)y1-y~}W$LS0 z*+za-u{W2eFELPB?0QX_jOg>k-&x8OOVKR*fJdsa*EcE@M0qv3d2?t|Ow%YYg#SiR zf9$xM(#O$qYyQ@8^Lyv(A(BSd08TQ~Owc<1THCl$}L zNRV(}(xUyi&33sofi_0eHheKb25U8-#x}0#tldwKUGY>D4#2LE;U7^{lw;m61iW@s zFc%OI&=;J`3K3qqgAQ+yMsyEdCS3!^yst1&0SB`a5Ds;)oV2rVu4W2I+di34%Zp1e z*bq4?^qpa<25Z6lkAB$b?e^SNeoPnTzIQzrMwXbOE)V{moRp~S7AuGsajMf<$L!xk z`y@Qcim04h7AH}6zQj!2!pUC>*vgG&zstv;B7;&U9=<|`!wCF1IAzGFKS#Ygxi~$z zY;7u-r>L>X25N{MLpOUNuLVQg(hWhd*OciZqR7*qJidHi8L-lx>Br1sf{+?`S4+O> zVlGxbHz9yxGHA(u>YBwVQIZn=;Xl!>Mej}7L6|4Ru+ppem>sOz!zJ|iO=gW6x0L_fLZzAk* zr`ATIes?R8t$w~`^@hP-=#Y&M+sNMASNv)>nE^F5Z{j>_zeFge{rvg!sM_n|a(8OK zOaLGMRv1uuc<}x>Y=qzJ>7;je{gH57GEuJAr&bt1&y4x`Y6NX3CqbdM1E=%m_vhZG zW2zbGRvREKDg^bZqfLx=V z?ZQ?3e&w&d*uf5rUSJmwBQnv&3A9$TPTML(cuJmk-ttZ20uz9aht*OMnM)v( z%N--CYq@o<(>>5gJw1f$SSA|UdZ#RyC z>fR}>`8d-^V+eLbMd##MfeDl_6$a8mY4Nm-un`nhYtsiMVfkzk(<)&HX(%Fv5TWIg z>79(_PV*4N*C0|Ec6S!Mea~N9EJ!M`wwLev=j}|yJkMB9epH`xeT~+7jjPx%6C z)894YP_E)t%?T*NDwaz#0?QEapn|cYilt>&8sGha@Px~K^Ag)LM$;t=T(mA^68{kl z#veO#2SIw!SBrg>^>u%blh?5g6a2`Z?UIDkklscTSnkNAp@^!vqAJ^eSnfWeCbz1T zmdz@h=`+wC-xva1Qn{#NY2zhkW3m}VY!WEYv?3FlKw7sWRHD!v)-2N(orgXhCV4sL zXj?bn%e-VGG=hY8yPR1F{7U&^`MfxX8njs*Z}9nFcE1z$4=5VTrA2r=pyFeKu|8c+ z-^?^7B8vkwJDC&+!`$Y+1&ZVbj?3+4Ws8@Urk{=7Aw01}*p&ffYM-VYJdPj3?ZVvM zg|f$5w%&dxXjRIz#00?nfT>kASu#u|B@6cNukOm4IJPR<5eNWmkQdM_*dmatXZ>fA zH`6V&RPt^m={A?uj>AMMssRI_Io;@=$w&god)p_q#ZZ5qJyo&V8t{C=c!lvb0l6%b zbQ)5d79dG+V8WiS{qi(FRB~04MZ>M85*uSFH&-WX^i%)9wo!(Sm03n3Q%}QO1tKuG zd3FElZ`%i4eIghVtrAX92G-tiQW^bdgEkT{0iT|qpf$hSgZGUPI)s?$^TpA_j9qoY zkqHJ#tgI%5r$7YXk^ORik_b)h+rh1)<#nTGMbtOCoYeb|;*Lc3?Q>>6Hm?iqI6D50 zuy#s_FLru8IA(k_tQj2X;D8z!KoI0GKlp;4o`iuT&*tW^nnZMYB#2-}$&f3^iaVi4 zi7lqklD(){+Gmfwh|W3|v+P~tcY8Pp5X&|oWc>vdAicbDt{9D$OuumdZFBI8_Zt4K zYOuW*nubt7(hmb9U6%qr`R85QHnk9yA{wdz?gpI)BObPLRF0x@{_obBXt7-1KfN!m zfA<9ui;m+`)?k&-?KbMT>9zuFX*k2pgsRNv9QxTxTkd;wCSMNLvI0u+Q8XR4Ck7D= zs`>}O55A%iP8$4j<_b?)XdxLY3MX~!`)u)`LF*x*1r9zxUi$8VFO=*4Y=(cj zB%|w zlEbH1?Ghk_?_;R?2eqgBFr4+ytu&u4=9=9N)VMK-1I&f6YKpv}yL%qOuRa-aqrkHN zuG<^Uxa!RS+>gg1Pxg*imI%{Zs+U@ zA02@@SX+B&*bjpv)1}Y36+E9btEcy>cHZCqWDmb)9bI~w$3NK6hC5Im#GNVBy6kXL z_^fmDV*F8jbvhr#eEQhERMR@7b(7dz_U3k!^L}>quAAmD$*;zg-8PyOQNbd|S#0Q- ze+D!$uE@J|z96$t+LKiuvU*ugh`ZfK0YM;SpoTxm_|Q}PvbqS6(daL^@4aWSVlwN!w0Q6aqhr7ywqbRoAAl+ZPK_3zP5t%|-+%4&O^j7)Cla{=+ zuH7qDiTqeEitM|eil}H_oZS$xOMfJHURAzX)C%~yS4X@%wu2&_yKeRiJa?XvP-Pe3e#% zlU|Kl5u@^S^9tWa56H8x-0MKxpJ|0P2%j^Yw3MX>Au-=0yLSB-p@uuJ34NX2gJak^i)0o}A0uKE7RnA^k7)7pcdWb?(*jNi)jpXnPfjShdC+ONon zi*v3wcb|WjuKuuhVt;T8eTaTKn-L82J(d8JW^sJ=Aa6$hO_9o1hqczOU`@a$Kx<{g zBW)F5ER>f3SIw~zOp<@d?G;PcF?O0u6mI7IKykket*j4|%mP5~Xa)d-K`+Y= zilXwBdZ%x^`OY`L`PC=4&Q#NJ{oqbNPvguoVNeDUiU?OEQws|L0ZQ$;x^pkhGCvJB zQ*jY_U0`Rmb;&Uz>EarUqXQJ?QcHxBn(zyj4og-oC!7%n*RC?NGCuvv?|kWZ|NgZ% zU%mU`I|uvw)2U_gJ!1&LFq88s`y|iOdR7a2U&(9?EWe_5wz0-0DFCJksc4cK!k`sK zMfFa}m_f=+lH{f?Ah9B%jx|lt3kY-N#Cbtb@60L`|?KKsAvoWs$Je(6@QO zQm+IT8+@IX7tY7awhW=R$s1oDb*PdeNCxKLpv`FDZka9(t+D~cu$_oI-$IR_V+4p> zKT1Rs0s@Dz!L70f+ZU+O2|i3K+S2sgtgwE976Y;xH8l@W#UmUi0Jk|EEsSbz@x0^G z`o=7!L84moC29XNxXuVtC`+(ABE{E?75iGms}BR=8AKEW)Pr#Fe@4|gbRI{wivr#` zn!R^-fB&@)vON9G-~NqX|BbKyPygcI?e8C++S&lXtltNKy7I1a0I+w^gp<13I~rAv zqmqN!Gkf;2*Veqjqr-zcw?2OE`OkVkscRrQX8F)r`-!nxo)-t>OV2-N6EiwIaIU_4 z@9v%Jw~Nu#*9-t25;C-X>D2nx%E95`#(HdB?3~)!+TP}`e(CD_SHJ!He^eFa@R_Hv)iD8>@Mi2*a734FFgf900)Sb35g%5Ejo|yZZL6yS;wzqw7~E zlW}@`_w2=;{^~%3g{LaSQd*!h?i8Z+C;M5EY^`83J+HAfXmN^IW6XQ;^T2X^`8Gq6 znj>QYB*1xx4;KpTF3;!_MlOzT-u&<{|NYVKol7r%?c%d9^oG6s!*_OX-5E_vC#tTl z>ndsrC~>_tW9>YwJwcAlML-pdt6unRsN zi)WI~u~6tdFfSsofo;N+5b6*Ii7+vk1Y)9UaI0%Xi3?!AHbSrk*4Mm@!EP-g?L3Ay z`HO}tz;W_YW3(FT8P+%vBToMa^B^O`1e@_wi>j)I(ipcyLrn=441oxc8nwZ*45($x zHE5oyvWNxTNmehFKAG*KMHE88MjNvbGZyyMNp?JV9QZ<9;K?uI&`nHR_WulW)<)3? zK%mTmNQA6lqKXxyDp*nxVdsRs3JZG?#>PBAV;JHvE(M#}465oxuif20I-E?3ax$C! zv;Xm5U%qm2b8GYd{^9<31^{_Z0Kgu^gQ|`v)7W`t)*vvF2yTYSyRab4gzc@hd-eF8 zw|;W!^qH063QLUd!Wvmg4b~spLk}wdrN8^P@7=h$|Ncj_qw)U!-oe4q{)64xac}|> zCIC1X9c`c6K6Cz*O|o9E2LQ%UmL;n2{L9a7pWgW1x4!$~>+hGfd*b4`{^o`PtgNnW zpF8*JPky$twqARF^UiG$AnHhk12Sp|37NSls?pH|P=_nMp6M+eOTh;cF{nXf8W6^c z-Ptlos{UbC-YuwClt8 zvRFw+K8;5F7nG|iGn)pXwPu|gt+qw`f>ZWd`7Q3)yvU%S`~mp=ShQ{1c*H!j;6)9n7C3^r<3Br-eK+e{kK23{M_@PiU&tc zflB~dyVz;)(8Y+rFWP1_bZ1D6UO0dDXmoVr+6TS7fA-vket!r6l~)k%@DNm40pRtc z^4Y!O+%qp;d+XKv?_9le{kjYC3)P^xycs-}=LU{s*sr|3~B9D=&Td^TU;u9+>l&p7_zvUjJu5 zes!bY|H*@qHI~`|6>(O!8f%JKF&R(TGXQLCZ)S}!O*?HKVXtZd&=^ERWR^E@QP&kX z0GO7AwUi|0{$Y&7qoafMjqO)oeLFnq-fWWf%(9P7m!@Ot}zsm?w1 zrKeu`YSz#9Zhw6B{r6M>O`=tS2*!{xgaE>VE30Hkjt>al#HOp7?FCAB#tClW$c$Vp;6$(YDmEWpmu5q2uT5)Qz8rW*vOW` zlCSwOXM;!U~2EiA1d zCOQ~5j`?vC!$OB{PLFP$MCYW!1_UEQC0OW#hHi5l3)N&-F~Brk_GYF@JBL14T964n zr8%P1)=Nl03Tnt$5{<`Jm|qBp!o0q*{@gQHwvT4?;5v#!6Ww~TQHCR);J~(+613i2 z^ATCt?SmaN?O%vU?YmX&MwC{Kj}nWx5EKLz8@d7l09RycMNsjgtiVBQ5`>V#Ub&fE z6R)eFpc0_Scv{x8x~}SJQ7}trq9R~_xN@|=zqYZ_81rCDXFawb^dbGV`K4K3TkqxT zH*Z~g^R3sOdh*#`uOFxXp7gQ5zyHl|eC_D`g?k_0`r3c~cb|OX)P+muhHINqZ=C0! zedTlSzVYsN|M<`De0cTkSKk<{49;D=*j?#WRfzC!{H_1wr$71W+durt=ORlVVn|XJgIU#(KZsBcey_F6PMQI7SPLBxoWO zRv(AX!*n{H6_e9vE>v|rna--ZVhcC-51y?nvV=i1geFaK!T6su(Eht(k!!Ss=g$H zqFOmI1c2mtP9g$8$C?lha_hX3pJI80PnMFPDoLTJ>8 zObL%h6IT|K>DV~t+Sg<}kuk|&eeKqVZ|8&00_OAOj=+sL0JZKqTSpe~{GBm?2Y}jZ z%oxy~KKInh+V+PZzO}lx@!XTog@y#H3k9NUX7&y019SVGE$jaNKl)F5gFY99lx~F6 z{$>UMP*lDC;H59T?78@Mtye#|cIw8xWY{-ZvQ9~$PJu~n$|gy^^35-0YlDCLFaGGQ z8+Qk(**U%Q+^Nm~`XBtQ>;L`#=YxZ)d*c|yJ6BI9#cY!1Js{ZGK9gigDFF1~d7+_SHozPz%!@$oya*LU}*H$<(uDnXbN4+{_Kk{ zu20?MjW?;$>~t)CTNAOsygo_U)uwt2#7bP!gW<6NgfZ?du%n}*NTl`a4I`SPm@(Kq zM`%`C_7}2Kc0fkOqedU6ATe*k2{%(zIFyqC5LAU(Q5BU7Q)5BY`C4p(XpFV%J7*u< zy1sq>QaJ9Lxf;#2kwpe@OClfyj;`tRhoedp*VZ;(e))?;WUNJ9VlilD?ce)kTq|smao0ZB?4MRZgbmYhJ%XKN}8m0lfQQcl)U)|IRnQ_}~7)_kG9wRlF;zF#x1_FUfjo z-lt}O7{HQxu0w)Q2_#@ujh!QmmnJAu0f3T3CCG-fs>m_yCTil zGe?PgnCtAD%z&Y>=WB^1!k*cw@VtOt-aBzxTdM$D0;U#Y56)?ec#LFv{NP?9^nWr+ zvhXowy!yQ#>^yj269TC&UitMGzV>@re{J^dKRP;^l1!03(Ago5S;wiiem4%I?fYvfa~*l|#pm)qfq1OOF0tM_UW zBm>L~F4?g)N)2VDRg=V-jlm5dSdu}U;~VG32Lcl|&SENIwuA^Y-%N=K(3yCD`pntS zKJ$EU_h|IiJH{8L5sgxJDAft~@DY=25W67-=7cUh)`5owl~~j*Vmu%eML>l_K!P)4 zB&(?^wT-x@fh8=;UKK+jA}Zj$FgGo34*tDCXCG7DW==;@yKs=Xn!{1%gN1x*R@OH^ zzJF`2JeB16{Mc>KNjfG#Y`Q2u7;>Gsqm04K4j%H8@W1)nf8&KGE)-RL?cV)Mi2y)v z&^Kw4udOD7A=#`dXXCxSYBqJQe6Y9I{6hcZ@BDOqV9&!9@7!?E&w71hjD!Oo@#!m1 z{O|tBKmP7ty?Xch?H~N~om%1RFFyaH5AJ>Y?T?PXhO|HE4b~rF#Nvc482}aWARYjW zNs;WMwxS4t0GRP~)E^E3bT%3L@nIZ-mu3pU%uUB#p>a};-qIaX4l1B`?0qByKs&_& zmkNg^+sV1u3zj^E=e9qJQFqc>6CL;R!V~28pDw0MYxeJa2#Wf-aymLXd-jxN^qcA!u+zXYV)8Uf92Px0;s6bQOpvI#rycJF!WcWfqC*s&dYI z&%MmBC@8Ra&uXm!0OyrJL?kgtgw8X9>E{U<4k;@cgR0gj5_t^{k^lzTtMi)YD38Iq z`b9)11f8bEvNstvC{LIJ9y$mbdhIQ?6qb$sR4*?I;-f(epoV{=tRuDtf;Syv80ynH zhc7t9wC7-_1HZgwT9r|h2+oBREfcYkErN*F6(>r}+Hs14cBXGL?iYZF=2PM^Od<*L z{a{WOd+!7aXd2O>&OtflHuo~2Gp$H;{?ervuUvtTZtuSJPG$(ABaKG40c}c?HqI~Y zD}+VKdzZv@JiaVfDV@_QobaK13r7h9Gl-BdssUjDj3y|6ExUmhN?H*XRHtF+n23aW zCC=X!)xeiw5RTr#Vax_1GjkMwt8=dQjse&^)GW!e?K77i+`fM9sppQTFrpn12>=9* z=rIGHmhEK{?e`Nz3?^Q~c$HzM005smxA~uauT?%n;=-~ssSyNC9#-u|t8Wplk6^m+g=T-zkj+0lWrl+$q! z;fpUkb*%d!0056Rl+x79Y?zJ@?%%uh*_Xexzx!Y|nQBoR#1(5h?a_8#F9)82 za+61pC{shMP+{R_xnNY1DQknxF6u%SJRq0W=3MwSmmJUneQTZ#Ev)2U%!0jSz5dpF z#dvz|o45?Eslp)uBbAFd?@3Ixlf^o23TnmjYL^NA&64WSys zG$Up-h}IhKe2J3Q;xd9?FlaooN|?(L@mLhlw!a@D3Lv$U!m%?N0d%oCSu{+0RESxc zP^JW-hUP^?h71Ubsuzt`j7$E69Y3IE1l1z$hx516R6pIqM6ls$130H50^K>nI`?#s z_KAdOFocs3JHn{oV1kK3#~QtqAyOAKnytiDMx+YDf~Y_stXYy>dHTsGwzjA5eLVf} zYC?uUsWTb%VMe42mZ~QtZaBxV7QYpjI`FMWOpHeH-l8aIbYUW46ejivfJ%tc!h6jV z%!jf+k8B@=*mLRAcu21G#TIC3pmd~^1TaCx{tSl{Tct{v^) zpX~4T*EZs*YD`x~zR-Xt0%15NGDifPK;BEj3@24!hEW49r{FK|Z2rUF`icSAeQ=*U zmXX%ltlu;1n|5QfuB!392ea{!s1{{aRP}6D9Na&+KN{bg7Wc~Xqnmf0xPJ4?>v4d* zv57S%?bp>f4u8zD^vcs0c1~}8^uetU-@p09g|q+QuYKX(!SpZR{15=hkVy<6u;(_| zvj`$0a@*uyYRe-al36TGFi@G%MAM_diWcVmg9j^vb!J~or?c6ZRI|<{B8am{qhePW zBfJ<)3aRy`Qv@fGfe+2>tj+?}ITF$t#)usaH7=w+xD1$JC-KK_V`5ol(wWpdFNhx1 zQ|Ww70Po+t4L|n;{X7y}Vm9=wB20@yUF@gweja865v)+4GvnmDy z&t5{-5Fj|#vR3Dq#V0n%=pjugpvu-j=A>9PC%E4}s)N!PgVIC$D|k|?{85r0jq_WP zqDhEo%Or3dlvf!g-Oh zgfO?9;3hzJurKUbv7;&ov_19ah;0N}gk(ZFLh%!tFL4N=B+ha~XeV`Gvu6lE(FF5W zBEsNZFR8W9KY8VhvHO4Zvuc0Oc2GNru4Qn{uvAYpy;&$u9-d9mi5!$pTfPx6DuRS4 zV#FhRVnY;cgH1DnKwJCbj)PMno`9xA7<9u(3NDJI9oojw#mqdK~MV2UPCW5}96{6=PO7sxXW@eRu zLP2}}!dE{3EV6gC3$HiaTSdy(hWYxc_J`{HWN&|bxCa2UvaGzDPNui6-n@P5ZsGZ( z(F6b<93EZ2d;jy#UMZ?V5RFYS&rO=6Eo_#1?*PddSX=2ob>(bTx_55ff9CAY|NifO zb8VRYPd|7AkrE;#41ySC6%uO7>4xA(h$fDCZ`@ZyyZ`|2=YC{CI7`Ne0ICXWF`Jb| z@xlww&L)$iqkUC1!eUI|Z?Qy>7Q@*lqM+fq3R*K8YZF5>T!CH>(%c;#xY~{P z4!w82b`Oq@0AT-M|HiGmM^*XJ?p^?w_ALDQYwtd_z0*Iv&DPRvBJ6FFQI;7ZUrf;$ z@#388_p*L31Dj9E8UX(0S6;p|8vWp-TZSwE40`EgI<3k&$t>CgnrWxX5DF0$F=^J; zDTo@}?vxi#5MzboHtTZ13B$UZ4I?oQ`AX-4~UHXcug zqx<*nyz%Z6U;E)zA(lj5u|By<>I-b z+MSK(#nwrhzU3ibgeEmy7YF-?u66)Yy66aihO*SwwXi~#S_CyjUZkk}ASXq#M)wYj zTF4s6Qp1u;c1OsX2yhvg8#bDNcM+N<&~`d;3J3SfqJT1zyuAS$MC?#^>3-7-D=6k2l>|A;I*^CT& zpJa}V36*Tx>s2$cNgBBZ%|~L*@BZ40Ug5nv2LOOX!+w8OmJfSVCQZ94lQx2O$p?tk z&E7_%1LvyY%If~%-gGkYZh2Dr&Z#wu(@z+}{t#*5Ds1(w06>_Ms;fYpvKNt#?zKTM zBl4gN44`JyXg?-3Wh8NFevuSyFP}Q@CQ)L%Mw`6$_6%NP6XjW9U zb4~!&AhD=TsH(ZFyaE)ZOAK0z))ITHoumn-iCGc#iewRy*lVebHChW*&4Zpr1Qt*) zn~tgt^9UoeUn03nqBC2EblET|qqeSbx{av1wn#$6o9x45#sZxI7iP0{)9z+Wp*D@W zj-fgcC5biU8VW#%F`?R_56nE?EJ>I3~Lx>%QK}2QAr7D?gbgnN7GQZaM@%i zU|!e(dys-BWD^gYR~?u%%A%mdG(EGqcDb(C?%(5SRf&>f7)KR%AS?>O=9<&vi?n&i zx+r3dGcP;U%P~UNoIkw4BGyJArYMTSD4y6F@rKES(Zxb-gvZF7P!K`p__w0cst~Da z9H}JW*%<|i^G-yqs0c83y714<4L3eZHEE-DD=t9}{FK0$NL=Ol+e4jreG{9P$zs zjkJC2CyM9*{7fX^6!zcAH4QgcW&Jg?~F-oe@%VC-ivr9 zuy)YvrAN1J0eXClwAv=tGartM$;`Xji>NWN?qUgU!Kf@rnjmOVxl|}I;04nl)-q%i zt-&-SOQfu2tu_He1_3-Pp{4}7lFZ-yr)B;=exDxW87uACIcHqWD^+OO`hmgU_&>IT zbnWAF&TzSzGp|wv6iDYYz?+z1i3Wo#tzA_(WyOT%lJ})l8@1~~j~y*l*W8pEO*=LF zurZbgKrlo>gh;WGj?fsOL^*x%hQa7D)tO$dL{S)kOU((49?EJkC9|1j7qaZ^bkyIU z2#Y8f0D_K%NuawK?Ajs2;vipy6YYUdO7CEqxwW(cPO4@aey}Pjo2ChGh>ReC5E%rT zi~R#iDHZj@c zX!82IA3O(7=2d@XZM~@GM4&X!>+tun;^X|*+JF6fU-`fN&p+6!Y5-tn7H>_0CNe&d z;y{It!C&-z6ZWkDCDb?VYMjj`_wL@zv)=mJb~QcnwG)wu8!zH8i8Y`q$|huAp?@OH znGpM$sw&1NLPPoG-?y%Xh<4=rTv{rQ~qI_Z@@`;D5 zsvO<8T8$6Se(sfOHmS<#6VHA9;&ZRm#n`6ly_HUQ$DAr7EKP`N|PppylZ&sX*r9jK*^0*iXnD3&hZvZwy77qtre^ z8G6i3szuPSs|g8}gyGf}y-^tYErH+zEuP_7Z99x`T+J<_PLV zA(0SQj1Zxcgb>>RUaBaOwAEICMp%Rcy+lM1K*gz=@NFS?4o#Fufs1!m!_6tZ7uC9D zh>LasKm{eD^A|5){ooy&ri1?Qgu45rp&=i82|RE(y$zeIUCSeJ z%z?60&?$_ia;m}#Q$-OSO-4rtN7vrDcI(D%@4Tow&xAP4(wvA8J!YbsTeE+3^u1T# zKqHr*yoiXFBG)Uk^I$;c1fkja&DHY>^1ee;{5b0A57u8JzCMzJdroHMK_!U1_=Pxbmw8Q3@)o3a8g z!RJ2gHxfVvqreLnzqOi1Jh8`q()wReD|XrWj|eu6h%-`!EnTR2L{4_g0KVvv2X}Uf^l$+0YnC5MPD0{aQ>^RN-09B zC_(_BGO?C6Hr97{@8dQR#ZI({XY~MJO|Ls6gDB93{l7ZA|6iWE4A<{Y+=LEGS?`+{ zH`R~wwE-fD@RV6pYscWldtY%Ke0kZsu;QncudDj{$M^T|-ka>+_v$Ng2cwB4sa-v= zXpAAGH3U^iNtvjqXIDSCjwom9Lf8!k{lwa$2_V$(>9i6bJ$nt2u_j59x~#8UIQ#d0 zt^6Op^CKa#Y7&#gG1@?sB#4+G8Rn4q)#^l*J%_uJU;+SSlR$x+j_+6HWbgjH)wQ!C zP*!E-W~_Cn)B%t*VYY=c$|WU&sG_2xh^*pN1V!f2LJLw%PH1^hXBD%ec#aAk4(C*QZLYe%E~r(U}A z%*$WxtqeZ<&A)lLd;k93eRY*bYm%hDy6)?ey)#5?*H^tv%KP|8yo(H#W#zqd%#H;B ztiiw`Ar(W&qMkKL&>DlNRpn5PAyh^2f(WcI=$ST?097Ig^dD;hjZVlXPOVLT{ve`{ z6;ZIU8CfJc;4wewCCV3S9EKj5xAr6<$;nNBU341O`M_Oik`xtH1QWuB0WhdNs9)H~ zc4u4H?>%5;SQu#1IW%!z)?l1g)nVz;u@y-R*Mg9e)bs}F`euIm^zhWyu&kUhRNXkN zY6THN>a>Ii35ZZoC;(sxN*$}kI4cWsG8Bl;Yl~xb%v8E@ zd}*;SxEBzfBv`~n+Sk(b)D9L#$7PPXm{3IVq@W6d5CDN75JfxYV5S@ma>Aj6ph!qj z$>^h=T7hdv8G(dJE*_atRha{*NE8dxwDA;WP>v4pLH|M_oHS}g1$B8pfB&QR2mRr3 zmKWD0n#3$_BqH;p@!@}Y3IM={R9O z@K9AZw@y1(m9v>hJ=grISAhm1gkis@L2jZR*fii(fHVOCh$3->j3H}~sHOVhT+$A$ zh2DvUz@qcdd{!MU3}H~j6OeLzi~~*=1(e5)sUDMKtSaUH-kl%(@Zk1sw7ttuec;w{jPd!{e zbb>n$&SzvT8VoPyfY>3=HStyPgxaNJMI7u^uw!D6b9GI)&C)DDW_A<=({bdc7q+S@ z-i!BSIZU4j3opn-E7(IBlzgE?=wI|LHk+MPt)wEDi?_BL%S-Ht{Iz2o*I5?UVmG=yB zptJ7n9o9z^0I2Ia7-V{8RmhU0^fe(7pa#(`q9NO`%|S(HMREP3TgGI*<~++b)`s1F zK$@kIrcMFSK_bfgdCBF))0@Bd(zAd5-pABT(m^wKFqL3Jpu+6E_w!pCT7_iTJMZf0 zbUL1l&z?Ce!jsX_(gSgAc*X7o{U-oG-e^kQ|+ulz=x?l z@L0>Q!_>kBwxV|V?eq{EtnOIfU+PDEN~x;JN7sJ*@BU@+%?IZ$Upar}1rUD!)gO+I z#)k(-;>BxXY;M7bFPXje*SXGn)fiy5&<$$CNt)J26E6ZP1|c!1s-jqnAYvFXK&WV- ztZGDH7E6GL-b-pJNk~|Pg~%8Hlac2bGo1$`DymK?%Psu>;G+{?bXhqHC$xKGc(;$! zdahJ$o<*dO$Ugy>Jn{?T|csSl3ou!)-I2bM5Li;M+D5yn7hHHI>4 zw|c#^{rr@3E0ZyKCehZ(9a})JwX!_Mz;DqkZA->Q6MEHdw7`c`9&w=)*ulcED@0LP z3}B0}vMvm}>$>zFH^^_tt5GxkL>n_gWft}dqG1AxuqvR2nHd;)RDi>tA@nR>)Qk6l zvaPCs^}H#f0IFVuJ*P>ZtNLJncjwfpZrjj%KoZds5fnTE5n2T>0RQyvb@cUfXEy)Q z+Os|I00sa`=e+amT*-A^)F{4i<=*|>2YW}%A^WN8%122bKrQT5tGaS7dYPbLf>xZ6 zjd)>j#I}eA(RuIKGs^pKzQ4J>xw^hOE6df@RRBid;D?c>DFB12vBrDf@Abf1@oMYh z*I&Bw!Gpuy$xPb*QuE=&vGXG0*{doULPTS&v57IMs5sA)$@JjhFw3$eNe=gSV=P&0 z*Er!ss|2yv+w7$&N|k+rK*qwPOQ*{DeV0))Sq7 z1sC=1^HT~xp;`P3g_34nd4KTkyH|UroF1gF^mi^jxpDf^2d{qX-B*7$t7`9ry|rnY z=RIE+;!AJML7H#R?laV_vu*o+tTiI)oAtv25-kG|P*V%SGM%w8Xov`@^fE{=F(k}I z#l74hkcg5>YK?#ldxlx_)K*SJ1QEmhoX13~mbpvx>8_J#SGxfV$BN7Y1}up-x;d|+ z)lVZjY5hC8c43HW4VjSp(j4KzXb6jdYN38+Xb#S<{q*jkt80tM&G>{k{IKJp(l}^6 zEQ@I@Qvw03aR?@ZHlaMTS(+v$C8A;8J9}z%cdwXCz4IatqV^5rwPud3FAJDiTFtV} zUcQrMo4~`-IB_*K^W>uu*4*wLqk2;pj(B~@n4psIZ85hWN+Ap+TuG-wN2?+n)d_BVemOA-Juxd8=VJ73kp z-m~Y1J1Z(xQFbzn`Ru8WjxyD)jT7sQiu3T=`c&debR%y`-J6I`18; zwnsTwYYCCaDu{?Lis|nCyH!=3J#$Jyg7UqosVe~##IEa*JEnmPIDMfmCue>OXsW&PpB zr(QmF;ib!;`GQSX-ucP*r?a|t-q#A68k?!BNu4C7%(wd0fyZ(gYcmk4@L(^1!PW@L zAgH1^7F8rl$b=hU5DO<3#Sk*Xq~Od(z_@Z+ItiwoL@EHDfkf8RlovtNU%cpe=;I5v zxiS1}7wL?i2#aRtIAfb@JWP1!KNYyBo8B+;?TQ0WKJVd|}fRGJNmeh-(+H z8A9wN@$+e-9f$Uo!qmX~*l8(rBPjw|GR6?1F^0&dNxHeRa(ZXFw_h9_)s3Vx%+L%b z-)t?d<^5CZE2sLsHG>1;-gJ_39gO8k0R>PRcYTG>I9YVSYiRhma8%5%Knp!80MN;O zs;;`^825L<3gvj{==M%K5}k;DV)KVkgJnLUV5d{0ilD3^ENIM7l~e@-3=?cM$p@}i zpj5_LzoO7gAXAo5psx3=cwk4uag9)%I3OKu1jjMU^~6|pD0}zsoWF1>cC!J*IPVky zY!U|87*YiQZ!G|P<>e>V)&^op5QBC7tSs3xd(RS@5S%25%~AkT<=WS6vQ|4573XU2 zYE=as0DwC>00yJR7!$MpacD9iZ-PmPRO1U!PekL{xN_xeHcOKvO{}%Fy15RBYisLS zFDvUZbexhbRZ_BO6wj@1KC!uR^JqMZHev*b%IvE;tP*3;kV%rnkWEbDy#ja^Kbua9 zV!F1u&6riQI?#)SIi@-d#U)oCy1Xwve|ddnpgQl|p{RnWN*Hp4^97>Dp!XFZF#A}K zE{tuRbL^Yjd{TjO3{uDA{H|k5`dCP4EL=|Zj#{4Wq@O1BUNm&lJMUeygI-;hlcU3n zPk;W4mp|KETmRrE-@AMJo@1$NC2!NTFQCNLh_J9Ln_Mf-OSdFVEAPE$mgXqd#=wI> zSk)Q@MbAeyi^`y%7(^BfG@jIA?UfJ`GQwVr7((yGS~Dw|5QTM146Io}cxXHPmoVyA zAJKAIxM6|uk8sj_wD~vLha?yHh~L@DTiRP|D0;SQcV2?V7GV~boo*0+Y0XT^{N}zl zfrv+_qBNhvdjf+v5~AZA|wV) z5<(MQSnq$~nX?kfNyG|VRV-4~wX1zl?z`FreTQdXd&j~oP&)?z&UqF^Wduc=V4ADr zFluZ0qACDuG3CK}99{?jRb7#xJj-3p03|zLBN5?fQmm}6tgNn!s;D?0NNr#eYppSc z5@Wu2<c~RTUBMg=IdTGtdEu2r0Cz4i69Zckdfxt<8Mw)60TsW*mK@2m(mG zo_*oUMPnkpJ=mmzN}y)N?4q4g1Yi?Vk&p}usAy!1D|RI!iVp>&@PrjOKOB~)Pvh}m z^9zt&@sqxc4?)q`)z^Of=K86fa(0yVvrA8Z`Q@+w&6U-SANKyBz1*ww}1g6bRz zP)`Ua_=)$dn>+*|xj{@!?1@A(T~$#K&z0wanN!bq41amus!#8XYKg%dHE<`Jv?=r; zn*k(9jUPyuRx+1cXlQsMpb}C}V4O(hd{H?wwLbH%_LWP7P7V9IuFOhEiJ{!sVVbVz z*)Yp9AlcuSY7%=eYEU3d{cX6mEy~yO8nioI@;DlRi&KF@K*EPpw@z+t;RzWQ&Uw)b z^2TNEbYt!aM1mw}gouqLW5^_GNC+3k-UuQ~)724BK=BI9%E}xLK4li>@YY$_FgGHG zpeGHoO=%EIWo5^n*)LHRL(5$|6C9X1Jb3r^jZ-O=g4_QXzRjcedC zE-Piv&Idn3Uq{i#csx1SJu0iJsw)5}oflC=%<~ig92&c|V~1-fF#hP{6KJ4e_AP*$ zacQtW2n8JgfOo#svX}ORi5u1bY&zLKeF_o9i`?v3f z@wUvRYG}SPJ0UlUJpY zl6Xv@##-@G)y}hc7Em=NfQaBcgCSN(EP0lMkV(B$1u=w-l2{T!At*{#1wt%Ju?EEn z06Nixa5Cj#W|Q3Vrx9;Hw!q}X$D6vfom7ZRI-r)J(HzH}2SZ_NKYebX%l#jO#6p%J zA#yWVO9Y6Nz1B|Dnn)KxFbGd%*x$-tC%spwO@LJmp(V7035Bx*6GMhDu|R+pF|}wc zCKy#HsA`&dwG3D*IzTd*)L=h88a*hhVgj9yCrs zQ6h!#2PK-*w;iBPSe;kf3sWiQt*PUl^s0+P%Z`nQM|{YwK4t?6v@oC%aBdUc>6%0@ zi>59yB*uubViK?xh_F2y6%a%T)y9FZ5T;w!42395az ziW&gmo6lT+=FBO?o=HtOPfPEZxvm^DGy6$1ruNRw-CH;A)^%MW9rlMugA@Q^PdFftPkq!!^;AA3^ux}-_8(qT%Jbn$UDoHG z`0R6E{LS^PbDsa`+I#Oh?}F1j8ACSrqDRD*Y~t*u9w`Wtsj6B<0-Ktf3t%ZY0s=zJ z!8}JnsAo|?wctcZ4U3LvE;U$IlB7tfQGwD40n|>m0APT>ta$M_Os*&wnp1SKnLUmQ zUHjQK$B$pu=o`|hP9E=e9H$*CA|!B(Q?P#aO(QPW<VStxvAm^M(aCIZQ zY5cSnLy!_Bq?XVcw1$Y#5TXrp5eW^%grGq8dPv#^3wm~p5=2`bgHo=P=CHLZ_| zf(5b7!H%6XI$jwC)WA%gj|;&?qp;3MNNm}z+Escs&2IrUU3Jd!o+Cb_2<*simRVo8 zj5sefGZE&if!YTM6$yxxj2f$C#9Fjg$pB#l#G45i4R%LyRsuSh)yk|O!AKZX#nl0+ z7j`N>jDBe*wZ)a^nF^O}4cEa|03cw(lY8$swsx3(RhIj^_h+L?RhO$PtKkf-T`gV! zU_2USz5MvH^Pa6W+k^h!`rLCnt8*27GdLbtxN?>E9v}gL-QB}`cOL+NGxjj=PqPes zT)KycM?c4Qqu$V-zaqsHc5jA9WMpfQQHiHXRDV^#F|$^7g0Cwq?hC7?_!B?Y+-*=i!a@YIs#ZVIoF? z6d{r(DTxv-sYOv?huzT~ZMVClJEHqz{{{WE!ynoaa=0zIWzClCCPmYtIEgd~5&!{^ zKmi4ys!&yL9`AIfJ*}0Q{b8-W&pzkg`-TF*8-+r>`|faty;o-b@;7|>xBjS}Pyh0- z{Ow!c|H{XI^6$O;iNBoB$CKm7hYt@~MFp7yER_y>nJK~zlLT$rM6^l`EBByA zjY$u9-bLOL_NC#X;mTf-R?JiZRL`5f5dfYL(sS-!acSSbTxL3kj0&hoiljt92CP+M z#U^n&>8^tU-?=T1;fxDoAP40M1CWGmi3mg)gEaR2+PXT;OFhoHtjcL#c2B1?$&4Xm z(lpD~H#f`qte8(9JbE-aIvV!-8=G4SiUv=P#x_mX*4JZ7PtZ1tfT}Qj;;5{^-wy=_mf?UwiM(KX~~gKmF<_ ze}>S0^|ycZ=xAKKz#UEy)U#reZS<04uRp&tKbhq0nJ7Bah9_w)1qk1HF+tm_YE9S# z1{!f7<0%;;gv4Uy7;q7T z77T%b!at3Qf&k+m7rw$6Fn`k4(l~^f2KuVTVi9ru76*z}_>Jdelcds6XhD~@=nBhA z^_JhdM9yDieg$*A72-bPQ%93igy2(wDA?J_FZ-Bvs?mx{K8jFC!+=6YjZtHi47B8) zaB0Wv;#T+uKxhaxCNKtk-fINvgH4oxAq{q3s$uw51p_Xh9hZ}N%`6_s#z*BEsxT6n zG|l?G&GpgF?%rWOzIXff?K`)JYkkUSnosGN0HN39$p-@#uIrCpzy5dsi~pS7{8|Y3 z^=KinWifZPcf!I_*R^LB1GC9=eso-ORhx8787vF_=6PNLP?AaIPyNe1*sto^^Sf`o-Rr-eW=X zCa8)<>3az&V(kHI$pqIcN-~qM`k{3{QiAX~3vzAW?KYz-x-m5pjrKp9?DKF=+NZ!= z_@sx>`Y#}0Vsx!XCz;6$MMP@|30Xl{=A}P0M%I%ds!>FiWP!74yI7_LRlU`sEJhP* z1`A#M4;K|8YzLzVJFs(g!CYE3F^(yVDA7a{akXrqo)v(3>W>Xv5$RYQsg$T)kIW}v zTU{CbnG5-IH=O5b$GQN+;-7{hOhKU&1Q8Lc3bykR*=U(+)M8n{0xF0o!2(31Aqa(} z2n0kFQ9=}egmimg?jK2TLuZbUjKsC&M3{+aTDvSw`orP%*Iz$5K7RK*ZxWf|U^t$P zhr?l8PQu5>KKlB9`Ct9NdcEF>p_6ZYRlVyf3F^A6eO*UaF1>gA!QtaWwRn)N3F69I zcJI;sdk^pLZSM>>H|HlO=K|~J^v7jgIltryUy9x={T3kr%**oegQLsW_EnT?21K=* zHdZmMH}J)c;m?2Jr~c`0{_dH-Vm6+L`xT4$6}G<9VL&=k2|h z3?a@W`8E^|c*0J831<6&3(BgqVKV`<|Mnk!$$IyNU%vS0)^~5b`L(V6y;nZ@d23T& z&e0^Qu)h7~`0?FZrscy&dEt-8b8I+Gv9$#xM3(4RCXLB|ph%*sB`ln(C>dnYpq(c} zy}nsrvqgzD`JU>COft&ykqJD$ANgp~^;79AVNd&Ybd(#7Pe*+65&ik#3jXeILj*%G z$Y^Vnil`zcgh@&Y>b%sB=ehHM6HB1zYf79BKwY2+qNB`75e(GpZ7QGT+lo_^J zkI*KkEE2@d%gSBMCD`TzFV1+yWgj$ckz`y1lePBBxKInKt_Hb9!tT=0vIwqhi)?-nug>QHAl|vE6cOJX@ka;Kqr)Sc+9S%UWs)T~OxwwL zytlm*(euenY6quG$SJ-lOSdkIRFixGY&J^LT=aXh@|97aCey={95zlQa!PXK@~w zMZ~$%`&e>Pn_2<@7-sfk*RN6tW=4hJ7R_g5^UvM@$Ky%ioR83)p^zc1qJ$X20BY74 zTL@}G7C0&a5&CQXzob~P7Lm4r;{SgTsaxkPk8 zFf@oJP9E5C0YQjK#7!*(JbPMggzGI4M3*U4Ek|M1sOp@yrSY;;fTa?(;BjH|32cRm zfhOAt2WmGk!PvJEf^=ZCU=oJ0uUXQrV;3#cmiZ5`lwh{_dhLX&c1OoJeT4?8S$L4r z!nZAkwFf{~qi0N`jF0t0IQTtb5lEPDxG<2FULofle zFw_w8K6-<}hd%j<$M+xHzj?cu<=t!Tul>R={NgYE)z;K&c=aXo^S{Jj`_jqX8!F7~ zi=yU?z8M3}w#zPc{?XCl8C+O^ zBcQbMAF=7-*tqo{LVACODHs}k>9N_=tho!aFI;wk8~L5|N?dSKTu5Ei`BrCq(AcS4@$4Xn?x%6&4Be8z_* z`uL|m^W%T}Z(e=v6I**Pt!-a=eE0j`{>qnkcKR>8{!v#Pz5n*NzxT~=JbM4z`Q+GQ zvfnpjr;};k1Sf+rRCx}=NP=Ji$P$apYbG+N;8j^PF>#0oP=iPaB2YO&gWhXm&@-Um z#-Nv`MhI1(+2`?x1O)05?ucik2``Ak&|1DPOr!9;RXjfLV@5Cl3Ch>^6mY*W?Q8%C;Yr&V9o z(c%&bab<8>VfQw3M5k_RP)sNT#x+gc@vC<9J#iF>PTdP17HM)MMS+^X3b`waOOGM6 z^`a1>DI_@kLL4jrK{`Q<0dbVpzBP&f!ALwqBQP$E!oURT1LLUigKo4UB5lo#NMU%6 zAsN1n5f2fK!bnD$l?Y^!RigrpCoX6+sUt03TazRJz+OqY37lPg?CkFWz@t0&x}O}5 z1}j%vZ|~A8U-y3yEh>PJc765=O6?Lsa z!`P4(8e3>3Y^`b#0LUOOeCGrKfFdpv{k0JEybkV6$N8mEq**8~tFGG3ADu>3{=z>D z;!7{Ty7Ssgvy;PvJMXM-UwU-!`~UL4{C^Y&H`Xug-gxIvjt-A*zjxc$o{Zs2uC9?! zMw4ki&kKtRphU>*!?c$rCS+U_kpf!6U@{b(i$nkjS(OEPDXKzM2_p1T0>w0e=p|Tq ztKdGE^Wz6q$#;j4Za6%|WSnKt61#7Aql$%dOje%bK`rbNKotqqlCGs5*OKv!y|1C> zEJ15&NH(*Wm%el&tf>ayNT`L6E1i13)=PJ?bPI7rVuA>-VGdAq1i{ExYKy^&`(e8i z*R##Dp7FWq((Bu9K``bDLa;tk0Te(EhcOaj^uTYU9+ZM8JqQUgSayW(YE88?hDpZ9 zOU;->i~g9jWf1^EkgZ2oR&4M$>hg*M$Pta4M41Ixz_U89Kt@3g2~x*PKMq{du&4ta zC8CEdXG>H$F5W4$ydc4+D?o!m#qAg#lsNUsngRxUMj#|^SMh)fYD{X#1iPAW7!o^C zCV;)Y&0a>gZ{FSA+1cCQ8x2NZ{qmpx_^1E$Cw}Uu5D|Ptfn>D);$Qt+^WpZR-}trs zWDX{)oWJ+*k#}y6$t^ns1I*GKr{cXA74JFKTN5An0W! zspdsyY@W~9b86EF6?;HSiLn4cWPsq!3OrDSY}((s zy#MLX_qNwd?*IUMy{L+mFM~@ynxOIH1%w1j805DkF8jjWzn^JhaU8%yYjef%y z?07+X001@iU{bz#F)qQ70EAJwHJj^zsRBr%+FgmOoF z=-s`#s>8v_UiL3-s;}=p9?xgBL4kzADM|>|pdsvKBw-E&Wey;*7ByQF;RjOfSyUF! z!N*9+Al8yuLx@1YqQ}1Sh3_W=F!X^-2|ly(qx+3pM93PGB_=V{^HMslYAFE{BB$g;7$C2`6PWmPD@!+1 zSR?HrH}nBmL=4*}B+3h^hdxl3;`|gjfaQ7AQoa z00JoxH8xo&7*22$L~g1^K%yvAqxLa}7BRBgx(w=2g=j%lXt~tb%JLW4XsDp96dboa zYwZ*$j1EMm(J=;B^QHDuTP44UD#tv7xJ?Z4jw+#|#586>APtC}stDA4O1!NL^M0@ImqU3?=N_cy-`%x)UKZ9orER=hxgBfo z93I*A(fay208~|#BuV(OYu8@B{F8rK*=uK?|Khc)ufP5wasKe|@WG=8;gf6YYik?p z5Eobi3SnUfPx~CDl0CzZs{U~an+nU ze&Jbw*BQHzMoC3aR_Z>dGT}ViF4R@)x+g#?;aT;`?7i|5j7EF=d%gATtqWJa@x|Z$ z<6r&PX__29o_dkg5F&6edqp!I=gx8Mz#GdMsuxuT5ldtm?ARIvgn7+@AM9AIY15R{ z5M~xDC!W=k8jFD7y^3d1$}Ayb-nwet>X%IL;Dor z*;ny;9`Mx?1!Lkffuy7+fpnCV71dsx*UC8}vWEJZ$$G}uQl!49r1nyaghWSHsp|5v zcXOW&({$GicKRlP8c8u2Fa_fFLPrCDtuYI9c84pjh-WW=&Z{`O3?s%AA`M$j2w^RP zzyKbV5rvEr2QENl&L9FotR*J;WeFh>N#k!7O>O`rBpBktUM^6i+B&Kbt@wi{KgKN> zbOKOfi>M8*sY)n7q~t}=GrFv|p6*>$g!=G-ff^AFc2mC=;VB{L7zt6F#<&2*a40bY zqJ;Pi?dqHa0YmMX)lhP`khB?`UHoIo+buk31J;X+<&^01mCL=X*Sf%4SW>D*Rh}+~D}CURm-nms`b6`5zP7fe3eNjH&qt%-*7c8E`kS4ze9}q)J0m-2VEsEbGv?6 zZX;ZMv%~5`05!etx&CX$^2vK*EVL@Pv5yeV;RH+MSwujz>uJqTF+E^EefukFTE#^ zNn#R1$uJ%Cq^zWAkO{Owv;@|WL9m2m$tGkh+Mw+xGz6w~ zPevjHLr91O8ss%5&SEhKmf|75IP-CFsKpASjlq8lSpguZmzqm4_LUEP`|kMByWjrY z_1#RJ0boRf(MpmcO`8yj3<()91QLqG(ntKM(es>|ItHpS^se~CqZm^SZl{qYy&~#k*84~K z@vPWcABKLN$e>LE?1iAD8Y%e-U@aO#RZ^wJJ)%pdd>st=ymYJOm1n?wY1uOv+g$Nw z2G5!6la7$(S&mfgAk^oI8CU;eXwzbE|I(;eS2_D)a`Nztzw7~ZiT-02#dWna5LX>8eYwsb62?9ifg=^lIjcR(Xy96YA?b+1rT3Kc~aKn-Y+5CA9| z=8#O(9JHi{ECmAvWFkU?WRTP#03Z_?69#}_e~jU0X+pYTbu;m4A{<0E0$vmeP>I3= zh$h$pQWKE?ltuUVC{I^aMg$RNtcohHPx^yv$0a?!`4PI29ZX!Z?rmZaErAKwNF(JJ zMeg+F@dAn?imHej0}A0_MvKwlr$gZy*T}j=XIPwr=VTp(in469aQ1sP&+~uq5C8FB z{~Lehm;dhHX3(lA**jNPbzOSy?_Ik3H~x?R*WvH|_Lsl(<-?PcsPt`VU8-H1wYn?uV1@nn4R+J`>GUKX+&in)GK;ocVY^FJP*ln3K{d#w+Gh@t>! z5(pXPEpafUa#)K*B&;1_ddG~b&4+K+v9TO19J)@oYq-quhovqS;d~+Q4@~KL&X3+m z4Jvs7$4GB|;~VdP>)Tn+a_yN##V3ghfHH@>z_awNVOBz@ohL+#D5ywih(N`20Bqx{ zkfo*;P7DrHV!%S+#=%T%w!bwT46@N6WoGZBuALW2t-avMaNossl7N+02x0zy#L;8{4IalKLRUahgLw=nY4j0LBPDZmC~AD#5g-AWu~g-(@C5(}EI0*TeW)&o=);uLfV^w{G42^ z_u*#9Os=^tz!T1B5tVn~a``179kx%@kpChP$?^pkS-~8h5_xcIe z60UbJsLCKfqGS{RikfZDk_E>ajJQMs)jw{gV)50e{HmNjIxYv6hKVg%RiS6J93P)R z)l1VXv;EZcdIm8=v_QQJjHWjeY*Lrce*vCKjQ(Iqm%^%zrY=+p+I61ld{xX8Y1IbL zWwLUP`_#(%A6NesMhIgxK}``1l9~INgR=6T$&eU>)?hC&S!%r3irI-4li97e-?*Rsvc9$7SD5)_K|FVIAxU=)YNExCyh7Q|o@jO%&QMPNWva1ld@7!fK_%U{DF zI*cqah+fLr<#071SBz3z9Dp=FD(`;v{$x7+sSmwyWp8c%@LghlctUU8%)j&gSQ-E3 zCk+n!WCKeGBg7W0gC%N9P>m+NR`iY`SA#T86bizw%{B|K!QA1vR#iD;64514*H2F8ne556c5?>x+p zC+pjfw=Qn4@9y=68_96k+t}z`ym0c(ug~t@iZMaBgrc5edFWyc(Wk!!&CrJkw7c1V z>H6-*W*?&EH(_uDBV71V#5<2V&L-i0G5|u2e|s=54kc^ZLy6i`xiKwkl#3VwS{u}W zV!dk==&)*QtO$T!C)Y=}%vK<@)_w@BP`=zZQmvovi@? zjCAdLZ-4(^{g1zqPfI*!o0r$+=3rD*wLj&Yf5z*1)+_wrxOCpD+J>hX>b_!dF9uZs zL;_>tqORkH*X7&eN(f?izBud1TS6(9WhU`k^anj#emKXh%H(_RjvqXF@8#WFw~wv! zuUy+1Pi6=@DSYX;Rs={@fmH~}c^>w%{hhtKo`dGr_=%`*mS6#l6afY`h$=dn`?-UZ zFfo)SMnwS0dl40DHRRM(H4qvFM&}i@kXo`d_sOOhn=#M_qA%zVT5Fe8@k@Obbe$qY zw?|Fltj3CKpYd&c-pLZr+P}0HSYlw9fn$)8MM1!p9+NOMDU+duD9KP2Sk&jm*wxi^ zHaj{VZEvk@ZKZ2#y=*jm^|fj|t;a_(n*Mz-fVJFRdr`~?Wcb7 zwR>;8b^G4Qy_&zB>wEWHF>!&YHFfgEoAY1U>a(WOI7|};jsRLjBqby!FrnpWhJ)76 zt64>0v-@aMSG2><6;)lDNCGwxO9&&d2&4v_kM3T^W?}kUy4C^r#oTE%{GypUZv}642L5>=v69S zVK%GRhikolACkW3L(tN0P*HPLdgsMS>>R^vSUZ`q+1=@rD-%KlO;W>(Xu*3yi-;Uh zW@ceaDSB1qs>;2uiSjbt?x+1efce3SgMI)t`cf7Lwq8&xPF@SI?*%y{cuv?KRsCPK zRnU(2k2X6TLy%czWmTxfdQU1Sg2sqJA%m948U#dUsb)o8)x~65Dv z_Hy-Rh4UFwP>Of6;m%~MKTkb`DD()ZMk58$rJ&t27LAQL8g$*{@pK*?;VImCIUn`3 z5j_BcVAu3M4on@=4I-rdB-jDv9G@H>9Y3}fE?>TUbaFDDPFMPR33*vjLjw;UKG@saf7aW?1UvOsiQmKekg;j|4|H~)l*;yo@Hscf^{hX zT>i<=TzT!szVYvW*G-P=+9ymiuNHwpJOcUhf@G5{753|EnNz8~WLYoU**`uygld*q zFg++M7B9k~=BsC!7QBDu!sf%dHO2^n2)k)jIu%uf3LOtg)wWj*mgE4c z;3k|tD5&%<(aL0*O-$t-8X(cy>strK#jU;}YUd1!J13+da6=%LfN5bo##FV5$M7DF z=`LSYVC)irXuAH?L<;6*rb@GiI@8FxvCxNnHcgTg3G2eO-~8zDNnWc7X)3)QAl-QH zeb0UsCknYK8a&d#`8tx;y7dcJo5FCP)Vzdr8EP}qw5~k>7(;;Y)o*>gyZq->ctBu$CKluar;b}JH+&}J|;!6vo;b)a$mR8$r9d01fl@EOcWNi zKnTur%s3$=#uyriYLI|HydoMhhJX!XFcfQTL-7pO&y&ugPO5xsu177cD}e0UqG`B% zScPq_wiP8<7GYC2QL)irpu{sfvb441tG5{Fwc{n#Ern-1?49lRzw@Q{{_N|dKC9IM z)FgwbH-%FDyclnqt%)__oy&74m}XfrSX&nwA7SmMhIBZ9o~O!jUP z3`D7kmVKlkph^e;mQX+l6#-MQhB=_!CS<3AAV4X#D*@E3Wr;DOz&}#bFm?`{Ga>`K zEH2fBHzi_tm%`~LOXcK=mrPgS@_|9QxW?5ETPGZ}s)U#r=@Td_t5LLMkpMx}J4R7! zO=?L%*>io8PrQ&Z*4qBX?ZKn<;_xJxXbYaz1>+OwDQ8#8wor$;M3;SPmb{6UJhq#R zfM{o<*m_tr0*%%nJF+4WmlGR1_KdJ{2QJ(#yCCKwS2uKxf~wL>;KyDZeDC|ix9^U{ z*C8-~tVQPml~vw3u0L5+gVb{Ll1J?d?jsjLJErvkgNlJC!d^LzLb>?d2oQj~5q_Wu z3~b1px*e=BsH)yoDol|p%z@~p-Cb8~Qi-b43&pMWeLDGgPyQ6M>-dHvk=gL2SHbCQ=^dW!~8e5D}w< zaY|I2s{z1hr1t*yZ~xBz<=w(bUW7|L``jl!a_8nPaWy*+fMf{}q$O^xc#BD_g2<8= zH0TYS^QtN)=$nvTbDMhy8V#XZ;u|wpQkD*HAIx_*284vhhr(duVJf(GL2YG+b|X&rWg+f8(@>+5OP<| zCQ~fREKN4Hcie#@m$fgE#AL+cL>P#YVM>Hr)`>x)h!;hVJcHsD01c6frV0Amm9?`8 zvZyd}VB;f^u}MnC%Ct=PHZ~Irz~!<3&Jv^MW*Rz1q*;bhP992C;g8^tfF{82sn-aa?aIB8fs%?SLWsmz#(53iQIZ-%DhQ$gIH~z- zH|Kxn$NFB$3nZrX;Ao@<0I1QmGEOFebStVN>^)frXc38Gmw@B?%$XDbz*uu?x$Q)} zF7FMVysNFsD($7h!k$$zFo4B-RTbf4UesvGXBm z9U4v$&f^rQDl4~SXaM;38*lxSfABAV<$w40R5hQ^zyICu-o1Sb08*322gBjWU1o%; z!fMy9)A=m1Nn%Xx{Zk=~fNLQ@GOUPYUMn3|#y zvcLkw5p=28)RH)2H>Rzjm%~;Ob+%2$?YG`7ANF=eYj+=xh0gAROB8jpEFB7wK|jyu zX|Hckk~AA_Y}WbAmr$0aMd=xGB!l5#bAOXWYws~4K_JlZO#zGvt9vC&2soRSi0Bvs zAvDFHbS6SfZBi>2UJjgF>#bR^3D+vc>7w#}N*rz8Z9w6bc5QwwRl4h;&C5|J1(70w+MPI9QTl#H>?J08!|VUqRI z-r8C==)3t`mraDvD{)W7>$<5AAtt?f<#fS1LT$5uaFxKloW&J6bsefqR9!BwBV(;^ zP1R^>r)Any&#;Pz;vq(V<^q(JKHd3{w5%OY7ghz5dM`2OwfVQ)6n+LC+xy-#@9| zxu1XN>be*30@i@(2;HD@XhsDIIgALx0wTMO@4mm~BK{pq4$uLRtGDCOe!`059F=%}A1e3(R@!dCH{Oa|}@!gxZ ziXsZ>9g0CR6i$|=MMnhY?t8lmdCjMr>syF8ttvP-i+xcw35glK)w?INdk6CmzkDH> zLkIg#LIPs~;SLR&gp8me6ePf?VAW0o)?TL)Lp!@yY&;;N8+Md{5h5vCh(T>}RioIx zwhKhKOB-+OC*pk;{!cJcOX3_N0@#!p3_i&U)ShbbE)Aevv_ryR9B@vkugKkl`P$kr zA+siae3pU$0h}@z9c^yXbf)Brx)SFHBeOOdPDE;!#A{hrwf8JI+TN^crjap4D5hu; zyod)>uZRKZQWnS%=2IqbH%E43XtxKwEF~56NMMX};!QFyDmTxIS!sxp#Q1FBm@+9z zt0zw+`posFJ!67JMeIvkU?c!OUEHKO-nx<_ZIBWBoEwlI&Niq$g>j`LZLLCDq8zAA zlq^93#Z)Gh1JWe1l$yRlS2OdwV+1pHeNJ&lj?7not&5b*<4{TuHYgpIBf* z;CSE>t457wGmf-uNl}OrlSp0Zq+qqXlNKn$1q9td2HL^B5J6y}O$|Q70-zv#VGTcZ zZS%cHvw2wqv$57%!x|0e3)ZjPoxQNzxBb)#Sizvr)J9816ue*s5d=flZ+y#Qfsx>9 zPKxlD`;YS(3xtY7#so04SvzrOE{Wfc!At6tAT1<5t+*}(sDP(0<>gm>RHO#4SQM1-mUP4yHTKR zLxNB{1z>C4n|F?0y0p>j4-`SgBMOj36VqLui2vl2wZ3B)Et)(Q`}fS36Zp^V77Ydc><-py?_!>SY68t>K0=ENzS@ z8b?z-WmQIP%buQhx1UVr{Va8aPw1AnKl_uz3m5Kv^(({au}tfuaam6xuj@fC9S(*_ zR5}soH!kh(ymZB8i89|kIWfUG50nrssS-)EOx^6;y~Ok~T1(6*vt-o~W&L4INuY<< zURl%-_L5{;)zitmGGuzr7*kT30U4<+`e&L{o_3I9+d6mXq)jPbsy&@RGXz-qOZX8o zyPmqwZ$YjY>DJ6rs?be^i;vR4a0ugEC%JblCgtHMO-C8Cu=6Y~=_Q@~C|zQ`pL#fY zns#_L(NPg#Ug*s`^U;_Af@5^JmI+lPMix@^8NrBPH7iJMia=m<#O;Dg6R=I;4sKrv z5RLx8q5?>$K-$>PBZN3NYKIYRgn#Xh5)ep96hTvQy8?aoSN8^&cGhm*KV)GLB7?#q zluuRl;k>?aRDAeiLI$h=DUhOV@=BwT47ALgRwzl@eU(*3@xkOt+d}riLo3?RFG)*< zm-|!@mCnXiLA9=G?|tCr`mjszEZ$4Vi)0p-s;q+j@Zv0Xbz(?5NKrRLi`kP&o~TY( zrgdm5U^D>}XLc)uv`hQ@I~$utRZVA;c~JwvY&J>LEbH|HZJLFHD|EDylv5E!qMi8= z0AN;@mMF2NTTa-o&T}jwK#U1Y?WC&QyAMu|C&lH#P(_8=1`7}kK5B|KH8v$I2$2^H zu#l>N0w9e!K=8?fIGse|hiH^UHC90cWDu;71+qvMjIjwNsZk)&@MEFtdlM3+7d(lG zcY%nC4d=f$W}_gi@eiS-Xr!jHgenq@S|zf+l|c!EnFBVgH9YrlDysS(p*g1!r6lX` zUs%6vA0nPqnU<1a(*x*}&M;s^i4Q@Bl)zmWD2wJ2%ctZ;VMzfe4rw#6BW` zd-Brzsb}j~e|@lHj4`N80%#S;AOSX}Ez6o5X|eD?xm7|O z`{Yi}Gr>|oX|Y@uQ3U}DX&^xYfHE_OgUb*>>qCV-_I2|<837lVS+xTIsLMJr2>{ew z35tM9NQMu!M5K1LqBK<&db?JMZ%0~fCO1s4MnJ<*Gn>iqvM`W*PTw_1* zxS>B-ae0F6jCMl07Y79>;PP*8PM+*vSAV-pr4}whs4H6m%qlv`XpU6N1U^aIR z0+9+5p@}1pfJZ>n@(WDhHDgQrKlHxV^3?(CWW!5)+qdpM@*>u77`!2x$m+d${hj;M z&-Z&KdfK5=Bmo3)ga+flr4r5h8xcT+B@vYcPn?~Vo>f^^FY8Wm0p?Iko_1|4`y7gR z<|?%VW)@NQ5|;4ZdlqrE6BXww@0^O@^0_QtC^cvPiin{RjCge)(f;-leAO_xR=(HE z890M;&Kqkj0RW3bG~&In2>^%_N=Vw-2-%YDjA*AWRO*tdWnNahqxGZtRLA|a?Y{NSnoq~p4Mw2(HP0I$_j-~O03YTWEyIE`mwL?-6g6?NsdGZZg;JhB$ikbgQlM( zJtOSEn8QbhA`(Ch1+WPTfF%Nz9)bmtelkNa0A`7{sZ**qW;5y!l}J@BnPBJSSrKt# z(C??oNnXzC%6ZQzWT~yaRvwd>1H~U`kc&aVb;&B~I4|x=!0t)C*yXr9ISVWEjzc*C0G3)?!K13w+}1WhVc;lXiJnKm z!odO)0ECq~QC%nVS(>D4y}`JcKO-|yRqr28^2(=ZjAhW>(>31y%5Z9>`>NnBdxava`Q=@$%LE zYuAVCYgir}_9m3%?ERB9S9sn$sHZe4!P;BI&F3uO1-v3NIK^rH*53N{2M;Dx<)PbT z&rp>qYLXgI6aWf(fYvI4v~jEz1tLTW#*ZKhDuSfIg3>r8d66)Vu_6GY3a|i>FbRkT z8ap<@TZAF^g5&-MLwp4y%+g+!rXx1nHd)`^oX#d9YDg@SBW%JL^|12axI4XcVKXs_ z%`(cYB|{+#^JuOOh7Il7vn6&A5kRT*IQOphtsNl(Bw_jv0kjNlf|F-&tqogiXtipH z?CWNpaP5gN49=DP!8)pV??i-wSy@DgsIDqzQ5F#q0uMMC4osSY2ZO0sr=VOhIyR{R zX}lmilw1|fTXzA|@mn+izH|t40*AD+iWd=)x@UW#<;G>2^> z;UVn02oT8_1!ITny}?>1WeK$vsGzpTf0(BmMUw`Bh9GK#d6DR1j}96XVD0xyP)TVk z<_(XvIo!NwMMD&Cx+WI#%8MV~+1UNFul_;Z)mLCUL&r-oA-g}i@bX7KiL=Q&58gN^ z-TgoPCS~TGufOs3cWw!ysCv%_<9s|RGJ{w8=EIl!P>#ItXnXtM;lb>1+)IEEgd-^@ zB^9SEh$%gZB8$KPbFD@mq2DtcBrDRKzz71yP!BOp>?q5Q=J~Xq*IH&-CWNj}7hF0& zm_GNZ+)%6+ONW?{*Y*t^F%K?k$`Zlprd_hA%JQUD?f1p}C9mi73E29wAAXKy0;@t@H_z0O4Vk9Tu+Ypjbo%%cz&CG7BJtTGH_xJJ+-{ z`~YTWOri>H>XZZ=A-rLu(>k}MgmtyOg-)CMGyh{Ovei;{rdL^*>kHAa_B+QY}R z`?X}AiqwS{O<2JID@Dl$a~Ly%QW}tyg&Xd7fCmxLCT7*ZV)>(}t4EWQ3maRrvgibn z8IjtU1X#MkTw&EeE~@X}J$z|n7s;5!k|D4bk$?;&$>Ybz-~G-#B!h+kH=|x8MFb3U z8H6i1P9O*b!WyW#()406LqODm5ZAjeUpH$z;RHH4Iy^pj9H6xpzifsIZDlMPeV!X9 zFHu&FS>BCKkq}TKupfGxkkZ8>$|@pjn;RD{zkmRW!1Cx>#u+A_ML~{`GemaT)X~Df9*f4D|hlZ|HEJV z=O>5Lxa1K4MJM{<-SqlizXG`Wk?Svf_?62qzy6#5>|YvC=Rib_p&(uJjPE}xMyXLr z?7(CO&&~5g@Srm(2L(WqM%E4j5_wTHNNBtPQcbMMvW?Ap@OV0zl?5Ogv?ro{qQ>^+ z`A+7DV9TPqiJm<<;EJ}IqN2b)R|Q+_B2Js`>$5-Xvuvx+7l5dOXBf}@q^vwBg3OLF zO~%&Ttn<3p9|6pnr^EH(WH_pe`ASc>^5FE@N?!O020;Z(xVRf-FO!18Zsx!6a`NI! z8^#(HU8Pefgu;TDo$HJg0 zUI<8l%c^AW6~wbcQbV-8wVl|+JI5$M!F)u)fPi}EDcZ@jH7pKWHGoI35Vg}1J7#5o z3c$zHAU;&W$arRzmGe-3II;(aFGSD3z0-{;i(*7 zlCUgLnN>jj{!M2eykOyoMzPQh6w^kon=yPnm2?w9{_o2IU=0Ez4T zbn9Krr(gcJ|2oM?cGorrn=qaq+`ShD;TK-GYPc4!mS8%aj_bsVq=HV){R%vTo=mDk(Jq?iD;`$G8f1V#0<{VNW7bNvoFZzb6Gd)Jo0@45L!xO2`CJGmtFpAa8=DUg4puab)>tB{ z>WYPtg4{(Oj^~rSTqh)xM0Z3)Fb2rF+9@bC>L^f+T=tluAW{Amt+J#=uN1Ti^l81~ z6jhNZ>t)!Owz;z09PqGbf}Lx)5<-o0|T$T@WS?1+DqT~DKY7orHC^{|$OLttw%pdF%@A~oq%?Yq$0fB-0*2D|Xtn6!GvUp&VH$7d}}p$D=E z>qG1xV^J%p07IhOBgu$UFr?N^>S!!&4HV`u;XH4FSvTIyz_ES3-qA|k;_k6Ni{ zmYI*fy8Yh!J8!;yk5vtV5IF-R60BOr)AD=w#ycPCr=l!EErCv#NfQgpS5?Jhmz_8# zvSb-T7!`_9>q#)^4Qf{vRbg$iyb{-LNQStD?MhHAhD_mBgTN}@`?{_{Ra}tAv$07p z?RV`Qu`!GalOGW>a~M#F=oy(WGg@hD2mqWoI<-wXttAW=52&C9YUZTjn{l%lB2*0Z zws|~}{Uq@ds>;0B>Mx;QRhd|`vDSZdG!>P8nzmnWj2ZTZMO`$w9F}$c=p^4v35{){ z9EbpwkX^9zR?oh6b+gD0j>3U$7~CA2=<3#35f_qPX-!>K4H*+ChKM4b>oP$Y46@Xc zLDa^VAkIBaDr89X2`i95uJ5?fEg}Jlnkb++O$E}tvT)Mt16XDcfTF_1B-}=uS6E>c_3?m3i@nGKzOY}; zXGBd;3OJd$gQ+u(DTb;V!cz2$_ls91?h}^>dwYE_XCFxp^u*0KWT=V7$gxIIRAE#~ z3`y8u5@f20O$X`rysl2Bv!W^q87&oF9Sh;JbnVNK|H27mDQu);^ts3(XzZfpgUT5H z0%C3!0(vBZ)M9D~6-AUi)Ly+)@t_9Ok|?`+s_d2?9xiKGPVoq^3!9%RsMvW4!=BCr zUUty&5h@@EpoznH%c1M;X7S0jdP#73R_0k`b1hfzZXQJE@u3=^;PH1);oSnJ7jc6|S#1wjLZr~KMK^@ej(`&$3>*JUlPRtp=IYvke(aYC)9@No@7v3X_|dBV!D+8zzZe zUz-<6;flO;Na7g1=otwar{E$zb41XNA;$vq?!e&MOsvnLJHhAYGXF3>7VfqfWB){@ z7M<0c&OMS<5HX-)7|(bG0JIigd1>pV8@tEjsj3(vOW+t=3E8}^-+1fh-se7K zQ3x|22uAGK_`F8UtGTNg%|XeHp%x$lnTDC!1YR{(e|+AB6Q>bk0ALpzf~qQmjNCb| zD$G7`XMOELMQK~8G);^(m8+j{@c@7^agn|hd%ajw#NBfnXDk2F^t+C!%U%0S@CRhi~`nkH`7udjI17&fVL$`h#Iv zy1`%s2+qxuq}R8pF{W}gA>BBbA8)1m1EbVv*^v~)dk$jY1m(l8zqq+~kwwrTk|kpR z4F%FTM$<@I_&E}gA{anKGSplpqI8h=YE?w=>|Hgpgse)0-0b3MVciE^0&rs^9U!mZ z{u<9Ji1Qv|?C5A=RtG?cS#M2Ct-DB^A#k>(wAP$B@%}!eY2$18jhlDV3`Uf{= zKjYzrmqs=_`-&=owVV!kK>Z8>EE!Y@slrW-qRQ2Yf;GlFmk_NF`#rJ76s9ODFEz)V zm_A{l(40nk+C5G?_ENC2Ne#PV41=$Ij#&YpcYgU{(ZCiJCCme&zzQX6A({a63>gG6 zgxo=AV0koohH9uQXk2GZS=ii#%R8a~D=Ca!1)45M56`qg(uL=J*JpC!4!)xG(bG?& zl^uoeMvB=4sHhmR@2&OpBiHxddHeo+ULrzjKs>QV<5#cv{=K97N7Lc{rm$B^f`VEj z%d9z!8=K0~!Es)NxrEGOSrR_OJ{Fknf+U=VqN@>vDfU*@9fKq@z@o@U;O)6_wV#*f~Br$#-^kk?8h~_s4mysy0cCVecIzy|vzO)E|v! z<69@?qq*4|P~5a4DiW)B=R^bz1*Q~PV=+ZSB#UIon3jqT02n$2B5b1No1JW+e}oaL z<(r{*l`r#@478UaH2Xhn4J07^=3q@6b59yprE1G33?2|coPv$MAm-Pkq|_T>L$W7T z?>sC#Th$dOU;EepU^1`r>8$diF4Ir!fASaqbw`-96c?Dr-Q%d~>G->rXk zcL=Y&cImS}l^5e1Z@>NSy_4Z_v3>DEJ}#!W?|9~M?hAzr!vqYHvDx0Pjz;g_yqBLG z6RI(hg~5(l8Gx9{Ao{2pj>3HkG89AvEd+n}&g^p~f8_dlZ!JA@t6Y#&<+ShJpx3Wr zuSo((2nGQR_=HZz<9^n+mIzf@fTf>Wgg%xvs4Mvy&f2a~x}0}Z#)S~JBEN-e>XV7W zcs{oE7m?|_1lS=01%?BnNy%n5$r8#E%u>oMcn+qD4P*}Z#5~Y*S!7Qbz1lpvXfmdR zO9Z9PU`Q z!7u|cMBp{^JtQM)h&n`S1Q9|osH7^S1_&h}XWkBU~WQUZWx z0el1-)3AeW!lN;?#k3%ywbb9x=Y{vwIj{AcLZ%U@0+FF^!ed8l(cB8nng*1hsVlnB zB?sf7#^sI-1Zf|t9c;F~`pC|CZrt+X^F4WZ*E?1(60H~d$g6`oLYPD>I?tj-}#l%*3Q59Z~p5y{`Aj}3SSG1r^QEK+S%J#PZTfh z?>`#P9Sfz_4MzvlnY(+puGy)2=X%DlkO(-o0!GvT;`UGI`SB@a2Ot z_xhO+Z}kG>>$E(Ubjo=>uaRjkF15CoW@*+VBw?Q#tBAMnKiF6wj)r}MhP`L?iKUdV zY|Z7*JKfsEOzlLDEW^U5Oi{2|QzJAJnEeYIHa_53(vU!i01a77#zL=`3`f~$t3TZ6 z_l7BERy9J*s!Fn?HNmGn_@|e}=0%HRK9T^4fV4ipE8CVvf2@(s_!RDS+Zo`Q)$e?9 zzphftRaa_bgHAV2tpv)O8_6d>y8r0G(b4ftQBy)8V8+lcG3eVj9)9fFg|%LyBmwJR z%xxGN%AotuiN}%^5nCE=T|4nCEd5@R8RJEqu(seAfHnifs)j=$K>6)@LwFJGuA;)a zlAV|&2^sRvBN18Dr@fO;kaXk(z3yzJnv-$MZypPGmpV_@h@AcqUJPn20W;WNGJAuT zJqPG0j9@`4!>rakX^wMMef0R@&i=*guUtPmK017Scyw}HR#jcr!@;nG%9zn$IJz;O z=U%gvC>m0uD0s)v`n#|&J7X=jeypMUUA%T0bhk>1iYN*pvL)u8n2?wu5Q0d^SGBrI zo7t*i%wr&y3MefMhD8)nQK`A4ap{y>5e8{T3ug?8F}Bxd_7QL^CL#SWs#@X(E|{-N za9lWfaPLq4*+0DX%`dNS4f8_Z`T7lCRlO|P*y#VkpMP)W{~Mgo%IW0FPH*2qqB@Xr zvzYcL;K06ib!%hLyK(}oF&S58$`(S_B~d7dTf@F%JfF|7B*G_o?vaOC4-mww ztE*|@iXhIXryPIF_FU&cIvpWJ!^>y_f1gFh`0QKr53sw}bH8qhh81*xl#pdZDfLI` z&V`K&*Dj4Vw~4^l(^{t{NsO1kQ)PpA+}4-32eFIf2DpF$cJz6T*kB`R7LGU3iwws+G)=&IJ}t5*kaU)`L| z3h&vFu}FfglL}4>_l6y^Z->}t>613;Q3i7^#3 zH(GiD3I7V`RAUdjm?uI?g^JeBL9odPeorG2#8 zQTbX$6%y!7_daAKbi(NbRE4|tN~#?nLu{!@xbk~LxE2fB#S%HZVSE-}q zGAp7WGDpu8M1b(h1VbQ1r2s6qw4U(v2!lsvAX$a(Dy-3o2(79K+;ae^in;eS*x(r6 zST%|wHmSV;9DWJ|0xBVdYz=Hk9f&H8RXPE}aw0Hl9FUNbY|u`+etL*8JXB8~G;POs zI3DL;{_6hz&c^znJUUj;w3ohqdGnn+hu{Cs&CgvMzH(`lTC&!lK|~Tn&&3*NIV5ZS zG_&b!T5cs4y!V1m)y23kG+=|QFF@kqcW(}|iaD>HrNyN}9nNP!k-u?LrEi{m;-i}| zIQL|3p$`^*G#PK@S(T<~nwm7-T3@$`ot62lnx&MeetvkKr<0&X51YS|wkZ#fM-MIIW zYr7x1xP~@au6S)>1zNoX!L2BC)iL>y7P&`nXr440FF)YWfZR1XFKCjpFwws%SYi; zm-qPetw}^ly|=j`9q@?ksGn?Ztqs@K`x}?mH(w&tOM5-C)|$R?didzEQmxunem(1P z=rX4P&w8}D2!s!d8!!f~l0j=dAo~DM8Gtk6>CciFyPP zEz8n*CtjF^S<+sb^;2e9$$Dam`{u^7SA|>C6h#D4u6+&B97PZ?W?nUKQk!8IY^lg3 z&d&jmj1^{sCTc*N+IeQ6Z?lvC0F92soG8Q)7(zsCz&a6zES6@lSbAS;t}P`_L;(^q zx_Ych=u?{)M;a~mJxGV})eVng1 z;z>Xt268cDvo#DO5J5|*3Mv$Xc3PcuU<(_>4o@_6r$|VIh`zaiM77B02pl$^ZA8yP zULB(j8h&IGc_iB07z)umzwsJwo1_?eA1VtfU}}!IKD@0=rj?Y5fQleuE1qA_Nz2?h1FCCTIvdZ}k}CBFgU!*#!Q%(TWOnnxphMD7xue>ZqH{*fH%1i&qZs)eQ7Y1O6dh4pC@l&Kz&_X%ssfku2|=_I06=OD z9hRbfafL9XozI7%M;j~EhWid}zE6O~&{`l9%&HNfE@lh5>-nXmZO6Up_aB{H^?Kpr zRtb3R($1TACl4#{MYEMJ4f7&GQO+pCovn>qcZ-K}_sN9rG;$(M`!HBX@jzuYn}K)2 zqCklyw8_OQyZ7q%eZz)tAR@)3HWPA{Z{3;w^dN!F;Cb9niR#0?t5LLkC6# zWQh{BDcY1w3O1QOoLBiAbRfQj81|qD1VkzpD2Ng;GKV53yOEM=j1g*PiokqfMZ%ym zCIsj} zFZQqRr@hotErP7LOc`iY8(z)h$^`p8S5;+Q6VYfiD$1g&s->k!bYzj1tQ>}$uw^j; zIaGz<5gA~vVAHl-*iCeLXXdcp5L||SwR34UrU8prENCS}#f!I;oSsFsWzEI>T0Gs_ zt}IIsk&pm`!^#;8|4<|VAeh^W0OKkXxkDz@AgKw1PWxK1k1O$zI1>(_Rz0)ik1SFY zMGtXzbM0t4Q&nbPRQ1;Ou82H7nauKHl=dx!tU?ekD#|fQFS7SSG#Cb&nrb^23usWG zLm=^(#vKxJUZ48c&r*tYus{`J$pP#G`N3&Y5 zkiSrxJ6lobPf6w|5g@fJXp8yWd!MEWQI@9JU^Gm6J@&4(zidlNcsm1PE&#Vn|DE@z zAN}|y*ykt);R+E|7G=bkt@+Fo13snK@8}{BI}U!>l3wl8hJ`L$7tZtM)iXf<=P!m! zLzLEnCo`V%ObE#$Szl!=Y~>(l+{(maWQ+STqtpNVQp|U7|-)& zX`-DaHt3np?FIs)};0>Qz{kC4{0Vhg4IDUhphj`SKALcR;G8W2(Ky+QFqcTpG2YwnIcf zXf!f_rRms$CBCZYW)FVyO8?*405X-k>xf} z8)*&^MKBPdo%2+IIc)A?ga*-43oareGR@1;pl1kb=aa-50O#rp7q8rVbnCsx)BW{K zQ7KKi-7Sg4$d%?TZZhBl3pecQ30r}*wqkXKAZm&MCFmPsxA(4|Pc3TjJ`Ho-;)!Bs z@NT)mf{Otg+i5Eb~GJ&LtNmH^cSeB>x z3QO=4c8iLhQ?C(_z?h_;ZGPgftbgn$RCNRC0${kuqBgqGbhX+pLIhlZ>mB}vh)QtJ z@FEc799bnKMoHsQ#ntR#=?blH2ht;oc9;qaksOA4q|yPrVi}1&zc%Z&(11!HRk4U7 z$cOjsFVfBnoBpMjw}w|Po!meE=GWi*;`heixnCX?vbm;v`@0ttQ`f%D$PA_GpfUwO zQRJ@3>#`hfZf$OCB7*lmY~88zo{O}>7TgHmfSD7Uh>CM{UAs{Anf;;;b%i6%HrOf& zdtaWAigfl6%%Z1oi8jR{LCY#}XO4!IZHIIx%S)*WCGN(_AR1K(1+pz0L}UyZLrIdJ zu6$t{BP0P~jvIbfc3k$U*SrowvUHmpV;B~^ui(A~0E8qg!V=tW5P=y&GPDSs>6c$S zKg-K3>*bSiT~|qx8e@Dp+gRWK-u>z87uI$+5;ew)XOS3mYN8BERaA&b0~#Jt(@^4r z(<5NRP*=dXT?uW5sQTu*W9O@UK15(O5bR0OL>tqNUUBidcR+zq3y|84;KI2US{7x% z%R~(e*`7oiKcYxo*X;rgpRkKR4SXRyBr)UT>sKzE_}#KB`h9bnl-UR?!Gm5|?@i09 zb=sL}R+c4z*3QrKvdk+{wQ<)bQdi|%nO&o@iI9bMaBAsXMTO|Sqx#kL6nZ~om4-h1 zmnfcHN;<3MIH3@N{*Z{mPvMs}o{UedLo1tV$c%1hz@GAwYwxgbhCxI!fH1b^L2Ahm zWeFNXX<~PVy`8P081O}&&x_FToQJusd_GVQmkK*kbSNScwBzC!9=~VE*d!$~h)84+ zhzKeC3k{ONAVPyiu%$|kI7_i5$cjQtsEx3Oh%$qC5YOIMb>;IT^*)#fw+wxS&X&Dx zZ-c{k*XFAEvKUaOsuI?#5z-I^G^jNXNj~?{i^GA>tjvn+&)+To*_RF<%rrqTNYh#U z?Qh@O{>T*|R8|S8MT!70Ve6{e)!sWt#%!!_CN?R`qBT1aInN&}j zeiw@Xk*W%jL0sYvG}B2auR%WF(x+R$Q;~qkqcLgiyb_H@Ym>=%QL(HkXjnS0pn-i9 z!9Wo-6!W7(en|}Ck(g#8poSKiX^pD8;kg7eZ4dz#3F!=VRYN_uhy>sPQGiGq+zf#g zXmKH9#2O8Qkdi_uXc}X*08p8b8q!Gzu2wB8x4E`XCdIlG1k0}WA^G=%FiR1jO6aAZ z`ohKajeq-J{kI!K0c+0du@Q0Y!sd89Gn2UkRW(PN7;w&WRn&D+frz!nT4N9iRGG^n z7nVqTXaa;5TOM2p8-S2iZ;tCrb3e+gevpm!>Md~Yq^3)Y4eE?FT|Q~YCJ+}XTFv$( zydd@|Osx-E7x7#s)w!i|6){825*+r(&ZrV(P(ol-1RxHc-Ka!l$dzg>u@z4SM_SDN zq|AeaR##rgE~3?BiQ?%YtR+cB{E-+i&kt>JoZ8GvW~?PNWUL`WYEZ}+V~|k|p+OAB zOyPedRRBbVQ5e{ZtLv(m%_f{5WM&#p5;^tw+G4WEB+mA*s?5(vNx!{~X zalqa)JMZ20-hPr=QD4YSi~zDZBx93!Pvzz3l>3{u>t|2s3eqFo2aOw2trZj7k7HY zUZQ|B$c*>GA*V`At00L3uQXkdVw=z_0LEZbYH*Rx7Yxdg1vui1T9tDU2Lw?S-;vd7 zNUK)tsuMv`K_~`Akg|r%2Wj%L7ecKt3XvkHFf((S^pQ{q5WrVO+&L}quID_hvi{(O zS1!MJxmaI+>(22I=u_@o`=y>L1rh5^hLl=Ny4rizYGFl0`)kG*FZk zM%95f3`FR%-Qu&PxXU|Xo*q<#^@arhqxfhRBCQ9JtSkobtknQ?RaT8z$dkY>d=h{L z=lMpR_d}inkZBiQYP@S3{{=z~d5BR;7)LB?$r!YVh-eKNi)s)I3WDfTf{}J!x6Zyl z7F%5=NNH!{j_i0qQ1s5JQe#~e27PKCAT4vL5f^e7YMIszSLJk^7j_#22u`^wAimlB zgv=LNXdR<9_!%umm-@H->;WlsoQe^nWdPLa_@uuzzHY01iuEm3dvvZJLhO))Pz4Rke5BWB_e2Zcs%v2&CR48H#)OU&mVNIMe7a6A!F zm}ZP2A~Iy?yi+FBZHd(CE?>FidIW=Lwrnf|P+O(K)KeOwID&7ro}WD3Zt|?oPp<|s-}Hw3a26pC|)R%fJQLdJjz-A%?dR08})e&y*`n$q)jtsKGc4Mq|>IgQ_`FyV;zINMdIJe^DpX(P*icS7U13{`p4Q?cfH6wAwz0c=Veie`2Uq8QyJybXB4B@WEu9pA zSfw^$Epryts&>GVC1k7#*G-7bQx)(0iv2}6BUG1cXBxu4dVm|Vdav$N4;ux&{s`*H zv!Yq zQIc7cTC^mMzt9*0vTv1$DVdK)oYeywz_kt7E%Db$S_|mm>jLyQiba@7PLZs}m zobmdcUcbg=xOX!@91e^%-ZQYbDV&skn%CPK!=#sBSqA5HiP^cJ!ioTB;v*MQ5>P-1 zVP1hNfgzv`m()-I3Z7X)){96!o1(8FHA=ygR24A9%_^xX5~xUIdP6gJQRB5N0w{ne zF}W026bTkm;iy1T5zurn3P}v8z-7^a7!~yS4_`y`;_&40?XA7-*Y~r{&2)+Lw9Lt4 z7EvrqUpaA1?6mgG-dl@dyB6b`2n=EA*f)OCtrah{J`RY>a=Q>?3`e3`c*v3uvbjF< z?89eSPdr`SLBTiz8>@_`=uQxxGM@ZEtDYy3g_mHd(DJC7;O{^{Q8E=^2P8`gVU{LY zX3`#+)Idecf=X@Zj*;TsMm1`q2kG9V5>?-T0zilo7Jur?T!aNIg~b4B8}{CYw>OmI z#dV>lfWBp~CtcDJ#RgG8{c!X8=I#|F)0z#d@Lb;c_rLeXY%(ULI7m^jFwx+-D#{vh zX^i4?l^_HV0aR&lFMcWIPh8F}Y-B0o7T0v|_Vumad;9D6?;NU9LdYUvhAF+=JDOeE z91^t=t;l7W5@qY_>l^D%DnObSh4*p>+bTM&M(+?7k$}9hr&ASWv4__si@**=suFhb zWeV!QR>eT4=pr3M=aqLHqc)QRX5|1JK-#<|zIpJ$7~dG9AY=>@hP4?qx_uxDr9RFa zpq@olnd5-6@&vv343bbt0&>PG6ygjea+frEL`vARh={7Q*iDlZbLMQ+v@OD-pvD;X zY*OQ$2LN&A=COKK1u{KyEFw&3K$$2DampBqSC}#gg41+p0ti5n6a)w(7AZkX9|m4! zY0y>GYCcgB04v%^OC=!3kVTJ#%0( zAQGV{Sc8TzF(e9AU2CI3jTXF1(!oVqZj)vlfe~MdQSE9C&lJ!+@f3c#vq_u*^xPA_ z^>=Px?M_Ny{DeLG7vfp`tn;hNv)2!N<{}!%Bw_WdEzLO!lQCFjlWFK)sYz2~dkOU{ z6?Lm5QC-CVb@BO#NVR@^jqN%t)CU46AOfV|%3Q^(q!9}}bi1*ZWl?QzM7>gq@En21Ay>Yhr0LTH>^$3TObJ^mRY!QL|FgC^gd1 zDJ_;4ZN@~$LT9mP(d9q3$MRxp5JzNY4hueE0nw{#6b1;irmk5njX2So9X!HUSDnno|%MaFs0CN=w~+zT2JOzb`YT~+hM@R+ax$0dF> zQ3S`@aw6obtFXYqXSyB5CJ+=VG5{uR#5fn~~ zs)i=R_;hxL0?BZRN1`CG@jJzZj?4kY6A=Ox8<_V-Q503{qLeas-iw5{U0d=1s16ZF7Hmp6 zm_gre7%ac@eucQ-qm#szEJ)P1`qF;yv#)NxHj;~j-k;vpy7A6Tlhl~BXGuMPLRHmV zItwW31MGwGJY@r*0M2)26lMZ6HW3L_X=U#a6KuH!-90=;(RoUY%p|1;0RTl}bpj}X zPNHB|;t^?y*=2~jJ&QE%%idL~Nf9vz*Z>fKS$qgs*Nymt423!zZR*duu01KJs8vvq zR$?D461gpk8)8{>(?4HCRMn-=N6>mrz%d` z|L*+R5Brz5fn0UoNyQEx`sC#ouj;J_)3PX;48=}M6_SKfLqzCBi?TG>%KH~Hr*>Kg z?Qk~RQfLTMf9%?D%I>T^sAp{ffkJ%*({bGs~vnpSN#1M$N?xs2esL&Gv9OIN=I z&v{zQk5XZULf51Sp0A>P3hF5&|MbB3<$5T?m@=jOh547+9>O2^>m40i_V2q|*BvE<~5~k|luOcc6Vt_G0 zvg(9Ytbq5vwHRtiV`pv`5YN9T5CEaNdw(4qX6L-Vtnbfou$kQa{+-(oCo`W|8?-VY ziiyR{5D_XsU9+-4V`iqyZ#k|C?gEt#1T;l>A+=XV$>w@u4gILo3iYXApmv!{qClV> z_gU@kPV{N}cI<2newb{vC^T4Nt2HNd)4jJeE$uMXBBDtY9J1p)-CYnVc05cw(P9m5 z*3=hV6u7wbX330yZQVNOk87Nh`uWt?rN(t*yAJ7q6WIL`XYkyXC5Vd4h}+_T5l|6P zdE>1cJt{8mZ`jIT-+y_uzU9iI$cxlhy@V)4M27$`ii|W>1ZGxHYr{J(fJDX;3TT4R zv$UJyMnai^5I5U|3LzqRfx87is@$cK{pHt(Klk(3uYTsW$2af)&R6gL!JEeibNRUs zCzrMc3c#xC!|X(D19uC6B4HcF*BUUc)RHqVVbwFSsx$6>RH5eWOo>EI*~mNMdDLt= zn?(Y)?lw_A*pwTRCi?(MH8@XV-RU@(*+ixQo=Hd6(6M(81P!}=Bwe9kbbi8dnctlr zuVwuOeg#X}23}&eJQ4`UqfJ=4mYOY`)&1E)-q==gKm;%v+j$|fh_#5+tYE2Pu1hBp zx+6gV(zx;h3MTWY;n`8xY@mWDc@hH@>W2YQ3>p*=7I1;*L{L|iFUw3j@GmGz-JW*ZHOJA zcME2?!t~AAv`~c`6Ufn2v+~^=?<6KcgYCx4ASMP421Xi`wc}t~AxrIcl*cqY1yB$a z&%hE+86bn(Lp$8dY-Z3tUsl@>Qcliy?rMw@8d__|@FYHY`)u2q)w+UR1L)`7lbk`3 z(3Xq7(tBy^-4mguC~_mzXuHwOEaACg)O;^4m9vJnbH?80dh()Msk`zL88Rk=skKSZ zWay$#x=Q0F6dX%TTlcVw&@GIH6txR|b*X@n5ZC}>QEkX9KX$RcbMxk(f5TjQ^+T_G za)4xo-DYq8H$SCBzEvy6&}@!sR#u&t`qm-LD~2RgieX8pUoC*x`j* z7B3<~bf9WLebkqj=*rbH@nor-NGvCvmLO>V;FLFhXJ(q#1prju4NTIGVdu;!@Dy8d z=&+)j>O$1uU1R`_MDjbLO%V%W$!ibRI_K-k1%V8y7z*yf+VBH33Q)q50%4(@PzI%7 z5s4@oDAb57;A*de%utrO_=+MiOXC*35d_7vloeWomPMx(#}{n1Rf+)0ss)6(tSYQtJ8q>CkJtkAt&{43muoxO#od&i*6rg;_w}6BXL(M- zRhRWR9XIy1kv7`yvZKP*F1?gb2Dp|zA9dwn;2FsR`t;ePTljF5K~K;|lsEng%a0Tr z9%;k$mxcwV*hvpuZG@ZOhuvj7wr-gqdNiqybD=c(@T(iUSN04UMIV4DRpBrhL{d)6 z*U*L7ci}0o%PzOerLskaTjL&K7D14Ud%Y?jygRN3!+w_aQIXkij-db1OR6TEoCJ|x z6A57o2wFM0_tp8N6oa^BX+41vQ5x+6RUw2@P^^f62vD=!nfqfGcmQy3uK)elj$VDg zxHB$~9aw9uh=|@B7cb7sjX_3a)5e2@g`Z~-J@uy*O^!Acmab(kBC1N70g@@^%d37g z5KnN8JY%I8d_`w)y7UaxIA}@$z#+Qku53+K0-CxE*ufdFeCO8~G}?&a&+N5CrKK92 zcV8&S%)_?qpDonsbh~H4a;G_(pa_mVd#@pDT?MH1EQsbjQH~EvupmJO0~7=l5>f=9 zNRK8|ugok$h*}i|dKZFGG(KBFM2%3DcGG^au}PMeZ?gy?4Y8^Kh~Y;|V^bH7GWL?L zZA+w{D=>?%qTcff6ra@<^rCQg5U>T~XJsor&*i;i_M#$ciCCd!2nH$_5(aJ@i_R(s z00vP2SX5M;7bof&yaFl%8B1yr4I&XHgdiBAi51|)OEq^}>&brDU_J9*{h3)^OJ9Pk zJBj8{*F_GBDxjzuZRrDxyZHG%yRhxDvEzC0g!*wF2^cyE(GuBlkt7ODJ|HfH8Pqk5 zX|3L3jB2=lX~}`+)VgU2sch2?vFmD#A$5WjKp9MbI7AhvzHuvAw2+;wnMFa+&hn&X z#zwCb(8ZrXpxrGtrj23VA=G4$q=WV0hD-ZvS=Ltoc6CBxH#Z@2HDkVTyq=r1+@z`v zYC4 zg5L}PBnDfTUqE0ERauhG5A5D43@ag8i~n}Q78MYj#JMMc;|u!+XZ$jW)&# zf(VO6QDb@)=i%mM6Ac-W(atg<1<>8Ko}hxTcO2MsaYNqrB!UR2%1VS1=l!680X2;Q zTzLBeRZu|DamaR5P*ar!x*CisC^H)}DnX;D+MUWZ3NS*3u(Y9E5v{-m84FbASn`7* z3j#1GD()Gp+)yQ*r?^P<)--Sb&|U(kb!M3oL8FaSwp z;wwVsg6#~(%>cLYu>sD?CaQAsCPoE!nGn8N)50 za&0ZSf>m|Z7JXsLTw&k>{-CH_r%F=@Q}O>&u4)g0xo^0vWh z;Kbom018ovN=ja+gdAKf4>oEwCQBkvp~EcWn`*@+r{B7$f5Ig{IL{>A>2#V)z!-x- zn1=ACjCnn|vHwek-W*w-IlyQJu`K#u#*qK32iSN$#am-HT zVvDh|iJV67Bks*(3}dp<<&XQ*S9e=gyy&bqP=c8Wik>+}aD?TDg9@bud4d*O5hTt! zmQ*NMmX!_{E8d6mY18-8#NB?s+wb3h_;CFDI1HnJOV+0fU6+EW;M&|Iwwz~^-Y}aVYJ!lr1&sa!=H&9xF9G$ zL7!AzIV#P0GeyCL5b)zS-K(38fM&xmj82-nRw6@jb%P2VqT&>tQgVuC+hlnlaQ*er-@Ld}u%$SV+)D^#<@Gi(#DJiB8ix08-jW=W zJAKoHU8hmesLbQNj2Jpyn3&g?KxjfzBKUF}e|pv5-9_kUnw_j74?nHLyx>ovsE=}e z7pIgvD=$=(s|y+mKKz7*K9@!=Yho7u3+!4sWi4hYW;~qb;Y@{uAPR(_MPCv$hHS)_ zDP2MlqYugDc>;^uT0Cr$heA)y|$+iYWuBrTnVqdRgd95BU0~?JONkj2MRW z&iL2i@!S3OW*g&4ni!nz;KTHX#@I2@I9#)qecJD_GI(4Ex0elg7>1ys*|d4TR_N|_ zC#T0WCbtoDeVXkL)+=oRf7O;zb&?ZmVL=Ef9?<;zyUPV_cLjsdbz#I(Q%|4xg43A# zCN7pHE?Z}t@*)Yq%(B%^3D;W9m7WE`R8>RB)2VjTe*ZFTH&Htf9M6g74zuDS1G*FL z;wbXiY#vF7iB6Mq<{^D~V36zqGbT%xa{lq@cnsoX$h|hSj57(!3Jj96fYfU>1X-e!TYxTzH_&|y zyGRJ_b91|@TMD0`%gn0U>{~Dk#J%i)R1x_Hiozxy=ap!N00000NkvXXu0mjf2fDBc literal 0 HcmV?d00001 diff --git a/templates/corner_br_type2.png b/templates/corner_br_type2.png new file mode 100644 index 0000000000000000000000000000000000000000..a526410b7954eca27ecf3b8da9f3c13b6bc976e6 GIT binary patch literal 2251 zcmV;+2sHPJP)`T z8XFViA0#62K_V1U)Phvo((+Pkfp)R4t=)ECGy9nL%zd1D&gZ$aGuwhzG4bqVGjs1b z-|su$oBVJO!j%^?KngV*T^%@u(P47cARc)GN@d`6$Zmvz8%cpz4;05=b?4On9?zr* zUxnDt^r{shnI8U749|$Z)E&Gy6ftg0)9u&jxte_OWO%lOvHuqX2`puBA?x>B_ zhY1E}@^a5xffmB5A#HGk)v5a-7;Oq^ewLRmp&ZBAY{(Mu$#vxF80~YYZM1frnih|j zD^84L9hSAk@!Gi;-Pwpy4ynY4{xgHNUzh?GI^@x7gY^!1_3ZbzRKX__zX8r?J`BS((bw_VHNi%Qd~1u@y4+<9j`%{OcF^~B#OyhD z`jN`H|BcbmAuwi6YFe_ZpQOmqw-~+nHLKyDdRmg20&^1HJk`@yYBeVri3S<~Kvh0Y8%`2U}s7rw`su`TfNweDW z0R_?aSaC&7SN8c@A>`*nt{BqCM>^^Hr2fjh9Y}I_szUbc4s+Y8xn8esh{bdu+QF;U zbkQ^%A*dOFl+lZjq985rRU{)s%Fq}H>9s&FmcpY2**N9g$bp~q}GiPAr$Jr(mBG*cAADwJ4rz= zXUv_pkdu_!N6PmgKinhH~Yztrn851G5gwU2BdrdPk#{ZSE z5>YM2_r_f3vr&e4LSC$g(>|Vcbp%pR!Ll?0XwZN}2T?;~i@_#1lrdr~duv$Y4c##F z4|(9A%+CSJrll$9dUSZ3^2cLiZs$(6wwt9b^1>;;Hz&u23!AzshN!6^5=agNDaUZs zv3FhFHtf%*Sdzh`dDvTPDLag~ISaEB^5Fh3zkrINY0x`_nkKy}*(4xh^xVTnGHftO z8m_zVRD&bAyRVdG71Fd9rL4n9=L9_+vX91{E~8afcIDV>4N*kslb0Bk3U0bxsRouM z;|`yEM;n?Ykuv$z!Z%l0-hgWe3d61s<1M)_@mO z-d)sU6w6jjkSK@)q-VVN>X^7KIVi3&!6{g;T79*2jDnK<^xAhVwkT%g|(DXBK$<8;@RYCG0p-aZnS$gz{ z*u0f}?p{_G@W-M!ln=@#t}q6&WYg79sLFBm{QPB%wS!hDuvV`XYsHj-eU4mVM011a zm<#9}r%7VW%;+@C@S|V3 zd=HR!_?oDcYocn>kq@8-*GU$m9@2wVwA2g6lu!)zyimG> zyvo@3cFh;)=%ng}tV3fq)d(?d=D8xe32p0CgH|n6wp;|wrp}1rcEbl4G7c(zNz>Gu zZ?BSje_HiSS*@r(DT7+QGCfb!V5`%b1zNXmbd0DnS|oB=t`x1Zs#aXtL?f{@3o@WE z)+F!!MOpuy8fv1qRzK}rt?Tj1E!)qLFFfUXUQ0LCAAHwyOEX?2Uq;DQrOK{VVspTW zohHd%Zb7?!066U?kpwLR&RKu>sRiRtR1GylcTU`Ji|p#QDD!)Jb@JfP>VfaK&*{MI z#{6lwaL&g_>E#eAh2kVybYMf+ll`xZk?my1myQn%bQwkzqNZt)y?cL)c==eZ6j2ul zR|dN%6*zCWUe#x3^4fVXHJ%wX{Qc>xHE_8`x?EV0lq})?EDN=D*y!)#~g_6LP8J Z{u7FABZIPt3|s&J002ovPDHLkV1l7aN`L?W literal 0 HcmV?d00001 diff --git a/templates/corner_br_type3.png b/templates/corner_br_type3.png new file mode 100644 index 0000000000000000000000000000000000000000..a1ed1843bdcd9ad9e27ddb5d87086c871e3e06c5 GIT binary patch literal 2631 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 AFaQ7m literal 1486 zcmbVMU1%Id9N!clG`0`HSc*!!P7y5ae%{5)4Y$@b7kdY88j{1rywtgwxw|E|JL~RT z?k><$O7SDKDTub9?UM*vN~ljh7_CZ-4@IFZK1iVtEwlxz5-AicrJcQxMI>77vb*#3 zo8SNY|IXOx?hU<9^-7YoA)m{Ri?KiY9$hE?>wcM^7Qo9)BYGdZZ6dY?7o$ zesc;_e9C?WV&X3C&H~vX7ML8cb=x1N6(=_r z(22Rx0-l?}X##e>C}$c_5V({hx#3p55H>8(;)P-!ZEHYoLHLXXh9ZITlszU7vw+Ge z)ljgm>vAfsQi9B4qO?Ocb<@xc@g^0+gb5ucO}X;`Aub>#IG%mEBTTF;P~qH%npUsZ z)p|l@L0L1>>9nSsnrSKmp@dD3qlV&zTe}Qd8sfn5xx+j;Vnjt&;}#IAwrz0znAQtB zei9zj8pziSRgY|H0SS(Ael4iBgcGdMDs`#HLxDA7tZj%nFHHyNdZ0##vZl|6oUa99!eV;eaB}f&ydJZluP#EV7wU4f**= z$0MvBZi}RJF3>FE)B+-E1Bxy}_Cmo(LURC`>FxR;)b%#h7KuO*NB<4Xh*N4l5K1sC zvA{*bOOA`mRP()ZOPX!NyccrhVVcicK&YxZ4uJ_|>V%XAloU=;rAU%#rASMLQX)7p zXb$SwOs2Z`vkcdw2zKw2|9O8;;D{1J)raSaBLs(8l?65>-o=mzkO-_ z%IY(lr*G~!+242T?%v)0YTwe_@oP(Wu8a-c=;t>s+&lL5(ek$9| diff --git a/templates/corner_tl_type2.png b/templates/corner_tl_type2.png new file mode 100644 index 0000000000000000000000000000000000000000..1e925a726f7a05ef60047b428de8fcd0088b65b6 GIT binary patch literal 1456 zcmV;h1yA~kP)k03k}Lnvl@cO`A4#9D8l=darX~)@f*Zr4OAQ%^t00e{**J=bTxza&-~5HzBVk zha0AX0BSKEMF_)_o53r?*OCOa8U+L+7!&ynJ-`)0XrnG7%n*@9`^)RQ3~W3ZYnHnf zZX)Rh)jQovoa&F@260&26J)b_|V6y_IF zH-DEa1gv&~mCnk$uMcf5qlqNX{Y`r@c3h^#(rbZ`&`q2Un`u$unqYS)a~}axgu()w zwu?L==*IHjL`i+7E(e8O<<;ly*26oPXo@HF7SGE)h$vjY)%X4MaEb1|s22seO5&x39=Qej`6(lfma2 ze83ffBLd5%9EIK~kb$%nOWU~|Fhe**S`X8}em1-#5EVZwi1+VJWtEvgDLmsioJ z)7(};#5rJ!;5Y>}h75uJ{QczKJ&vMoakm^!K=Hv8fcjttU^bZ368RuXzx(Yv2(Ip( zqF$GE+|MRrTM;zSFSq*HTZ%ej=ZAF#&3yCK?%izHq`jTX0R>=mp%8#B1N9CoNvK>S z=trl|%`1DSlpW*At(&a9vB^DuEk3`}z0~eA!`NAIj*S-3g`duJI-RkxVBh{>jIpV+ zfscj@g)oDn9^~$@Vme5TupgcI@gG#FV%wJf($N2~6fcJ8?9a;tDL6}0HOc8ha5R7E zk8Zd3?e}Nj`{eC%wN#^ZDOVG05A*~`n>#ITGo*l~183AnYhPn;X%Huh-|IIT&6|`` zLnWo5n&p(zgvL=ChW#ureBVEP?%bOn94(cLk}I`ZgQ6756UGJ~@`af}Wj-+`Qq;MK zH_i<9N+e0r@AcX;Q7h%zR*GYUw(3qC$8+-+UwZ2W%gLAMY9DkL@;#AvMIjLi6(l8w zkX21)07q#9x8{KM07q)nix{VpQphxUXhT}9Rriy=4A)T}${=|>S8y@Aa0L$TlUy9SO)tK5wbfcnj8GN? z*jV*)O_VbgvqTJC-(8zbd}@|dT^*C$K@oopSKemluPh|~^s(0h&%JPYeRg@{z({d= z*N%)^_l$=04&7e#RHTlVYIema)byv~{&2(l{OYZ>RuhPc6h#}KoIbewz;hM64(e&Z z*W;|nnk0&2VzvvpA=9w7SCuDmzqr|MblRjmjtEgI<^5Ib0d95T{$lr(+>_c4R{+H{ zJLA&iX}NrGB$3B)SN$)|3CTs4As9K zZ8x^KYvBoNXGr_>gNA6^cZfCP{!D^=XRqM z3;wLQofB5IC3QnA%b+?j!}o=q*5LH^WeS~lL-`2Eri>}SChgO0Yw0ry`4C`!G%32JHp0000< KMNUMnLSTX=`@P}- literal 0 HcmV?d00001 diff --git a/templates/corner_tl_type3.png b/templates/corner_tl_type3.png new file mode 100644 index 0000000000000000000000000000000000000000..a0ef7d969ebaa8bf67dcd05da4db4dbb926fca79 GIT binary patch literal 2172 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 B73BZ` literal 0 HcmV?d00001 diff --git a/templates/keyword_wolf_upper_type3.png b/templates/keyword_wolf_upper_type3.png new file mode 100644 index 0000000000000000000000000000000000000000..cd96185715801a583aee2cc6fef2de38f535614b GIT binary patch literal 2096 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 Q>i_@%07*qoM6N<$g4!$y{ zl&DT@B}gSuHQ@n?0_sx*rG2UECW>MRj!=@;@!jj#yS2Ucu6Mn=Gdq`aIAe#T1SA6L z1GCc3WoLf#UH<>?EL@nG1fTQ<|4*UBvdaU5YJcA+0EOu^^Y)+6jSuJ{2}xf6F`{Yz z6O;g$$)KS@*2fsQzz0477{JZ-g7U`i{_9BL%?ak+VPcUQEd6*x7QMb+sA@tr^3SEI=9 zf6H9G#!#{#7$;97QwPuo1Oe;=>~8$<8IaA=V++Wj?|EdJ{}~Ej>}P7Wx4c|8G8~+H zJ^+aBYuB#izWseE)y_C2KqF^E#gY^L)!BO=|5qrVS@wVNDpEx0xubkr8@*Ea*tToP7MRVJpM#wV1RE-phOZ_RY2&qt$^v? zueqF9Tgcvg=_Le_5jPCoP{N8LN%`FJy->82L1%xyee7Y65W*3EJ9*!uF#uX+NKb06 zRo5_Mnz=o@V_HlzG{Z1dMRh8re-kO+*AM*4mIUz~5!`(xZQ#}qm}T6_#gP}E4?|v6 zRWo76jkv1F#r531P_&pqubl^;4Lsn}UNd*F?j2|#cgH0=TW0H9W_H#aYD72mm=SBJ zZy3HX+|%35Vf;@}{^gfJwHo9}{X)9|X93b|_v|ii6k=iRE|fVv$QJTJPy7H#ZH>0`C*D@O3E(*(tB$on^nT&G8FLy>KxfdSwrm`DFDLtP&IdmJ!_xA(h1PN}-kWi`*qB_av;mo{^T^jCU zqT$+?4lxmxMXavrnxdIW(~O(dYPFEhyVdHZ9&J8wFw*g<0Lo>v@Z2wh7Wg9WU0qA_vnXGvJM&8V z!2STrzwkm3V0{#%SHR?KEu!F!@>pa2J6$~pu>uQ{(I_W`!m7gZAypJvj*UbkX3V7N z3rHBhISwIoS&;b3*SPkM{E1_5@yccP#DG0B#|b>DT9=y|-)w5}k=GE5>tR*XBZ@4C zcu`X|Q50#FsTT!akR_RFUI+~cHWws;aAtgXU?oN?r0wx2y8mHXK&`wgT^<%6XHUF4TF(EFys)&h%`@L zU>Q~tr2KlmjLX?fmUx5;kIot?Byj>)w(N?JcYfpRTys-#a3~fFGz)J$wAXKbKpPke zl+I8l*WeA%iCJMgaklMVH^hxGVE4~ykZfwtp&PgTG0MK=Pb#&f(7V*HK$&DS-# z^#0P54Mv=!QL=~@W&1HqmnTAICT>U!Y(0MJqNaTA>?`meg92W@@SgGPk5t?8V!Fq1 z{zr?h<$5b?`2(r7rd1N_oe{ z5s63&RwG`i!j(30p}@7bjeqxWM|anKt^1T~S+->bU$j_sv7*|XotW4=_BS{?E0?Rm l$d$NhfK%sQ`*@gi`#aR(L@q#!){X!G002ovPDHLkV1mgkc=!MS literal 0 HcmV?d00001 diff --git a/templates/positions/science.png b/templates/positions/science.png new file mode 100644 index 0000000000000000000000000000000000000000..b81660083ae01b468f83909b51491a847b24b266 GIT binary patch literal 1573 zcmV+=2HN?FP)9ml^jySKOZ*t@eGpOcI2_)$_PR^D#fK+`mlib_-Dl7}MouSyX}ls`cIgNi07 zAY=ieAi*CfwRx1Fw1~=W6KFuqTTPR;2~|y+cTy9G6Wg`#&Ud%Znb~31t%Q)UYDdgJ zyE{9-&-|YA8*=md75rZZnTgm~J8^ud@`di47`a8AlC&(Iu~O-LFmheEj0qMR24Asr zw!{nX9NM@v*8lqTuWnxwFkcABPzdhpFi0Y%bB~p4$wq73`todvAD;Q^rY`N}!&jf$ zw%7|iFNh|kgia9Cdr9C%=9xc85j)^ydSm~n5Dz=ArAPswEb(ua0i zJ0Vi07PXY?H_w`Q`SR$&7Z)$izVPSCfge5OI2tq)!KBC~45t(nZJ(#rN4jU$Iy#P1 zDwj*A-X0gUyS38f+jiCY$6G`Bwru_C?#spl9!S0^-_p`Nhghp|`rtrsZ^w6cAKCrv zdQCI9A4o|gXQ_1J2481c>A7W(8prnkV#k&x-+TV(5BEGaK2ZkaX3`=e%*5$%Y@@F0 zb0*$==fHidl0W#_(Y{@qZ;wt98Ybcf2}Cw&3>%v?O`9|E#+d`_dXm51cWGfRzj~cT zhz7^BQpH@>x=Z4FhhKU8;oMt$TbJe*&YM_WssJ8x-|jPYb2$0f-4NeD`Kpz;({<#z zjMro}Ah01B`uSb0-G}e>?D#6?G}#`g#AhWOgA2G@Mt^SR)WxB)G#k_TZ0JYf5c zC%5nEn@MU$V!2$-zffR7Q-Z`O2QmmlgbqX%At`!K9EyE@?Hq{ZQn|H%xd)`$O?C&w zg`}VvctxNJw~UG?7oO>NyPlX8u~MG8=cVq5vB*^N?4J;7HV(`K3qWtdu7r-mq?B~$ z&l5dg`3#~{DYd=40!hPsoCc>6j=>*=RGBA%7LaDRCjl3ZkIICvH~p*r=_9;-(~LwC z^2ULs9*5P7@EU}&@fNrj5g9-WNIi<;FCl!envjCmm{;ZQqFrzBw)OvmNO;xiUj;@} z8=w_XIRaV&E5oGl2xttkZlt%t9)NcP%#S?a`+@J<749#3?oYgZ!+#T*ZR}kclO%C3 z%r&Tp8mKHo`+ zP6OJo72bIj9mpzVfLI6-0vbpRiASM-3E{hNei!Y*126@(MZjD;oYZoqx^Iojd`Cna zK6tqG(vF~#j3a3#VVY_^bVKP4T|hNLl7?==d>B+&CgALfTuBWOrs6KEZa8IELu#Y0 z)YwNOV&>|;vBB+Pq(hVORE}oXlHj7IQ%H`OGDB6g5?1-BUVP;r2&J-!P7PH;jKFne z#r4Z}F}dN5^n(Vu@WJ1z$A8nm|Jc_yKHJ)UAzkRS-`#<60}cf@bLKRWDEl=P@<4GZ zboMFQH-uZ!SpA9X{wb5@wEUaPxAsOv{k`A+<@DIvwaZrF1{zby<^zFHf@w89ZWG5) z$Ct`1Xwz+5*$+)ih``sRH2K+cZcQ62+1D0qo3BqS%uSGM7tfEq_??R*MH%qs&dz(< zn=^G;m1d!>f@r#)7iQ2~+0fBqP$znQA*MFV0b{;DJW@S#_GV|# z%E*r&eKZ`NKYsP^Z>x*Vu~XxNSFrQJj`cka_utn$ZL?SMppLGXfk@fux@W#4n1AT( z2YdDoZdlT?<%`QUuU?6`G1hT^L=3}lan<|LEt#9@2jhma9oE( zS%i38rMPly7?rlFMM^p6N(r_8lfSCrBobyOlli3ckk7n7DoG_i{frQVP&F}AvsUBZ X`hYMbq*BmU00000NkvXXu0mjf3l$V8 literal 0 HcmV?d00001 diff --git a/templates/positions/security.png b/templates/positions/security.png new file mode 100644 index 0000000000000000000000000000000000000000..c7db8982911d4d0f45ccbe28ed097d2394ab2d12 GIT binary patch literal 2003 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 literal 0 HcmV?d00001 diff --git a/ui_interaction.py b/ui_interaction.py index 045cfdf..bbd9db7 100644 --- a/ui_interaction.py +++ b/ui_interaction.py @@ -19,19 +19,41 @@ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) TEMPLATE_DIR = os.path.join(SCRIPT_DIR, "templates") os.makedirs(TEMPLATE_DIR, exist_ok=True) +# --- Debugging --- +DEBUG_SCREENSHOT_DIR = os.path.join(SCRIPT_DIR, "debug_screenshots") +MAX_DEBUG_SCREENSHOTS = 5 +os.makedirs(DEBUG_SCREENSHOT_DIR, exist_ok=True) +# --- End Debugging --- + # --- Template Paths (Consider moving to config.py or loading dynamically) --- # Bubble Corners CORNER_TL_IMG = os.path.join(TEMPLATE_DIR, "corner_tl.png") -CORNER_TR_IMG = os.path.join(TEMPLATE_DIR, "corner_tr.png") -CORNER_BL_IMG = os.path.join(TEMPLATE_DIR, "corner_bl.png") +# CORNER_TR_IMG = os.path.join(TEMPLATE_DIR, "corner_tr.png") # Unused +# CORNER_BL_IMG = os.path.join(TEMPLATE_DIR, "corner_bl.png") # Unused CORNER_BR_IMG = os.path.join(TEMPLATE_DIR, "corner_br.png") +# --- Additional Regular Bubble Types (Skins) --- +CORNER_TL_TYPE2_IMG = os.path.join(TEMPLATE_DIR, "corner_tl_type2.png") # Added +CORNER_BR_TYPE2_IMG = os.path.join(TEMPLATE_DIR, "corner_br_type2.png") # Added +CORNER_TL_TYPE3_IMG = os.path.join(TEMPLATE_DIR, "corner_tl_type3.png") # Added +CORNER_BR_TYPE3_IMG = os.path.join(TEMPLATE_DIR, "corner_br_type3.png") # Added +# --- End Additional Regular Types --- BOT_CORNER_TL_IMG = os.path.join(TEMPLATE_DIR, "bot_corner_tl.png") -BOT_CORNER_TR_IMG = os.path.join(TEMPLATE_DIR, "bot_corner_tr.png") -BOT_CORNER_BL_IMG = os.path.join(TEMPLATE_DIR, "bot_corner_bl.png") +# BOT_CORNER_TR_IMG = os.path.join(TEMPLATE_DIR, "bot_corner_tr.png") # Unused +# BOT_CORNER_BL_IMG = os.path.join(TEMPLATE_DIR, "bot_corner_bl.png") # Unused BOT_CORNER_BR_IMG = os.path.join(TEMPLATE_DIR, "bot_corner_br.png") +# --- Additional Bot Bubble Types (Skins) --- +# Type 2 +BOT_CORNER_TL_TYPE2_IMG = os.path.join(TEMPLATE_DIR, "bot_corner_tl_type2.png") +BOT_CORNER_BR_TYPE2_IMG = os.path.join(TEMPLATE_DIR, "bot_corner_br_type2.png") +# Type 3 +BOT_CORNER_TL_TYPE3_IMG = os.path.join(TEMPLATE_DIR, "bot_corner_tl_type3.png") +BOT_CORNER_BR_TYPE3_IMG = os.path.join(TEMPLATE_DIR, "bot_corner_br_type3.png") +# --- End Additional Types --- # Keywords KEYWORD_wolf_LOWER_IMG = os.path.join(TEMPLATE_DIR, "keyword_wolf_lower.png") KEYWORD_Wolf_UPPER_IMG = os.path.join(TEMPLATE_DIR, "keyword_wolf_upper.png") +KEYWORD_wolf_LOWER_TYPE3_IMG = os.path.join(TEMPLATE_DIR, "keyword_wolf_lower_type3.png") # Added for type3 bubbles +KEYWORD_Wolf_UPPER_TYPE3_IMG = os.path.join(TEMPLATE_DIR, "keyword_wolf_upper_type3.png") # Added for type3 bubbles # UI Elements COPY_MENU_ITEM_IMG = os.path.join(TEMPLATE_DIR, "copy_menu_item.png") PROFILE_OPTION_IMG = os.path.join(TEMPLATE_DIR, "profile_option.png") @@ -42,10 +64,38 @@ CHAT_INPUT_IMG = os.path.join(TEMPLATE_DIR, "chat_input.png") PROFILE_NAME_PAGE_IMG = os.path.join(TEMPLATE_DIR, "Profile_Name_page.png") PROFILE_PAGE_IMG = os.path.join(TEMPLATE_DIR, "Profile_page.png") CHAT_ROOM_IMG = os.path.join(TEMPLATE_DIR, "chat_room.png") +BASE_SCREEN_IMG = os.path.join(TEMPLATE_DIR, "base.png") # Added for navigation +WORLD_MAP_IMG = os.path.join(TEMPLATE_DIR, "World_map.png") # Added for navigation # Add World/Private chat identifiers later WORLD_CHAT_IMG = os.path.join(TEMPLATE_DIR, "World_Label_normal.png") # Example PRIVATE_CHAT_IMG = os.path.join(TEMPLATE_DIR, "Private_Label_normal.png") # Example +# Position Icons (Near Bubble) +POS_DEV_IMG = os.path.join(TEMPLATE_DIR, "positions", "development.png") +POS_INT_IMG = os.path.join(TEMPLATE_DIR, "positions", "interior.png") +POS_SCI_IMG = os.path.join(TEMPLATE_DIR, "positions", "science.png") +POS_SEC_IMG = os.path.join(TEMPLATE_DIR, "positions", "security.png") +POS_STR_IMG = os.path.join(TEMPLATE_DIR, "positions", "strategy.png") + +# Capitol Page Elements +CAPITOL_BUTTON_IMG = os.path.join(TEMPLATE_DIR, "capitol", "capitol_#11.png") +PRESIDENT_TITLE_IMG = os.path.join(TEMPLATE_DIR, "capitol", "president_title.png") +POS_BTN_DEV_IMG = os.path.join(TEMPLATE_DIR, "capitol", "position_development.png") +POS_BTN_INT_IMG = os.path.join(TEMPLATE_DIR, "capitol", "position_interior.png") +POS_BTN_SCI_IMG = os.path.join(TEMPLATE_DIR, "capitol", "position_science.png") +POS_BTN_SEC_IMG = os.path.join(TEMPLATE_DIR, "capitol", "position_security.png") +POS_BTN_STR_IMG = os.path.join(TEMPLATE_DIR, "capitol", "position_strategy.png") +PAGE_DEV_IMG = os.path.join(TEMPLATE_DIR, "capitol", "page_DEVELOPMENT.png") +PAGE_INT_IMG = os.path.join(TEMPLATE_DIR, "capitol", "page_INTERIOR.png") +PAGE_SCI_IMG = os.path.join(TEMPLATE_DIR, "capitol", "page_SCIENCE.png") +PAGE_SEC_IMG = os.path.join(TEMPLATE_DIR, "capitol", "page_SECURITY.png") +PAGE_STR_IMG = os.path.join(TEMPLATE_DIR, "capitol", "page_STRATEGY.png") +DISMISS_BUTTON_IMG = os.path.join(TEMPLATE_DIR, "capitol", "dismiss.png") +CONFIRM_BUTTON_IMG = os.path.join(TEMPLATE_DIR, "capitol", "confirm.png") +CLOSE_BUTTON_IMG = os.path.join(TEMPLATE_DIR, "capitol", "close_button.png") +BACK_ARROW_IMG = os.path.join(TEMPLATE_DIR, "capitol", "black_arrow_down.png") + + # --- Operation Parameters (Consider moving to config.py) --- CHAT_INPUT_REGION = None # Example: (100, 800, 500, 50) CHAT_INPUT_CENTER_X = 400 @@ -53,7 +103,7 @@ CHAT_INPUT_CENTER_Y = 1280 SCREENSHOT_REGION = None CONFIDENCE_THRESHOLD = 0.8 STATE_CONFIDENCE_THRESHOLD = 0.7 -AVATAR_OFFSET_X = -50 +AVATAR_OFFSET_X = -55 # Adjusted as per user request (was -50) BBOX_SIMILARITY_TOLERANCE = 10 RECENT_TEXT_HISTORY_MAXLEN = 5 # This state likely belongs in the coordinator @@ -64,6 +114,7 @@ def are_bboxes_similar(bbox1: Optional[Tuple[int, int, int, int]], """Check if two bounding boxes' top-left corners are close.""" if bbox1 is None or bbox2 is None: return False + # Compare based on bbox top-left (index 0 and 1) return abs(bbox1[0] - bbox2[0]) <= tolerance and abs(bbox1[1] - bbox2[1]) <= tolerance # ============================================================================== @@ -81,7 +132,7 @@ class DetectionModule: print("DetectionModule initialized.") def _find_template(self, template_key: str, confidence: Optional[float] = None, region: Optional[Tuple[int, int, int, int]] = None, grayscale: bool = False) -> List[Tuple[int, int]]: - """Internal helper to find a template by its key.""" + """Internal helper to find a template by its key. Returns list of CENTER coordinates.""" template_path = self.templates.get(template_key) if not template_path: print(f"Error: Template key '{template_key}' not found in provided templates.") @@ -99,9 +150,11 @@ class DetectionModule: current_confidence = confidence if confidence is not None else self.confidence try: + # locateAllOnScreen returns Box objects (left, top, width, height) matches = pyautogui.locateAllOnScreen(template_path, region=current_region, confidence=current_confidence, grayscale=grayscale) if matches: for box in matches: + # Calculate center coordinates from the Box object center_x = box.left + box.width // 2 center_y = box.top + box.height // 2 locations.append((center_x, center_y)) @@ -111,88 +164,172 @@ class DetectionModule: print(f"Error finding template '{template_key}' ({template_path}): {e}") return [] + def _find_template_raw(self, template_key: str, confidence: Optional[float] = None, region: Optional[Tuple[int, int, int, int]] = None, grayscale: bool = False) -> List[Tuple[int, int, int, int]]: + """Internal helper to find a template by its key. Returns list of raw Box tuples (left, top, width, height).""" + template_path = self.templates.get(template_key) + if not template_path: + print(f"Error: Template key '{template_key}' not found in provided templates.") + return [] + if not os.path.exists(template_path): + if template_path not in self._warned_paths: + print(f"Error: Template image doesn't exist: {template_path}") + self._warned_paths.add(template_path) + return [] + + locations = [] + current_region = region if region is not None else self.region + current_confidence = confidence if confidence is not None else self.confidence + try: + # --- Temporary Debug Print --- + print(f"DEBUG: Searching for template '{template_key}' with confidence {current_confidence}...") + # --- End Temporary Debug Print --- + matches = pyautogui.locateAllOnScreen(template_path, region=current_region, confidence=current_confidence, grayscale=grayscale) + match_count = 0 # Initialize count + if matches: + for box in matches: + locations.append((box.left, box.top, box.width, box.height)) + match_count += 1 # Increment count + # --- Temporary Debug Print --- + print(f"DEBUG: Found {match_count} instance(s) of template '{template_key}'.") + # --- End Temporary Debug Print --- + return locations + except Exception as e: + print(f"Error finding template raw '{template_key}' ({template_path}): {e}") + return [] + def find_elements(self, template_keys: List[str], confidence: Optional[float] = None, region: Optional[Tuple[int, int, int, int]] = None) -> Dict[str, List[Tuple[int, int]]]: - """Find multiple templates by their keys.""" + """Find multiple templates by their keys. Returns center coordinates.""" results = {} for key in template_keys: results[key] = self._find_template(key, confidence=confidence, region=region) return results - def find_dialogue_bubbles(self) -> List[Tuple[Tuple[int, int, int, int], bool]]: + def find_dialogue_bubbles(self) -> List[Dict[str, Any]]: """ - Scan screen for regular and bot bubble corners and pair them. - Returns list of (bbox, is_bot_flag). Basic matching logic. + Scan screen for regular and multiple types of bot bubble corners and pair them. + Returns a list of dictionaries, each containing: + {'bbox': (tl_x, tl_y, br_x, br_y), 'is_bot': bool, 'tl_coords': (original_tl_x, original_tl_y)} """ - all_bubbles_with_type = [] + all_bubbles_info = [] + processed_tls = set() # Keep track of TL corners already used in a bubble - # Find corners using the internal helper - tl_corners = self._find_template('corner_tl') - br_corners = self._find_template('corner_br') - bot_tl_corners = self._find_template('bot_corner_tl') - bot_br_corners = self._find_template('bot_corner_br') + # --- Find ALL Regular Bubble Corners (Raw Coordinates) --- + regular_tl_keys = ['corner_tl', 'corner_tl_type2', 'corner_tl_type3'] # Modified + regular_br_keys = ['corner_br', 'corner_br_type2', 'corner_br_type3'] # Modified - # Match regular bubbles - processed_tls = set() - if tl_corners and br_corners: - for i, tl in enumerate(tl_corners): - if i in processed_tls: continue - potential_br = None + all_regular_tl_boxes = [] + for key in regular_tl_keys: + all_regular_tl_boxes.extend(self._find_template_raw(key)) + + all_regular_br_boxes = [] + for key in regular_br_keys: + all_regular_br_boxes.extend(self._find_template_raw(key)) + + # --- Find Bot Bubble Corners (Raw Coordinates - Single Type) --- + bot_tl_boxes = self._find_template_raw('bot_corner_tl') # Modified + bot_br_boxes = self._find_template_raw('bot_corner_br') # Modified + + # --- Match Regular Bubbles (Any Type TL with Any Type BR) --- + if all_regular_tl_boxes and all_regular_br_boxes: + for tl_box in all_regular_tl_boxes: + tl_coords = (tl_box[0], tl_box[1]) # Extract original TL (left, top) + # Skip if this TL is already part of a matched bubble + if tl_coords in processed_tls: continue + + potential_br_box = None min_dist_sq = float('inf') - for j, br in enumerate(br_corners): - if br[0] > tl[0] + 20 and br[1] > tl[1] + 10: - dist_sq = (br[0] - tl[0])**2 + (br[1] - tl[1])**2 + # Find the closest valid BR corner (from any regular type) below and to the right + for br_box in all_regular_br_boxes: + br_coords = (br_box[0], br_box[1]) # BR top-left + if br_coords[0] > tl_coords[0] + 20 and br_coords[1] > tl_coords[1] + 10: # Basic geometric check + dist_sq = (br_coords[0] - tl_coords[0])**2 + (br_coords[1] - tl_coords[1])**2 if dist_sq < min_dist_sq: - potential_br = br + potential_br_box = br_box min_dist_sq = dist_sq - if potential_br: - bubble_bbox = (tl[0], tl[1], potential_br[0], potential_br[1]) - all_bubbles_with_type.append((bubble_bbox, False)) - processed_tls.add(i) - # Match Bot bubbles - processed_bot_tls = set() - if bot_tl_corners and bot_br_corners: - for i, tl in enumerate(bot_tl_corners): - if i in processed_bot_tls: continue - potential_br = None - min_dist_sq = float('inf') - for j, br in enumerate(bot_br_corners): - if br[0] > tl[0] + 20 and br[1] > tl[1] + 10: - dist_sq = (br[0] - tl[0])**2 + (br[1] - tl[1])**2 - if dist_sq < min_dist_sq: - potential_br = br - min_dist_sq = dist_sq - if potential_br: - bubble_bbox = (tl[0], tl[1], potential_br[0], potential_br[1]) - all_bubbles_with_type.append((bubble_bbox, True)) - processed_bot_tls.add(i) + if potential_br_box: + # Calculate bbox using TL's top-left and BR's bottom-right + bubble_bbox = (tl_coords[0], tl_coords[1], + potential_br_box[0] + potential_br_box[2], potential_br_box[1] + potential_br_box[3]) + all_bubbles_info.append({ + 'bbox': bubble_bbox, + 'is_bot': False, + 'tl_coords': tl_coords # Store the original TL coords + }) + processed_tls.add(tl_coords) # Mark this TL as used - return all_bubbles_with_type + # --- Match Bot Bubbles (Single Type) --- + if bot_tl_boxes and bot_br_boxes: + for tl_box in bot_tl_boxes: + tl_coords = (tl_box[0], tl_box[1]) # Extract original TL (left, top) + # Skip if this TL is already part of a matched bubble + if tl_coords in processed_tls: continue + + potential_br_box = None + min_dist_sq = float('inf') + # Find the closest valid BR corner below and to the right + for br_box in bot_br_boxes: + br_coords = (br_box[0], br_box[1]) # BR top-left + if br_coords[0] > tl_coords[0] + 20 and br_coords[1] > tl_coords[1] + 10: # Basic geometric check + dist_sq = (br_coords[0] - tl_coords[0])**2 + (br_coords[1] - tl_coords[1])**2 + if dist_sq < min_dist_sq: + potential_br_box = br_box + min_dist_sq = dist_sq + + if potential_br_box: + # Calculate bbox using TL's top-left and BR's bottom-right + bubble_bbox = (tl_coords[0], tl_coords[1], + potential_br_box[0] + potential_br_box[2], potential_br_box[1] + potential_br_box[3]) + all_bubbles_info.append({ + 'bbox': bubble_bbox, + 'is_bot': True, + 'tl_coords': tl_coords # Store the original TL coords + }) + processed_tls.add(tl_coords) # Mark this TL as used + + # Note: This logic prioritizes matching regular bubbles first, then bot bubbles. + # Confidence thresholds might need tuning. + return all_bubbles_info def find_keyword_in_region(self, region: Tuple[int, int, int, int]) -> Optional[Tuple[int, int]]: - """Look for keywords within a specified region.""" + """Look for keywords within a specified region. Returns center coordinates.""" if region[2] <= 0 or region[3] <= 0: return None # Invalid region width/height - # Try lowercase - locations_lower = self._find_template('keyword_wolf_lower', region=region) + # Try original lowercase with grayscale matching + locations_lower = self._find_template('keyword_wolf_lower', region=region, grayscale=True) if locations_lower: - print(f"Found keyword (lowercase) in region {region}, position: {locations_lower[0]}") + print(f"Found keyword (lowercase, grayscale) in region {region}, position: {locations_lower[0]}") return locations_lower[0] - # Try uppercase - locations_upper = self._find_template('keyword_wolf_upper', region=region) + # Try original uppercase with grayscale matching + locations_upper = self._find_template('keyword_wolf_upper', region=region, grayscale=True) if locations_upper: - print(f"Found keyword (uppercase) in region {region}, position: {locations_upper[0]}") + print(f"Found keyword (uppercase, grayscale) in region {region}, position: {locations_upper[0]}") return locations_upper[0] + # Try type3 lowercase (white text, no grayscale) + locations_lower_type3 = self._find_template('keyword_wolf_lower_type3', region=region, grayscale=False) # Added type3 check + if locations_lower_type3: + print(f"Found keyword (lowercase, type3) in region {region}, position: {locations_lower_type3[0]}") + return locations_lower_type3[0] + + # Try type3 uppercase (white text, no grayscale) + locations_upper_type3 = self._find_template('keyword_wolf_upper_type3', region=region, grayscale=False) # Added type3 check + if locations_upper_type3: + print(f"Found keyword (uppercase, type3) in region {region}, position: {locations_upper_type3[0]}") + return locations_upper_type3[0] + return None - def calculate_avatar_coords(self, bubble_bbox: Tuple[int, int, int, int], offset_x: int = AVATAR_OFFSET_X) -> Tuple[int, int]: - """Calculate avatar coordinates based on bubble top-left.""" - tl_x, tl_y = bubble_bbox[0], bubble_bbox[1] + def calculate_avatar_coords(self, bubble_tl_coords: Tuple[int, int], offset_x: int = AVATAR_OFFSET_X) -> Tuple[int, int]: + """ + Calculate avatar coordinates based on the EXACT top-left corner coordinates of the bubble. + Uses the Y-coordinate of the TL corner directly. + """ + tl_x, tl_y = bubble_tl_coords[0], bubble_tl_coords[1] avatar_x = tl_x + offset_x - avatar_y = tl_y # Assuming Y is same as top-left - # print(f"Calculated avatar coordinates: ({int(avatar_x)}, {int(avatar_y)})") # Reduce noise + avatar_y = tl_y # Use the exact Y from the detected TL corner + # print(f"Calculated avatar coordinates using TL {bubble_tl_coords}: ({int(avatar_x)}, {int(avatar_y)})") # Reduce noise return (int(avatar_x), int(avatar_y)) def get_current_ui_state(self) -> str: @@ -277,7 +414,7 @@ class InteractionModule: time.sleep(0.1) self.click_at(coords[0], coords[1]) - time.sleep(0.2) # Wait for menu/reaction + time.sleep(0.1) # Wait for menu/reaction copied = False # Try finding "Copy" menu item first @@ -292,7 +429,7 @@ class InteractionModule: print("'Copy' menu item not found. Attempting Ctrl+C.") try: self.hotkey('ctrl', 'c') - time.sleep(0.2) + time.sleep(0.1) print("Simulated Ctrl+C.") copied = True except Exception as e_ctrlc: @@ -323,7 +460,7 @@ class InteractionModule: try: # 1. Click avatar self.click_at(avatar_coords[0], avatar_coords[1]) - time.sleep(0.3) # Wait for profile card + time.sleep(0.1) # Wait for profile card # 2. Find and click profile option profile_option_locations = self.detector._find_template('profile_option', confidence=0.7) @@ -332,7 +469,7 @@ class InteractionModule: return None # Fail early if critical step missing self.click_at(profile_option_locations[0][0], profile_option_locations[0][1]) print("Clicked user details option.") - time.sleep(0.3) # Wait for user details window + time.sleep(0.1) # Wait for user details window # 3. Find and click "Copy Name" button copy_name_locations = self.detector._find_template('copy_name_button', confidence=0.7) @@ -385,14 +522,14 @@ class InteractionModule: # Click input, paste, send self.click_at(input_coords[0], input_coords[1]) - time.sleep(0.3) + time.sleep(0.1) print("Pasting response...") self.set_clipboard(reply_text) time.sleep(0.1) try: self.hotkey('ctrl', 'v') - time.sleep(0.5) + time.sleep(0.1) print("Pasted.") except Exception as e: print(f"Error pasting response: {e}") @@ -412,12 +549,175 @@ class InteractionModule: try: self.press_key('enter') print("Pressed Enter.") - time.sleep(0.5) + time.sleep(0.1) return True except Exception as e_enter: print(f"Error pressing Enter: {e_enter}") return False +# ============================================================================== +# Position Removal Logic +# ============================================================================== +def remove_user_position(detector: DetectionModule, interactor: InteractionModule, trigger_bubble_region: Tuple[int, int, int, int]) -> bool: + """ + Performs the sequence of UI actions to remove a user's position based on the triggering chat bubble. + Returns True if successful, False otherwise. + """ + print(f"\n--- Starting Position Removal Process (Trigger Bubble Region: {trigger_bubble_region}) ---") + bubble_x, bubble_y, bubble_w, bubble_h = trigger_bubble_region # This is the BBOX, y is top + + # 1. Find the closest position icon above the bubble + search_region_y_end = bubble_y + search_region_y_start = max(0, bubble_y - 55) # Search 55 pixels above + search_region_x_start = max(0, bubble_x - 100) # Search wider horizontally + search_region_x_end = bubble_x + bubble_w + 100 + search_region = (search_region_x_start, search_region_y_start, search_region_x_end - search_region_x_start, search_region_y_end - search_region_y_start) + print(f"Searching for position icons in region: {search_region}") + + position_templates = { + 'DEVELOPMENT': POS_DEV_IMG, 'INTERIOR': POS_INT_IMG, 'SCIENCE': POS_SCI_IMG, + 'SECURITY': POS_SEC_IMG, 'STRATEGY': POS_STR_IMG + } + found_positions = [] + for name, path in position_templates.items(): + # Use unique keys for detector templates + locations = detector._find_template(name.lower() + '_pos', confidence=0.75, region=search_region) + for loc in locations: + found_positions.append({'name': name, 'coords': loc, 'path': path}) + + if not found_positions: + print("Error: No position icons found near the trigger bubble.") + return False + + # Find the closest one to the bubble's top-center + bubble_top_center_x = bubble_x + bubble_w // 2 + bubble_top_center_y = bubble_y + closest_position = min(found_positions, key=lambda p: + (p['coords'][0] - bubble_top_center_x)**2 + (p['coords'][1] - bubble_top_center_y)**2) + + target_position_name = closest_position['name'] + print(f"Found pending position: |{target_position_name}| at {closest_position['coords']}") + + # 2. Click user avatar (offset from bubble top-left) + # IMPORTANT: Use the bubble_y (top of the bbox) for the click Y coordinate. + # The AVATAR_OFFSET_X handles the horizontal positioning relative to the bubble's left edge (bubble_x). + avatar_click_x = bubble_x + AVATAR_OFFSET_X # Use constant offset + avatar_click_y = bubble_y # Use the top Y coordinate of the bubble's bounding box + print(f"Clicking avatar at estimated position: ({avatar_click_x}, {avatar_click_y}) based on bubble top-left ({bubble_x}, {bubble_y})") + interactor.click_at(avatar_click_x, avatar_click_y) + time.sleep(0.15) # Wait for profile page + + # 3. Verify Profile Page and Click Capitol Button + if not detector._find_template('profile_page', confidence=detector.state_confidence): + print("Error: Failed to verify Profile Page after clicking avatar.") + perform_state_cleanup(detector, interactor) # Attempt cleanup + return False + print("Profile page verified.") + + capitol_button_locs = detector._find_template('capitol_button', confidence=0.8) + if not capitol_button_locs: + print("Error: Capitol button (#11) not found on profile page.") + perform_state_cleanup(detector, interactor) + return False + interactor.click_at(capitol_button_locs[0][0], capitol_button_locs[0][1]) + print("Clicked Capitol button.") + time.sleep(0.15) # Wait for capitol page + + # 4. Verify Capitol Page + if not detector._find_template('president_title', confidence=detector.state_confidence): + print("Error: Failed to verify Capitol Page (President Title not found).") + perform_state_cleanup(detector, interactor) + return False + print("Capitol page verified.") + + # 5. Find and Click Corresponding Position Button + position_button_templates = { + 'DEVELOPMENT': 'pos_btn_dev', 'INTERIOR': 'pos_btn_int', 'SCIENCE': 'pos_btn_sci', + 'SECURITY': 'pos_btn_sec', 'STRATEGY': 'pos_btn_str' + } + target_button_key = position_button_templates.get(target_position_name) + if not target_button_key: + print(f"Error: Internal error - unknown position name '{target_position_name}'") + perform_state_cleanup(detector, interactor) + return False + + pos_button_locs = detector._find_template(target_button_key, confidence=0.8) + if not pos_button_locs: + print(f"Error: Position button for '{target_position_name}' not found on Capitol page.") + perform_state_cleanup(detector, interactor) + return False + interactor.click_at(pos_button_locs[0][0], pos_button_locs[0][1]) + print(f"Clicked '{target_position_name}' position button.") + time.sleep(0.15) # Wait for position page + + # 6. Verify Position Page + position_page_templates = { + 'DEVELOPMENT': 'page_dev', 'INTERIOR': 'page_int', 'SCIENCE': 'page_sci', + 'SECURITY': 'page_sec', 'STRATEGY': 'page_str' + } + target_page_key = position_page_templates.get(target_position_name) + if not target_page_key: + print(f"Error: Internal error - unknown position name '{target_position_name}' for page verification") + perform_state_cleanup(detector, interactor) + return False + + if not detector._find_template(target_page_key, confidence=detector.state_confidence): + print(f"Error: Failed to verify correct position page for '{target_position_name}'.") + perform_state_cleanup(detector, interactor) + return False + print(f"Verified '{target_position_name}' position page.") + + # 7. Find and Click Dismiss Button + dismiss_locs = detector._find_template('dismiss_button', confidence=0.8) + if not dismiss_locs: + print("Error: Dismiss button not found on position page.") + perform_state_cleanup(detector, interactor) + return False + interactor.click_at(dismiss_locs[0][0], dismiss_locs[0][1]) + print("Clicked Dismiss button.") + time.sleep(0.1) # Wait for confirmation + + # 8. Find and Click Confirm Button + confirm_locs = detector._find_template('confirm_button', confidence=0.8) + if not confirm_locs: + print("Error: Confirm button not found after clicking dismiss.") + # Don't cleanup here, might be stuck in confirmation state + return False # Indicate failure, but let main loop decide next step + interactor.click_at(confirm_locs[0][0], confirm_locs[0][1]) + print("Clicked Confirm button. Position should be dismissed.") + time.sleep(0.1) # Wait for action to complete + + # 9. Cleanup: Return to Chat Room + # Click Close on position page (should now be back on capitol page implicitly) + close_locs = detector._find_template('close_button', confidence=0.8) + if close_locs: + interactor.click_at(close_locs[0][0], close_locs[0][1]) + print("Clicked Close button (returning to Capitol).") + time.sleep(0.15) + else: + print("Warning: Close button not found after confirm, attempting back arrow anyway.") + + # Click Back Arrow on Capitol page (should return to profile) + back_arrow_locs = detector._find_template('back_arrow', confidence=0.8) + if back_arrow_locs: + interactor.click_at(back_arrow_locs[0][0], back_arrow_locs[0][1]) + print("Clicked Back Arrow (returning to Profile).") + time.sleep(0.15) + else: + print("Warning: Back arrow not found on Capitol page, attempting ESC cleanup.") + + # Use standard ESC cleanup + print("Initiating final ESC cleanup to return to chat...") + cleanup_success = perform_state_cleanup(detector, interactor) + + if cleanup_success: + print("--- Position Removal Process Completed Successfully ---") + return True + else: + print("--- Position Removal Process Completed, but failed to confirm return to chat room ---") + return False # Technically removed, but UI state uncertain + + # ============================================================================== # Coordinator Logic (Placeholder - To be implemented in main.py) # ============================================================================== @@ -432,7 +732,7 @@ def perform_state_cleanup(detector: DetectionModule, interactor: InteractionModu returned_to_chat = False for attempt in range(max_attempts): print(f"Cleanup attempt #{attempt + 1}/{max_attempts}") - time.sleep(0.2) + time.sleep(0.1) current_state = detector.get_current_ui_state() print(f"Detected state: {current_state}") @@ -444,14 +744,14 @@ def perform_state_cleanup(detector: DetectionModule, interactor: InteractionModu elif current_state == 'user_details' or current_state == 'profile_card': print(f"{current_state.replace('_', ' ').title()} detected, pressing ESC...") interactor.press_key('esc') - time.sleep(0.3) # Wait longer for UI response after ESC + time.sleep(0.1) # Wait longer for UI response after ESC continue else: # Unknown state print("Unknown page state detected.") if attempt < max_attempts - 1: print("Trying one ESC press as fallback...") interactor.press_key('esc') - time.sleep(0.3) + time.sleep(0.1) else: print("Maximum attempts reached, stopping cleanup.") break @@ -473,26 +773,64 @@ def run_ui_monitoring_loop(trigger_queue: queue.Queue, command_queue: queue.Queu # Load templates directly using constants defined in this file for now # Consider passing config or a template loader object in the future templates = { + # Regular Bubble (Original + Skins) - Keys match those used in find_dialogue_bubbles 'corner_tl': CORNER_TL_IMG, 'corner_br': CORNER_BR_IMG, + 'corner_tl_type2': CORNER_TL_TYPE2_IMG, 'corner_br_type2': CORNER_BR_TYPE2_IMG, + 'corner_tl_type3': CORNER_TL_TYPE3_IMG, 'corner_br_type3': CORNER_BR_TYPE3_IMG, # Corrected: Added missing keys here + # Bot Bubble (Single Type) 'bot_corner_tl': BOT_CORNER_TL_IMG, 'bot_corner_br': BOT_CORNER_BR_IMG, - 'keyword_wolf_lower': KEYWORD_wolf_LOWER_IMG, 'keyword_wolf_upper': KEYWORD_Wolf_UPPER_IMG, + # Keywords & UI Elements + 'keyword_wolf_lower': KEYWORD_wolf_LOWER_IMG, + 'keyword_wolf_upper': KEYWORD_Wolf_UPPER_IMG, + 'keyword_wolf_lower_type3': KEYWORD_wolf_LOWER_TYPE3_IMG, # Added + 'keyword_wolf_upper_type3': KEYWORD_Wolf_UPPER_TYPE3_IMG, # Added 'copy_menu_item': COPY_MENU_ITEM_IMG, 'profile_option': PROFILE_OPTION_IMG, 'copy_name_button': COPY_NAME_BUTTON_IMG, 'send_button': SEND_BUTTON_IMG, 'chat_input': CHAT_INPUT_IMG, 'profile_name_page': PROFILE_NAME_PAGE_IMG, 'profile_page': PROFILE_PAGE_IMG, 'chat_room': CHAT_ROOM_IMG, - 'world_chat': WORLD_CHAT_IMG, 'private_chat': PRIVATE_CHAT_IMG # Add other templates as needed + 'base_screen': BASE_SCREEN_IMG, 'world_map_screen': WORLD_MAP_IMG, # Added for navigation + 'world_chat': WORLD_CHAT_IMG, 'private_chat': PRIVATE_CHAT_IMG, + # Add position templates + 'development_pos': POS_DEV_IMG, 'interior_pos': POS_INT_IMG, 'science_pos': POS_SCI_IMG, + 'security_pos': POS_SEC_IMG, 'strategy_pos': POS_STR_IMG, + # Add capitol templates + 'capitol_button': CAPITOL_BUTTON_IMG, 'president_title': PRESIDENT_TITLE_IMG, + 'pos_btn_dev': POS_BTN_DEV_IMG, 'pos_btn_int': POS_BTN_INT_IMG, 'pos_btn_sci': POS_BTN_SCI_IMG, + 'pos_btn_sec': POS_BTN_SEC_IMG, 'pos_btn_str': POS_BTN_STR_IMG, + 'page_dev': PAGE_DEV_IMG, 'page_int': PAGE_INT_IMG, 'page_sci': PAGE_SCI_IMG, + 'page_sec': PAGE_SEC_IMG, 'page_str': PAGE_STR_IMG, + 'dismiss_button': DISMISS_BUTTON_IMG, 'confirm_button': CONFIRM_BUTTON_IMG, + 'close_button': CLOSE_BUTTON_IMG, 'back_arrow': BACK_ARROW_IMG } # Use default confidence/region settings from constants detector = DetectionModule(templates, confidence=CONFIDENCE_THRESHOLD, state_confidence=STATE_CONFIDENCE_THRESHOLD, region=SCREENSHOT_REGION) # Use default input coords/keys from constants interactor = InteractionModule(detector, input_coords=(CHAT_INPUT_CENTER_X, CHAT_INPUT_CENTER_Y), input_template_key='chat_input', send_button_key='send_button') - # --- State Management (Local to this monitoring thread) --- - last_processed_bubble_bbox = None +# --- State Management (Local to this monitoring thread) --- + last_processed_bubble_info = None # Store the whole dict now recent_texts = collections.deque(maxlen=RECENT_TEXT_HISTORY_MAXLEN) # Context-specific history needed + screenshot_counter = 0 # Initialize counter for debug screenshots while True: - # --- Process Commands First (Non-blocking) --- + # --- Check for Main Screen Navigation First --- + try: + base_locs = detector._find_template('base_screen', confidence=0.8) + map_locs = detector._find_template('world_map_screen', confidence=0.8) + if base_locs or map_locs: + print("UI Thread: Detected main screen (Base or World Map). Clicking to return to chat...") + # Coordinates provided by user (adjust if needed based on actual screen resolution/layout) + # IMPORTANT: Ensure these coordinates are correct for the target window/resolution + target_x, target_y = 600, 1300 + interactor.click_at(target_x, target_y) + time.sleep(0.2) # Short delay after click + print("UI Thread: Clicked to return to chat. Re-checking screen state...") + continue # Skip the rest of the loop and re-evaluate + except Exception as nav_err: + print(f"UI Thread: Error during main screen navigation check: {nav_err}") + # Decide if you want to continue or pause after error + + # --- Process Commands Second (Non-blocking) --- try: command_data = command_queue.get_nowait() # Check for commands without blocking action = command_data.get('action') @@ -503,6 +841,16 @@ def run_ui_monitoring_loop(trigger_queue: queue.Queue, command_queue: queue.Queu interactor.send_chat_message(text_to_send) else: print("UI Thread: Received send_reply command with no text.") + elif action == 'remove_position': # <--- Handle new command + region = command_data.get('trigger_bubble_region') + if region: + print(f"UI Thread: Received command to remove position triggered by bubble region: {region}") + # Call the new UI function + success = remove_user_position(detector, interactor, region) # Call synchronous function + print(f"UI Thread: Position removal attempt finished. Success: {success}") + # Note: No need to send result back unless main thread needs confirmation + else: + print("UI Thread: Received remove_position command without trigger_bubble_region.") else: print(f"UI Thread: Received unknown command: {action}") except queue.Empty: @@ -510,49 +858,100 @@ def run_ui_monitoring_loop(trigger_queue: queue.Queue, command_queue: queue.Queu except Exception as cmd_err: print(f"UI Thread: Error processing command queue: {cmd_err}") - # --- Then Perform UI Monitoring --- + # --- Verify Chat Room State Before Bubble Detection --- + try: + # Use a slightly lower confidence maybe, or state_confidence + chat_room_locs = detector._find_template('chat_room', confidence=detector.state_confidence) + if not chat_room_locs: + print("UI Thread: Not in chat room state before bubble detection. Attempting cleanup...") + # Call the existing cleanup function to try and return + perform_state_cleanup(detector, interactor) + # Regardless of cleanup success, restart the loop to re-evaluate state from the top + print("UI Thread: Continuing loop after attempting chat room cleanup.") + time.sleep(0.5) # Small pause after cleanup attempt + continue + # else: # Optional: Log if chat room is confirmed + # print("UI Thread: Chat room state confirmed.") + + except Exception as state_check_err: + print(f"UI Thread: Error checking for chat room state: {state_check_err}") + # Decide how to handle error - maybe pause and retry? For now, continue cautiously. + time.sleep(1) + + + # --- Then Perform UI Monitoring (Bubble Detection) --- try: # 1. Detect Bubbles - all_bubbles = detector.find_dialogue_bubbles() - if not all_bubbles: time.sleep(2); continue + all_bubbles_data = detector.find_dialogue_bubbles() # Returns list of dicts + if not all_bubbles_data: time.sleep(2); continue # Filter out bot bubbles, find newest non-bot bubble (example logic) - other_bubbles = [bbox for bbox, is_bot in all_bubbles if not is_bot] - if not other_bubbles: time.sleep(2); continue + other_bubbles_data = [b_info for b_info in all_bubbles_data if not b_info['is_bot']] + if not other_bubbles_data: time.sleep(0.2); continue # Simple logic: assume lowest bubble is newest (might need improvement) - target_bubble = max(other_bubbles, key=lambda b: b[3]) # b[3] is y_max + # Sort by bbox bottom y-coordinate (index 3) + target_bubble_info = max(other_bubbles_data, key=lambda b_info: b_info['bbox'][3]) # 2. Check for Duplicates (Position & Content) - if are_bboxes_similar(target_bubble, last_processed_bubble_bbox): - time.sleep(2); continue + # Compare using the 'bbox' from the info dicts + if are_bboxes_similar(target_bubble_info.get('bbox'), last_processed_bubble_info.get('bbox') if last_processed_bubble_info else None): + time.sleep(0.2); continue # 3. Detect Keyword in Bubble - bubble_region = (target_bubble[0], target_bubble[1], target_bubble[2]-target_bubble[0], target_bubble[3]-target_bubble[1]) + target_bbox = target_bubble_info['bbox'] + bubble_region = (target_bbox[0], target_bbox[1], target_bbox[2]-target_bbox[0], target_bbox[3]-target_bbox[1]) keyword_coords = detector.find_keyword_in_region(bubble_region) if keyword_coords: - print(f"\n!!! Keyword detected in bubble {target_bubble} !!!") + print(f"\n!!! Keyword detected in bubble {target_bbox} !!!") + + # --- Debug Screenshot Logic --- + try: + screenshot_index = (screenshot_counter % MAX_DEBUG_SCREENSHOTS) + 1 + screenshot_filename = f"debug_bubble_{screenshot_index}.png" + screenshot_path = os.path.join(DEBUG_SCREENSHOT_DIR, screenshot_filename) + # --- Enhanced Logging --- + print(f"Attempting to save debug screenshot to: {screenshot_path}") + print(f"Screenshot Region: {bubble_region}") + # Check if region is valid before attempting screenshot + if bubble_region and len(bubble_region) == 4 and bubble_region[2] > 0 and bubble_region[3] > 0: + # Convert numpy types to standard Python int for pyautogui + region_int = (int(bubble_region[0]), int(bubble_region[1]), int(bubble_region[2]), int(bubble_region[3])) + pyautogui.screenshot(region=region_int, imageFilename=screenshot_path) + print(f"Successfully saved debug screenshot: {screenshot_path}") + screenshot_counter += 1 + else: + print(f"Error: Invalid screenshot region {bubble_region}. Skipping screenshot.") + # --- End Enhanced Logging --- + except Exception as ss_err: + # --- Enhanced Error Logging --- + print(f"Error taking debug screenshot for region {bubble_region} to path {screenshot_path}: {repr(ss_err)}") + import traceback + traceback.print_exc() # Print full traceback for detailed debugging + # --- End Enhanced Error Logging --- + # --- End Debug Screenshot Logic --- # 4. Interact: Get Bubble Text bubble_text = interactor.copy_text_at(keyword_coords) if not bubble_text: print("Error: Could not get dialogue content.") - last_processed_bubble_bbox = target_bubble # Mark as processed even if failed + last_processed_bubble_info = target_bubble_info # Mark as processed even if failed perform_state_cleanup(detector, interactor) # Attempt cleanup after failed copy continue # Check recent text history (needs context awareness) if bubble_text in recent_texts: print(f"Content '{bubble_text[:30]}...' in recent history, skipping.") - last_processed_bubble_bbox = target_bubble + last_processed_bubble_info = target_bubble_info continue print(">>> New trigger event <<<") - last_processed_bubble_bbox = target_bubble + last_processed_bubble_info = target_bubble_info recent_texts.append(bubble_text) # 5. Interact: Get Sender Name - avatar_coords = detector.calculate_avatar_coords(target_bubble) + # *** Use the precise TL coordinates for avatar calculation *** + avatar_coords = detector.calculate_avatar_coords(target_bubble_info['tl_coords']) sender_name = interactor.retrieve_sender_name_interaction(avatar_coords) # 6. Perform Cleanup (Crucial after potentially leaving chat screen) @@ -569,15 +968,22 @@ def run_ui_monitoring_loop(trigger_queue: queue.Queue, command_queue: queue.Queu print("\n>>> Putting trigger info in Queue <<<") print(f" Sender: {sender_name}") print(f" Content: {bubble_text[:100]}...") + print(f" Bubble Region: {bubble_region}") # Include region derived from bbox try: - data_to_send = {'sender': sender_name, 'text': bubble_text} + # Include bubble_region in the data sent + data_to_send = { + 'sender': sender_name, + 'text': bubble_text, + 'bubble_region': bubble_region # Use bbox-derived region for general use + # 'tl_coords': target_bubble_info['tl_coords'] # Optionally send if needed elsewhere + } trigger_queue.put(data_to_send) # Put in the queue for main loop - print("Trigger info placed in Queue.") + print("Trigger info (with region) placed in Queue.") except Exception as q_err: print(f"Error putting data in Queue: {q_err}") print("--- Single trigger processing complete ---") - time.sleep(1) # Pause after successful trigger + time.sleep(0.1) # Pause after successful trigger time.sleep(1.5) # Polling interval @@ -591,8 +997,8 @@ def run_ui_monitoring_loop(trigger_queue: queue.Queue, command_queue: queue.Queu # Attempt cleanup in case of unexpected error during interaction print("Attempting cleanup after unexpected error...") perform_state_cleanup(detector, interactor) - print("Waiting 5 seconds before retry...") - time.sleep(5) + print("Waiting 3 seconds before retry...") + time.sleep(3) # Note: The old monitor_chat_for_trigger function is replaced by the example_coordinator_loop. # The actual UI monitoring thread started in main.py should call a function like this example loop. From 3ec4017a1efae2c0cf1adf8fc58b6157d595a879 Mon Sep 17 00:00:00 2001 From: z060142 Date: Sat, 19 Apr 2025 01:37:57 +0800 Subject: [PATCH 2/7] Add Reply to specific conversation. Fix Conversation bubble detection --- ClaudeCode.md | 109 +++++- main.py | 80 +++- templates/reply_button.png | Bin 0 -> 2863 bytes ui_interaction.py | 740 ++++++++++++++++++++++++++++++------- 4 files changed, 771 insertions(+), 158 deletions(-) create mode 100644 templates/reply_button.png diff --git a/ClaudeCode.md b/ClaudeCode.md index 42bb7a4..85484a8 100644 --- a/ClaudeCode.md +++ b/ClaudeCode.md @@ -75,11 +75,26 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機 系統使用基於圖像辨識的方法監控遊戲聊天界面: -1. **泡泡檢測**:通過辨識聊天泡泡的左上角 (TL) 和右下角 (BR) 角落圖案定位聊天訊息。系統能區分一般用戶泡泡和機器人泡泡。**為了適應玩家可能使用的不同聊天泡泡外觀 (skin),一般用戶泡泡的偵測機制已被擴充,可以同時尋找多組不同的角落模板 (例如 `corner_tl_type2.png`, `corner_br_type2.png` 等),提高了對自訂外觀的兼容性。機器人泡泡目前僅偵測預設的角落模板。** -2. **關鍵字檢測**:在泡泡區域內搜尋 "wolf" 或 "Wolf" 關鍵字圖像 -3. **內容獲取**:點擊關鍵字位置,使用剪貼板複製聊天內容 -4. **發送者識別**:**關鍵步驟** - 系統會根據**偵測到關鍵字的那個特定聊天泡泡**的左上角座標,計算出頭像的點擊位置(目前水平偏移量為 -55 像素)。這確保了點擊的是觸發訊息的發送者頭像,而不是其他位置的頭像。接著通過點擊計算出的頭像位置,導航菜單,最終複製用戶名稱。 -5. **防重複處理**:使用位置比較和內容歷史記錄防止重複回應 +1. **泡泡檢測(含 Y 軸優先配對)**:通過辨識聊天泡泡的左上角 (TL) 和右下角 (BR) 角落圖案定位聊天訊息。 + - **多外觀支援**:為了適應玩家可能使用的不同聊天泡泡外觀 (skin),一般用戶泡泡的偵測機制已被擴充,可以同時尋找多組不同的角落模板 (例如 `corner_tl_type2.png`, `corner_br_type2.png` 等)。機器人泡泡目前僅偵測預設的角落模板。 + - **配對邏輯優化**:在配對 TL 和 BR 角落時,系統現在會優先選擇與 TL 角落 **Y 座標最接近** 的有效 BR 角落,以更好地區分垂直堆疊的聊天泡泡。 +2. **關鍵字檢測**:在泡泡區域內搜尋 "wolf" 或 "Wolf" 關鍵字圖像。 +3. **內容獲取**:點擊關鍵字位置,使用剪貼板複製聊天內容。 +4. **發送者識別(含氣泡重新定位與偏移量調整)**:**關鍵步驟** - 為了提高在動態聊天環境下的穩定性,系統在獲取發送者名稱前,會執行以下步驟: + a. **初始偵測**:像之前一樣,根據偵測到的關鍵字定位觸發的聊天泡泡。 + b. **氣泡快照**:擷取該聊天泡泡的圖像快照。 + c. **重新定位**:在點擊頭像前,使用該快照在當前聊天視窗區域內重新搜尋氣泡的最新位置。 + d. **計算座標(新偏移量)**: + - 如果成功重新定位氣泡,則根據找到的**新**左上角座標 (`new_tl_x`, `new_tl_y`),應用新的偏移量計算頭像點擊位置:`x = new_tl_x - 45` (`AVATAR_OFFSET_X_REPLY`),`y = new_tl_y + 10` (`AVATAR_OFFSET_Y_REPLY`)。 + - 如果無法重新定位(例如氣泡已滾動出畫面),則跳過此次互動,以避免點擊錯誤位置。 + e. **互動(含重試)**: + - 使用計算出的(新的)頭像位置進行第一次點擊。 + - 檢查是否成功進入個人資料頁面 (`Profile_page.png`)。 + - **如果失敗**:系統會使用步驟 (b) 的氣泡快照,在聊天區域內重新定位氣泡,重新計算頭像座標,然後再次嘗試點擊。此過程最多重複 3 次。 + - **如果成功**(無論是首次嘗試還是重試成功):繼續導航菜單,最終複製用戶名稱。 + - **如果重試後仍失敗**:放棄獲取該用戶名稱。 + f. **原始偏移量**:原始的 `-55` 像素水平偏移量 (`AVATAR_OFFSET_X`) 仍保留在程式碼中,用於其他不需要重新定位或不同互動邏輯的場景(例如 `remove_user_position` 功能)。 +5. **防重複處理**:使用位置比較和內容歷史記錄防止重複回應。 #### LLM 整合 @@ -112,10 +127,20 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機 系統使用多種技術實現 UI 自動化: -1. **圖像辨識**:使用 OpenCV 和 pyautogui 進行圖像匹配和識別 -2. **鍵鼠控制**:模擬鼠標點擊和鍵盤操作 -3. **剪貼板操作**:使用 pyperclip 讀寫剪貼板 -4. **狀態式處理**:基於 UI 狀態判斷的互動流程,確保操作穩定性 +1. **圖像辨識**:使用 OpenCV 和 pyautogui 進行圖像匹配和識別。 +2. **鍵鼠控制**:模擬鼠標點擊和鍵盤操作。 +3. **剪貼板操作**:使用 pyperclip 讀寫剪貼板。 +4. **狀態式處理**:基於 UI 狀態判斷的互動流程,確保操作穩定性。 +5. **針對性回覆(上下文激活)**: + - **時機**:在成功獲取發送者名稱並返回聊天介面後,但在將觸發資訊放入隊列傳遞給主線程之前。 + - **流程**: + a. 再次使用氣泡快照重新定位觸發訊息的氣泡。 + b. 如果定位成功,點擊氣泡中心,並等待 0.25 秒(增加的延遲時間)以允許 UI 反應。 + c. 尋找並點擊彈出的「回覆」按鈕 (`reply_button.png`)。 + d. 如果成功點擊回覆按鈕,則設置一個 `reply_context_activated` 標記為 `True`。 + e. 如果重新定位氣泡失敗或未找到回覆按鈕,則該標記為 `False`。 + - **傳遞**:將 `reply_context_activated` 標記連同其他觸發資訊(發送者、內容、氣泡區域)一起放入隊列。 + - **發送**:主控模塊 (`main.py`) 在處理 `send_reply` 命令時,不再需要執行點擊回覆的操作,只需直接調用 `send_chat_message` 即可(因為如果 `reply_context_activated` 為 `True`,輸入框應已準備好)。 ## 配置與部署 @@ -192,6 +217,72 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機 - 在「技術實現」的「發送者識別」部分強調了點擊位置是相對於觸發泡泡計算的,並註明了新的偏移量。 - 添加了此「最近改進」條目。 +### 聊天泡泡重新定位以提高穩定性 + +- **UI 互動模塊 (`ui_interaction.py`)**: + - 在 `run_ui_monitoring_loop` 中,於偵測到關鍵字並成功複製文字後、獲取發送者名稱前,加入了新的邏輯: + 1. 擷取觸發氣泡的圖像快照。 + 2. 使用 `pyautogui.locateOnScreen` 在聊天區域內重新尋找該快照的當前位置。 + 3. 若找到,則根據**新位置**的左上角座標和新的偏移量 (`AVATAR_OFFSET_X_RELOCATED = -50`) 計算頭像點擊位置。 + 4. 若找不到,則記錄警告並跳過此次互動。 + - 新增了 `AVATAR_OFFSET_X_RELOCATED` 和 `BUBBLE_RELOCATE_CONFIDENCE` 常數。 +- **目的**:解決聊天視窗內容滾動後,原始偵測到的氣泡位置失效,導致點擊錯誤頭像的問題。透過重新定位,確保點擊的是與觸發訊息相對應的頭像。 +- **文件更新 (`ClaudeCode.md`)**: + - 更新了「技術實現」中的「發送者識別」部分,詳細說明了重新定位的步驟。 + - 在此「最近改進」部分添加了這個新條目。 + +### 互動流程優化 (頭像偏移、氣泡配對、針對性回覆) + +- **UI 互動模塊 (`ui_interaction.py`)**: + - **頭像偏移量調整**:修改了重新定位氣泡後計算頭像座標的邏輯,使用新的偏移量:左 `-45` (`AVATAR_OFFSET_X_REPLY`),下 `+10` (`AVATAR_OFFSET_Y_REPLY`)。原始的 `-55` 偏移量 (`AVATAR_OFFSET_X`) 保留用於其他功能。 + - **氣泡配對優化**:修改 `find_dialogue_bubbles` 函數,使其在配對左上角 (TL) 和右下角 (BR) 時,優先選擇 Y 座標差異最小的 BR 角落,以提高垂直相鄰氣泡的區分度。 + - **頭像點擊重試**:修改 `retrieve_sender_name_interaction` 函數,增加了最多 3 次的重試邏輯。如果在點擊頭像後未能檢測到個人資料頁面,會嘗試重新定位氣泡並再次點擊。 + - **針對性回覆時機調整與延遲增加**: + - 將點擊氣泡中心和回覆按鈕的操作移至成功獲取發送者名稱並返回聊天室之後、將觸發資訊放入隊列之前。 + - **增加了點擊氣泡中心後、尋找回覆按鈕前的等待時間至 0.25 秒**,以提高在 UI 反應較慢時找到按鈕的成功率。 + - 在放入隊列的數據中增加 `reply_context_activated` 標記,指示是否成功激活了回覆上下文。 + - 簡化了處理 `send_reply` 命令的邏輯,使其僅負責發送消息。 + - **氣泡快照保存 (用於除錯)**:在偵測到關鍵字後,擷取用於重新定位的氣泡圖像快照 (`bubble_snapshot`) 時,會將此快照保存到 `debug_screenshots` 文件夾中,檔名格式為 `debug_relocation_snapshot_X.png` (X 為 1 到 5 的循環數字)。這取代了先前僅保存氣泡區域截圖的邏輯。 +- **目的**: + - 進一步提高獲取發送者名稱的穩定性。 + - 改善氣泡配對的準確性。 + - 調整針對性回覆的流程,使其更符合邏輯順序,並通過增加延遲提高可靠性。 + - 提供用於重新定位的實際圖像快照,方便除錯。 +- **文件更新 (`ClaudeCode.md`)**: + - 更新了「技術實現」中的「泡泡檢測」、「發送者識別」部分。 + - 更新了「UI 自動化」部分關於「針對性回覆」的說明,反映了新的時機、標記和增加的延遲。 + - 在此「最近改進」部分更新了這個匯總條目,以包含最新的修改(包括快照保存和延遲增加)。 + +### UI 監控暫停與恢復機制 (2025-04-18) + +- **目的**:解決在等待 LLM 回應期間,持續的 UI 監控可能導致的不穩定性或干擾問題,特別是與 `remove_position` 等需要精確 UI 狀態的操作相關。 +- **`ui_interaction.py`**: + - 引入了全局(模塊級)`monitoring_paused_flag` 列表(包含一個布爾值)。 + - 在 `run_ui_monitoring_loop` 的主循環開始處檢查此標誌。若為 `True`,則循環僅檢查命令隊列中的 `resume` 命令並休眠,跳過所有 UI 偵測和觸發邏輯。 + - 在命令處理邏輯中添加了對 `pause` 和 `resume` 動作的處理,分別設置 `monitoring_paused_flag[0]` 為 `True` 或 `False`。 +- **`ui_interaction.py` (進一步修改)**: + - **修正命令處理邏輯**:修改了 `run_ui_monitoring_loop` 的主循環。現在,在每次迭代開始時,它會使用一個內部 `while True` 循環和 `command_queue.get_nowait()` 來**處理完隊列中所有待處理的命令**(包括 `pause`, `resume`, `send_reply`, `remove_position` 等)。 + - **狀態檢查後置**:只有在清空當前所有命令後,循環才會檢查 `monitoring_paused_flag` 的狀態。如果標誌為 `True`,則休眠並跳過 UI 監控部分;如果為 `False`,則繼續執行 UI 監控(畫面檢查、氣泡偵測等)。 + - **目的**:解決先前版本中 `resume` 命令可能導致 UI 線程過早退出暫停狀態,從而錯過緊隨其後的 `send_reply` 或 `remove_position` 命令的問題。確保所有來自 `main.py` 的命令都被及時處理。 +- **`main.py`**: + - (先前修改保持不變)在主處理循環 (`run_main_with_exit_stack` 的 `while True` 循環) 中: + - 在從 `trigger_queue` 獲取數據後、調用 `llm_interaction.get_llm_response` **之前**,向 `command_queue` 發送 `{ 'action': 'pause' }` 命令。 + - 使用 `try...finally` 結構,確保在處理 LLM 回應(包括命令處理和發送回覆)**之後**,向 `command_queue` 發送 `{ 'action': 'resume' }` 命令,無論處理過程中是否發生錯誤。 + +### `remove_position` 穩定性改進 (使用快照重新定位) (2025-04-19) + +- **目的**:解決 `remove_position` 命令因聊天視窗滾動導致基於舊氣泡位置計算座標而出錯的問題。 +- **`ui_interaction.py` (`run_ui_monitoring_loop`)**: + - 在觸發事件放入 `trigger_queue` 的數據中,額外添加了 `bubble_snapshot`(觸發氣泡的圖像快照)和 `search_area`(用於快照的搜索區域)。 +- **`main.py`**: + - 修改了處理 `remove_position` 命令的邏輯,使其從 `trigger_data` 中提取 `bubble_snapshot` 和 `search_area`,並將它們包含在發送給 `command_queue` 的命令數據中。 +- **`ui_interaction.py` (`remove_user_position` 函數)**: + - 修改了函數簽名,以接收 `bubble_snapshot` 和 `search_area` 參數。 + - 在函數執行開始時,使用傳入的 `bubble_snapshot` 和 `search_area` 調用 `pyautogui.locateOnScreen` 來重新定位觸發氣泡的當前位置。 + - 如果重新定位失敗,則記錄錯誤並返回 `False`。 + - 如果重新定位成功,則後續所有基於氣泡位置的計算(包括尋找職位圖標的搜索區域 `search_region` 和點擊頭像的座標 `avatar_click_x`, `avatar_click_y`)都將使用這個**新找到的**氣泡座標。 +- **效果**:確保 `remove_position` 操作基於氣泡的最新位置執行,提高了在動態滾動的聊天界面中的可靠性。 + ## 開發建議 ### 優化方向 diff --git a/main.py b/main.py index 395b8f7..b0281b1 100644 --- a/main.py +++ b/main.py @@ -225,9 +225,21 @@ async def run_main_with_exit_stack(): # Use run_in_executor to wait for item from standard queue trigger_data = await loop.run_in_executor(None, trigger_queue.get) + # --- Pause UI Monitoring --- + print("Pausing UI monitoring before LLM call...") + pause_command = {'action': 'pause'} + try: + await loop.run_in_executor(None, command_queue.put, pause_command) + print("Pause command placed in queue.") + except Exception as q_err: + print(f"Error putting pause command in queue: {q_err}") + # --- End Pause --- + sender_name = trigger_data.get('sender') bubble_text = trigger_data.get('text') bubble_region = trigger_data.get('bubble_region') # <-- Extract bubble_region + bubble_snapshot = trigger_data.get('bubble_snapshot') # <-- Extract snapshot + search_area = trigger_data.get('search_area') # <-- Extract search_area print(f"\n--- Received trigger from UI ---") print(f" Sender: {sender_name}") print(f" Content: {bubble_text[:100]}...") @@ -262,25 +274,61 @@ async def run_main_with_exit_stack(): cmd_type = cmd.get("type", "") cmd_params = cmd.get("parameters", {}) # Parameters might be empty for remove_position - # --- Command Processing --- +# --- Command Processing --- if cmd_type == "remove_position": if bubble_region: # Check if we have the context - print("Sending 'remove_position' command to UI thread...") - command_to_send = { - 'action': 'remove_position', - 'trigger_bubble_region': bubble_region # Pass the region - } - try: - await loop.run_in_executor(None, command_queue.put, command_to_send) - print("Command placed in queue.") - except Exception as q_err: - print(f"Error putting remove_position command in queue: {q_err}") + # Debug info - print what we have + print(f"Processing remove_position command with:") + print(f" bubble_region: {bubble_region}") + print(f" bubble_snapshot available: {'Yes' if bubble_snapshot is not None else 'No'}") + print(f" search_area available: {'Yes' if search_area is not None else 'No'}") + + # Check if we have snapshot and search_area as well + if bubble_snapshot and search_area: + print("Sending 'remove_position' command to UI thread with snapshot and search area...") + command_to_send = { + 'action': 'remove_position', + 'trigger_bubble_region': bubble_region, # Original region (might be outdated) + 'bubble_snapshot': bubble_snapshot, # Snapshot for re-location + 'search_area': search_area # Area to search in + } + try: + await loop.run_in_executor(None, command_queue.put, command_to_send) + except Exception as q_err: + print(f"Error putting remove_position command in queue: {q_err}") + else: + # If we have bubble_region but missing other parameters, use a dummy search area + # and let UI thread take a new screenshot + print("Missing bubble_snapshot or search_area, trying with defaults...") + + # Use the bubble_region itself as a fallback search area if needed + default_search_area = None + if search_area is None and bubble_region: + # Convert bubble_region to a proper search area format if needed + if len(bubble_region) == 4: + default_search_area = bubble_region + + command_to_send = { + 'action': 'remove_position', + 'trigger_bubble_region': bubble_region, + 'bubble_snapshot': bubble_snapshot, # Pass as is, might be None + 'search_area': default_search_area if search_area is None else search_area + } + + try: + await loop.run_in_executor(None, command_queue.put, command_to_send) + print("Command sent with fallback parameters.") + except Exception as q_err: + print(f"Error putting remove_position command in queue: {q_err}") else: print("Error: Cannot process 'remove_position' command without bubble_region context.") # Add other command handling here if needed # elif cmd_type == "some_other_command": # # Handle other commands # pass + # elif cmd_type == "some_other_command": + # # Handle other commands + # pass else: print(f"Received unhandled command type: {cmd_type}, parameters: {cmd_params}") # --- End Command Processing --- @@ -307,6 +355,16 @@ async def run_main_with_exit_stack(): print(f"\nError processing trigger or sending response: {e}") import traceback traceback.print_exc() + finally: + # --- Resume UI Monitoring --- + print("Resuming UI monitoring after processing...") + resume_command = {'action': 'resume'} + try: + await loop.run_in_executor(None, command_queue.put, resume_command) + print("Resume command placed in queue.") + except Exception as q_err: + print(f"Error putting resume command in queue: {q_err}") + # --- End Resume --- # No task_done needed for standard queue except asyncio.CancelledError: diff --git a/templates/reply_button.png b/templates/reply_button.png new file mode 100644 index 0000000000000000000000000000000000000000..b6a8bd223258441799e219475a9c52f8ffde1cca GIT binary patch literal 2863 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 N002ovPDHLkV1nN#ssI20 literal 0 HcmV?d00001 diff --git a/ui_interaction.py b/ui_interaction.py index bbd9db7..497b605 100644 --- a/ui_interaction.py +++ b/ui_interaction.py @@ -13,6 +13,12 @@ import pygetwindow as gw # Used to check/activate windows import config # Used to read window title import queue from typing import List, Tuple, Optional, Dict, Any +import threading # Import threading for Lock if needed, or just use a simple flag + +# --- Global Pause Flag --- +# Using a simple mutable object (list) for thread-safe-like access without explicit lock +# Or could use threading.Event() +monitoring_paused_flag = [False] # List containing a boolean # --- Configuration Section --- SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -21,7 +27,7 @@ os.makedirs(TEMPLATE_DIR, exist_ok=True) # --- Debugging --- DEBUG_SCREENSHOT_DIR = os.path.join(SCRIPT_DIR, "debug_screenshots") -MAX_DEBUG_SCREENSHOTS = 5 +MAX_DEBUG_SCREENSHOTS = 8 os.makedirs(DEBUG_SCREENSHOT_DIR, exist_ok=True) # --- End Debugging --- @@ -94,6 +100,7 @@ DISMISS_BUTTON_IMG = os.path.join(TEMPLATE_DIR, "capitol", "dismiss.png") CONFIRM_BUTTON_IMG = os.path.join(TEMPLATE_DIR, "capitol", "confirm.png") CLOSE_BUTTON_IMG = os.path.join(TEMPLATE_DIR, "capitol", "close_button.png") BACK_ARROW_IMG = os.path.join(TEMPLATE_DIR, "capitol", "black_arrow_down.png") +REPLY_BUTTON_IMG = os.path.join(TEMPLATE_DIR, "reply_button.png") # Added for reply functionality # --- Operation Parameters (Consider moving to config.py) --- @@ -101,9 +108,14 @@ CHAT_INPUT_REGION = None # Example: (100, 800, 500, 50) CHAT_INPUT_CENTER_X = 400 CHAT_INPUT_CENTER_Y = 1280 SCREENSHOT_REGION = None -CONFIDENCE_THRESHOLD = 0.8 +CONFIDENCE_THRESHOLD = 0.9 # Increased threshold for corner matching STATE_CONFIDENCE_THRESHOLD = 0.7 -AVATAR_OFFSET_X = -55 # Adjusted as per user request (was -50) +AVATAR_OFFSET_X = -55 # Original offset, used for non-reply interactions like position removal +# AVATAR_OFFSET_X_RELOCATED = -50 # Replaced by specific reply offsets +AVATAR_OFFSET_X_REPLY = -45 # Horizontal offset for avatar click after re-location (for reply context) +AVATAR_OFFSET_Y_REPLY = 10 # Vertical offset for avatar click after re-location (for reply context) +BUBBLE_RELOCATE_CONFIDENCE = 0.8 # Reduced confidence for finding the bubble snapshot (was 0.9) +BUBBLE_RELOCATE_FALLBACK_CONFIDENCE = 0.6 # Lower confidence for fallback attempts BBOX_SIMILARITY_TOLERANCE = 10 RECENT_TEXT_HISTORY_MAXLEN = 5 # This state likely belongs in the coordinator @@ -237,15 +249,20 @@ class DetectionModule: if tl_coords in processed_tls: continue potential_br_box = None - min_dist_sq = float('inf') - # Find the closest valid BR corner (from any regular type) below and to the right + min_y_diff = float('inf') # Prioritize minimum Y difference + # Find the valid BR corner (from any regular type) with the closest Y-coordinate for br_box in all_regular_br_boxes: br_coords = (br_box[0], br_box[1]) # BR top-left - if br_coords[0] > tl_coords[0] + 20 and br_coords[1] > tl_coords[1] + 10: # Basic geometric check - dist_sq = (br_coords[0] - tl_coords[0])**2 + (br_coords[1] - tl_coords[1])**2 - if dist_sq < min_dist_sq: + # Basic geometric check: BR must be below and to the right of TL + if br_coords[0] > tl_coords[0] + 20 and br_coords[1] > tl_coords[1] + 10: + y_diff = abs(br_coords[1] - tl_coords[1]) # Calculate Y difference + if y_diff < min_y_diff: potential_br_box = br_box - min_dist_sq = dist_sq + min_y_diff = y_diff + # Optional: Add a secondary check for X distance if Y diff is the same? + # elif y_diff == min_y_diff: + # if potential_br_box is None or abs(br_coords[0] - tl_coords[0]) < abs(potential_br_box[0] - tl_coords[0]): + # potential_br_box = br_box if potential_br_box: # Calculate bbox using TL's top-left and BR's bottom-right @@ -266,15 +283,20 @@ class DetectionModule: if tl_coords in processed_tls: continue potential_br_box = None - min_dist_sq = float('inf') - # Find the closest valid BR corner below and to the right + min_y_diff = float('inf') # Prioritize minimum Y difference + # Find the valid BR corner with the closest Y-coordinate for br_box in bot_br_boxes: br_coords = (br_box[0], br_box[1]) # BR top-left - if br_coords[0] > tl_coords[0] + 20 and br_coords[1] > tl_coords[1] + 10: # Basic geometric check - dist_sq = (br_coords[0] - tl_coords[0])**2 + (br_coords[1] - tl_coords[1])**2 - if dist_sq < min_dist_sq: + # Basic geometric check: BR must be below and to the right of TL + if br_coords[0] > tl_coords[0] + 20 and br_coords[1] > tl_coords[1] + 10: + y_diff = abs(br_coords[1] - tl_coords[1]) # Calculate Y difference + if y_diff < min_y_diff: potential_br_box = br_box - min_dist_sq = dist_sq + min_y_diff = y_diff + # Optional: Add a secondary check for X distance if Y diff is the same? + # elif y_diff == min_y_diff: + # if potential_br_box is None or abs(br_coords[0] - tl_coords[0]) < abs(potential_br_box[0] - tl_coords[0]): + # potential_br_box = br_box if potential_br_box: # Calculate bbox using TL's top-left and BR's bottom-right @@ -295,16 +317,16 @@ class DetectionModule: """Look for keywords within a specified region. Returns center coordinates.""" if region[2] <= 0 or region[3] <= 0: return None # Invalid region width/height - # Try original lowercase with grayscale matching - locations_lower = self._find_template('keyword_wolf_lower', region=region, grayscale=True) + # Try original lowercase with color matching + locations_lower = self._find_template('keyword_wolf_lower', region=region, grayscale=False) # Changed grayscale to False if locations_lower: - print(f"Found keyword (lowercase, grayscale) in region {region}, position: {locations_lower[0]}") + print(f"Found keyword (lowercase, color) in region {region}, position: {locations_lower[0]}") # Updated log message return locations_lower[0] - # Try original uppercase with grayscale matching - locations_upper = self._find_template('keyword_wolf_upper', region=region, grayscale=True) + # Try original uppercase with color matching + locations_upper = self._find_template('keyword_wolf_upper', region=region, grayscale=False) # Changed grayscale to False if locations_upper: - print(f"Found keyword (uppercase, grayscale) in region {region}, position: {locations_upper[0]}") + print(f"Found keyword (uppercase, color) in region {region}, position: {locations_upper[0]}") # Updated log message return locations_upper[0] # Try type3 lowercase (white text, no grayscale) @@ -423,7 +445,7 @@ class InteractionModule: copy_coords = copy_item_locations[0] self.click_at(copy_coords[0], copy_coords[1]) print("Clicked 'Copy' menu item.") - time.sleep(0.2) + time.sleep(0.15) copied = True else: print("'Copy' menu item not found. Attempting Ctrl+C.") @@ -446,60 +468,108 @@ class InteractionModule: print("Error: Copy operation unsuccessful or clipboard content invalid.") return None - def retrieve_sender_name_interaction(self, avatar_coords: Tuple[int, int]) -> Optional[str]: + def retrieve_sender_name_interaction(self, + initial_avatar_coords: Tuple[int, int], + bubble_snapshot: Any, # PIL Image object + search_area: Optional[Tuple[int, int, int, int]]) -> Optional[str]: """ Perform the sequence of actions to copy sender name, *without* cleanup. + Includes retries with bubble re-location if the initial avatar click fails. Returns the name or None if failed. """ - print(f"Attempting interaction to get username from avatar {avatar_coords}...") + print(f"Attempting interaction to get username, initial avatar guess: {initial_avatar_coords}...") original_clipboard = self.get_clipboard() or "" self.set_clipboard("___MCP_CLEAR___") time.sleep(0.1) sender_name = None + profile_page_found = False + current_avatar_coords = initial_avatar_coords - try: - # 1. Click avatar - self.click_at(avatar_coords[0], avatar_coords[1]) - time.sleep(0.1) # Wait for profile card + for attempt in range(3): # Retry up to 3 times + print(f"Attempt #{attempt + 1} to click avatar and find profile page...") - # 2. Find and click profile option - profile_option_locations = self.detector._find_template('profile_option', confidence=0.7) - if not profile_option_locations: - print("Error: User details option not found on profile card.") - return None # Fail early if critical step missing - self.click_at(profile_option_locations[0][0], profile_option_locations[0][1]) - print("Clicked user details option.") - time.sleep(0.1) # Wait for user details window + # --- Re-locate bubble on retries --- + if attempt > 0: + print("Re-locating bubble before retry...") + if bubble_snapshot is None: + print("Error: Cannot retry re-location, bubble snapshot is missing.") + break # Cannot retry without snapshot - # 3. Find and click "Copy Name" button - copy_name_locations = self.detector._find_template('copy_name_button', confidence=0.7) - if not copy_name_locations: - print("Error: 'Copy Name' button not found in user details.") - return None # Fail early - self.click_at(copy_name_locations[0][0], copy_name_locations[0][1]) - print("Clicked 'Copy Name' button.") - time.sleep(0.1) + new_bubble_box_retry = pyautogui.locateOnScreen(bubble_snapshot, region=search_area, confidence=BUBBLE_RELOCATE_CONFIDENCE) + if new_bubble_box_retry: + new_tl_x_retry, new_tl_y_retry = new_bubble_box_retry.left, new_bubble_box_retry.top + print(f"Successfully re-located bubble snapshot for retry at: ({new_tl_x_retry}, {new_tl_y_retry})") + # Recalculate avatar coords for the retry + current_avatar_coords = (new_tl_x_retry + AVATAR_OFFSET_X_REPLY, new_tl_y_retry + AVATAR_OFFSET_Y_REPLY) + print(f"Recalculated avatar coordinates for retry: {current_avatar_coords}") + else: + print("Warning: Failed to re-locate bubble snapshot on retry. Aborting name retrieval.") + break # Stop retrying if bubble can't be found - # 4. Get name from clipboard - copied_name = self.get_clipboard() - if copied_name and copied_name != "___MCP_CLEAR___": - print(f"Successfully copied username: {copied_name}") - sender_name = copied_name.strip() + # --- Click Avatar --- + try: + self.click_at(current_avatar_coords[0], current_avatar_coords[1]) + time.sleep(0.15) # Slightly longer wait after click to allow UI to update + except Exception as click_err: + print(f"Error clicking avatar at {current_avatar_coords} on attempt {attempt + 1}: {click_err}") + time.sleep(0.3) # Wait a bit longer after a click error before retrying + continue # Go to next attempt + + # --- Check for Profile Page --- + if self.detector._find_template('profile_page', confidence=self.detector.state_confidence): + print("Profile page verified.") + profile_page_found = True + break # Success, exit retry loop else: - print("Error: Clipboard content invalid after clicking copy name.") - sender_name = None + print(f"Profile page not found after click attempt {attempt + 1}.") + # Optional: Press ESC once to close potential wrong menus before retrying? + # self.press_key('esc') + # time.sleep(0.1) + time.sleep(0.3) # Wait before next attempt - return sender_name + # --- If Profile Page was found, proceed --- + if profile_page_found: + try: + # 2. Find and click profile option + profile_option_locations = self.detector._find_template('profile_option', confidence=0.7) + if not profile_option_locations: + print("Error: User details option not found on profile card.") + return None # Fail early if critical step missing + self.click_at(profile_option_locations[0][0], profile_option_locations[0][1]) + print("Clicked user details option.") + time.sleep(0.1) # Wait for user details window - except Exception as e: - print(f"Error during username retrieval interaction: {e}") - import traceback - traceback.print_exc() - return None - finally: - # Restore clipboard regardless of success/failure - self.set_clipboard(original_clipboard) - # NO cleanup logic here - should be handled by coordinator + # 3. Find and click "Copy Name" button + copy_name_locations = self.detector._find_template('copy_name_button', confidence=0.7) + if not copy_name_locations: + print("Error: 'Copy Name' button not found in user details.") + return None # Fail early + self.click_at(copy_name_locations[0][0], copy_name_locations[0][1]) + print("Clicked 'Copy Name' button.") + time.sleep(0.1) + + # 4. Get name from clipboard + copied_name = self.get_clipboard() + if copied_name and copied_name != "___MCP_CLEAR___": + print(f"Successfully copied username: {copied_name}") + sender_name = copied_name.strip() + else: + print("Error: Clipboard content invalid after clicking copy name.") + sender_name = None + + except Exception as e: + print(f"Error during username retrieval interaction (after profile page found): {e}") + import traceback + traceback.print_exc() + sender_name = None # Ensure None is returned on error + else: + print("Failed to verify profile page after multiple attempts.") + sender_name = None + + # --- Final Cleanup & Return --- + self.set_clipboard(original_clipboard) # Restore clipboard + # NO cleanup logic (like ESC) here - should be handled by coordinator after this function returns + return sender_name def send_chat_message(self, reply_text: str) -> bool: """Paste text into chat input and send it.""" @@ -558,20 +628,183 @@ class InteractionModule: # ============================================================================== # Position Removal Logic # ============================================================================== -def remove_user_position(detector: DetectionModule, interactor: InteractionModule, trigger_bubble_region: Tuple[int, int, int, int]) -> bool: +def remove_user_position(detector: DetectionModule, + interactor: InteractionModule, + trigger_bubble_region: Tuple[int, int, int, int], # Original region, might be outdated + bubble_snapshot: Any, # PIL Image object for re-location + search_area: Optional[Tuple[int, int, int, int]]) -> bool: # Area to search snapshot in """ Performs the sequence of UI actions to remove a user's position based on the triggering chat bubble. + Includes re-location using the provided snapshot before proceeding. Returns True if successful, False otherwise. """ - print(f"\n--- Starting Position Removal Process (Trigger Bubble Region: {trigger_bubble_region}) ---") - bubble_x, bubble_y, bubble_w, bubble_h = trigger_bubble_region # This is the BBOX, y is top + print(f"\n--- Starting Position Removal Process (Initial Trigger Region: {trigger_bubble_region}) ---") - # 1. Find the closest position icon above the bubble - search_region_y_end = bubble_y - search_region_y_start = max(0, bubble_y - 55) # Search 55 pixels above - search_region_x_start = max(0, bubble_x - 100) # Search wider horizontally + # --- Re-locate Bubble First --- + print("Attempting to re-locate bubble using snapshot before removing position...") + # If bubble_snapshot is None, try to create one from the trigger_bubble_region + if bubble_snapshot is None: + print("Bubble snapshot is missing. Attempting to create a new snapshot from the trigger region...") + try: + if trigger_bubble_region and len(trigger_bubble_region) == 4: + bubble_region_tuple = (int(trigger_bubble_region[0]), int(trigger_bubble_region[1]), + int(trigger_bubble_region[2]), int(trigger_bubble_region[3])) + + if bubble_region_tuple[2] <= 0 or bubble_region_tuple[3] <= 0: + print(f"Warning: Invalid bubble region {bubble_region_tuple} for taking new snapshot.") + return False + + print(f"Taking new screenshot of region: {bubble_region_tuple}") + bubble_snapshot = pyautogui.screenshot(region=bubble_region_tuple) + if bubble_snapshot: + print("Successfully created new bubble snapshot.") + else: + print("Failed to create new bubble snapshot.") + return False + else: + print("Invalid trigger_bubble_region format, cannot create snapshot.") + return False + except Exception as e: + print(f"Error creating new bubble snapshot: {e}") + return False + if search_area is None: + print("Warning: Search area for snapshot is missing. Creating a default search area.") + # Create a default search area centered around the original trigger region + # This creates a search area that's twice the size of the original bubble + if trigger_bubble_region and len(trigger_bubble_region) == 4: + x, y, width, height = trigger_bubble_region + # Expand by 100% in each direction + search_x = max(0, x - width//2) + search_y = max(0, y - height//2) + search_width = width * 2 + search_height = height * 2 + search_area = (search_x, search_y, search_width, search_height) + print(f"Created default search area based on bubble region: {search_area}") + else: + # If no valid trigger_bubble_region, default to full screen search + search_area = None # Set search_area to None for full screen search + print(f"Using full screen search as fallback.") + + # Try to locate the bubble with decreasing confidence levels if needed + new_bubble_box = None + + # Determine the region to search: use provided search_area or None for full screen + region_to_search = search_area + print(f"Attempting bubble location. Search Region: {'Full Screen' if region_to_search is None else region_to_search}") + + # First attempt with standard confidence + print(f"First attempt with confidence {BUBBLE_RELOCATE_CONFIDENCE}...") + try: + new_bubble_box = pyautogui.locateOnScreen(bubble_snapshot, + region=region_to_search, + confidence=BUBBLE_RELOCATE_CONFIDENCE) + except Exception as e: + print(f"Exception during initial bubble location attempt: {e}") + + # Second attempt with fallback confidence if first failed + if not new_bubble_box: + print(f"First attempt failed. Trying with lower confidence {BUBBLE_RELOCATE_FALLBACK_CONFIDENCE}...") + try: + # Try with a lower confidence threshold + new_bubble_box = pyautogui.locateOnScreen(bubble_snapshot, + region=region_to_search, + confidence=BUBBLE_RELOCATE_FALLBACK_CONFIDENCE) + except Exception as e: + print(f"Exception during fallback bubble location attempt: {e}") + + # Third attempt with even lower confidence as last resort + if not new_bubble_box: + print("Second attempt failed. Trying with even lower confidence 0.4...") + try: + # Last resort with very low confidence + new_bubble_box = pyautogui.locateOnScreen(bubble_snapshot, + region=region_to_search, + confidence=0.4) + except Exception as e: + print(f"Exception during last resort bubble location attempt: {e}") + + # If we still can't find the bubble using snapshot, try re-detecting bubbles + if not new_bubble_box: + print("Snapshot location failed. Attempting secondary fallback: Re-detecting bubbles...") + try: + # Helper function to calculate distance - define it here or move globally if used elsewhere + def calculate_distance(p1, p2): + return ((p1[0] - p2[0])**2 + (p1[1] - p2[1])**2)**0.5 + + current_bubbles_info = detector.find_dialogue_bubbles() + non_bot_bubbles = [b for b in current_bubbles_info if not b.get('is_bot')] + + if non_bot_bubbles and trigger_bubble_region and len(trigger_bubble_region) == 4: + original_tl = (trigger_bubble_region[0], trigger_bubble_region[1]) + closest_bubble = None + min_distance = float('inf') + MAX_ALLOWED_DISTANCE = 150 # Example threshold: Don't match bubbles too far away + + for bubble_info in non_bot_bubbles: + bubble_bbox = bubble_info.get('bbox') + if bubble_bbox: + current_tl = (bubble_bbox[0], bubble_bbox[1]) + distance = calculate_distance(original_tl, current_tl) + if distance < min_distance: + min_distance = distance + closest_bubble = bubble_info + + if closest_bubble and min_distance <= MAX_ALLOWED_DISTANCE: + print(f"Found a close bubble via re-detection (Distance: {min_distance:.2f}). Using its bbox.") + bbox = closest_bubble['bbox'] + # Create a dummy box using PyAutoGUI's Box class or a similar structure + from collections import namedtuple + Box = namedtuple('Box', ['left', 'top', 'width', 'height']) + new_bubble_box = Box(left=bbox[0], top=bbox[1], width=bbox[2]-bbox[0], height=bbox[3]-bbox[1]) + print(f"Created fallback bubble box from re-detected bubble: {new_bubble_box}") + else: + print(f"Re-detection fallback failed: No close bubble found (Min distance: {min_distance:.2f} > Threshold: {MAX_ALLOWED_DISTANCE}).") + else: + print("Re-detection fallback failed: No non-bot bubbles found or invalid trigger region.") + + except Exception as redetect_err: + print(f"Error during bubble re-detection fallback: {redetect_err}") + + + # Final fallback: If STILL no bubble box, use original trigger region + if not new_bubble_box: + print("All location attempts failed (snapshot & re-detection). Using original trigger region as last resort.") + if trigger_bubble_region and len(trigger_bubble_region) == 4: + # Create a mock bubble_box from the original region + x, y, width, height = trigger_bubble_region + print(f"Using original trigger region as fallback: {trigger_bubble_region}") + + # Create a dummy box using PyAutoGUI's Box class or a similar structure + from collections import namedtuple + Box = namedtuple('Box', ['left', 'top', 'width', 'height']) + new_bubble_box = Box(left=x, top=y, width=width, height=height) + print("Created fallback bubble box from original coordinates.") + else: + print("Error: No original trigger region available for fallback. Aborting position removal.") + return False + + # Use the NEW coordinates for all subsequent calculations + bubble_x, bubble_y = new_bubble_box.left, new_bubble_box.top + bubble_w, bubble_h = new_bubble_box.width, new_bubble_box.height + print(f"Successfully re-located bubble at: ({bubble_x}, {bubble_y}, {bubble_w}, {bubble_h})") + # --- End Re-location --- + + + # 1. Find the closest position icon above the *re-located* bubble + search_height_pixels = 50 # Search exactly 50 pixels above as requested + search_region_y_end = bubble_y # Use re-located Y + search_region_y_start = max(0, bubble_y - search_height_pixels) # Search 50 pixels above + search_region_x_start = max(0, bubble_x - 100) # Keep horizontal search wide search_region_x_end = bubble_x + bubble_w + 100 - search_region = (search_region_x_start, search_region_y_start, search_region_x_end - search_region_x_start, search_region_y_end - search_region_y_start) + search_region_width = search_region_x_end - search_region_x_start + search_region_height = search_region_y_end - search_region_y_start + + # Ensure region has positive width and height + if search_region_width <= 0 or search_region_height <= 0: + print(f"Error: Invalid search region calculated for position icons: width={search_region_width}, height={search_region_height}") + return False + + search_region = (search_region_x_start, search_region_y_start, search_region_width, search_region_height) print(f"Searching for position icons in region: {search_region}") position_templates = { @@ -579,9 +812,10 @@ def remove_user_position(detector: DetectionModule, interactor: InteractionModul 'SECURITY': POS_SEC_IMG, 'STRATEGY': POS_STR_IMG } found_positions = [] + position_icon_confidence = 0.8 # Slightly increased confidence (was 0.75) for name, path in position_templates.items(): # Use unique keys for detector templates - locations = detector._find_template(name.lower() + '_pos', confidence=0.75, region=search_region) + locations = detector._find_template(name.lower() + '_pos', confidence=position_icon_confidence, region=search_region) for loc in locations: found_positions.append({'name': name, 'coords': loc, 'path': path}) @@ -598,12 +832,12 @@ def remove_user_position(detector: DetectionModule, interactor: InteractionModul target_position_name = closest_position['name'] print(f"Found pending position: |{target_position_name}| at {closest_position['coords']}") - # 2. Click user avatar (offset from bubble top-left) - # IMPORTANT: Use the bubble_y (top of the bbox) for the click Y coordinate. - # The AVATAR_OFFSET_X handles the horizontal positioning relative to the bubble's left edge (bubble_x). - avatar_click_x = bubble_x + AVATAR_OFFSET_X # Use constant offset - avatar_click_y = bubble_y # Use the top Y coordinate of the bubble's bounding box - print(f"Clicking avatar at estimated position: ({avatar_click_x}, {avatar_click_y}) based on bubble top-left ({bubble_x}, {bubble_y})") + # 2. Click user avatar (offset from *re-located* bubble top-left) + # --- MODIFIED: Use specific offsets for remove_position command as requested --- + avatar_click_x = bubble_x + AVATAR_OFFSET_X_REPLY # Use -45 offset + avatar_click_y = bubble_y + AVATAR_OFFSET_Y_REPLY # Use +10 offset + print(f"Clicking avatar for position removal at calculated position: ({avatar_click_x}, {avatar_click_y}) using offsets ({AVATAR_OFFSET_X_REPLY}, {AVATAR_OFFSET_Y_REPLY}) from re-located bubble top-left ({bubble_x}, {bubble_y})") + # --- END MODIFICATION --- interactor.click_at(avatar_click_x, avatar_click_y) time.sleep(0.15) # Wait for profile page @@ -693,7 +927,7 @@ def remove_user_position(detector: DetectionModule, interactor: InteractionModul if close_locs: interactor.click_at(close_locs[0][0], close_locs[0][1]) print("Clicked Close button (returning to Capitol).") - time.sleep(0.15) + time.sleep(0.1) else: print("Warning: Close button not found after confirm, attempting back arrow anyway.") @@ -702,7 +936,7 @@ def remove_user_position(detector: DetectionModule, interactor: InteractionModul if back_arrow_locs: interactor.click_at(back_arrow_locs[0][0], back_arrow_locs[0][1]) print("Clicked Back Arrow (returning to Profile).") - time.sleep(0.15) + time.sleep(0.1) else: print("Warning: Back arrow not found on Capitol page, attempting ESC cleanup.") @@ -800,7 +1034,8 @@ def run_ui_monitoring_loop(trigger_queue: queue.Queue, command_queue: queue.Queu 'page_dev': PAGE_DEV_IMG, 'page_int': PAGE_INT_IMG, 'page_sci': PAGE_SCI_IMG, 'page_sec': PAGE_SEC_IMG, 'page_str': PAGE_STR_IMG, 'dismiss_button': DISMISS_BUTTON_IMG, 'confirm_button': CONFIRM_BUTTON_IMG, - 'close_button': CLOSE_BUTTON_IMG, 'back_arrow': BACK_ARROW_IMG + 'close_button': CLOSE_BUTTON_IMG, 'back_arrow': BACK_ARROW_IMG, + 'reply_button': REPLY_BUTTON_IMG # Added reply button template key } # Use default confidence/region settings from constants detector = DetectionModule(templates, confidence=CONFIDENCE_THRESHOLD, state_confidence=STATE_CONFIDENCE_THRESHOLD, region=SCREENSHOT_REGION) @@ -813,7 +1048,70 @@ def run_ui_monitoring_loop(trigger_queue: queue.Queue, command_queue: queue.Queu screenshot_counter = 0 # Initialize counter for debug screenshots while True: - # --- Check for Main Screen Navigation First --- + # --- Process ALL Pending Commands First --- + commands_processed_this_cycle = False + try: + while True: # Loop to drain the queue + command_data = command_queue.get_nowait() # Check for commands without blocking + commands_processed_this_cycle = True + action = command_data.get('action') + + if action == 'send_reply': + text_to_send = command_data.get('text') + if not text_to_send: + print("UI Thread: Received send_reply command with no text.") + continue # Process next command in queue + print(f"UI Thread: Processing command to send reply: '{text_to_send[:50]}...'") + interactor.send_chat_message(text_to_send) + + elif action == 'remove_position': + # region = command_data.get('trigger_bubble_region') # This is the old region, keep for reference? + snapshot = command_data.get('bubble_snapshot') + area = command_data.get('search_area') + # Pass all necessary data to the function, including the original region if needed for context + # but the function should primarily use the snapshot for re-location. + original_region = command_data.get('trigger_bubble_region') + if snapshot: # Check for snapshot presence + print(f"UI Thread: Processing command to remove position (Snapshot provided: {'Yes' if snapshot else 'No'})") + success = remove_user_position(detector, interactor, original_region, snapshot, area) + print(f"UI Thread: Position removal attempt finished. Success: {success}") + else: + print("UI Thread: Received remove_position command without necessary snapshot data.") + + + elif action == 'pause': + if not monitoring_paused_flag[0]: # Avoid redundant prints if already paused + print("UI Thread: Processing pause command. Pausing monitoring.") + monitoring_paused_flag[0] = True + # No continue needed here, let it finish draining queue + + elif action == 'resume': + if monitoring_paused_flag[0]: # Avoid redundant prints if already running + print("UI Thread: Processing resume command. Resuming monitoring.") + monitoring_paused_flag[0] = False + # No continue needed here + + else: + print(f"UI Thread: Received unknown command: {action}") + + except queue.Empty: + # No more commands in the queue for this cycle + if commands_processed_this_cycle: + print("UI Thread: Finished processing commands for this cycle.") + pass + except Exception as cmd_err: + print(f"UI Thread: Error processing command queue: {cmd_err}") + # Consider if pausing is needed on error, maybe not + + # --- Now, Check Pause State --- + if monitoring_paused_flag[0]: + # If paused, sleep and skip UI monitoring part + time.sleep(0.1) # Sleep briefly while paused + continue # Go back to check commands again + + # --- If not paused, proceed with UI Monitoring --- + + # --- Check for Main Screen Navigation --- try: base_locs = detector._find_template('base_screen', confidence=0.8) map_locs = detector._find_template('world_map_screen', confidence=0.8) @@ -823,7 +1121,7 @@ def run_ui_monitoring_loop(trigger_queue: queue.Queue, command_queue: queue.Queu # IMPORTANT: Ensure these coordinates are correct for the target window/resolution target_x, target_y = 600, 1300 interactor.click_at(target_x, target_y) - time.sleep(0.2) # Short delay after click + time.sleep(0.1) # Short delay after click print("UI Thread: Clicked to return to chat. Re-checking screen state...") continue # Skip the rest of the loop and re-evaluate except Exception as nav_err: @@ -831,34 +1129,52 @@ def run_ui_monitoring_loop(trigger_queue: queue.Queue, command_queue: queue.Queu # Decide if you want to continue or pause after error # --- Process Commands Second (Non-blocking) --- - try: - command_data = command_queue.get_nowait() # Check for commands without blocking - action = command_data.get('action') - if action == 'send_reply': - text_to_send = command_data.get('text') - if text_to_send: - print(f"UI Thread: Received command to send reply: '{text_to_send[:50]}...'") - interactor.send_chat_message(text_to_send) - else: - print("UI Thread: Received send_reply command with no text.") - elif action == 'remove_position': # <--- Handle new command - region = command_data.get('trigger_bubble_region') - if region: - print(f"UI Thread: Received command to remove position triggered by bubble region: {region}") - # Call the new UI function - success = remove_user_position(detector, interactor, region) # Call synchronous function - print(f"UI Thread: Position removal attempt finished. Success: {success}") - # Note: No need to send result back unless main thread needs confirmation - else: - print("UI Thread: Received remove_position command without trigger_bubble_region.") - else: - print(f"UI Thread: Received unknown command: {action}") - except queue.Empty: - pass # No command waiting, continue with monitoring - except Exception as cmd_err: - print(f"UI Thread: Error processing command queue: {cmd_err}") + # This block seems redundant now as commands are processed at the start of the loop. + # Keeping it commented out for now, can be removed later if confirmed unnecessary. + # try: + # command_data = command_queue.get_nowait() # Check for commands without blocking + # action = command_data.get('action') + # if action == 'send_reply': + # text_to_send = command_data.get('text') + # # reply_context_activated = command_data.get('reply_context_activated', False) # Check if reply context was set + # + # if not text_to_send: + # print("UI Thread: Received send_reply command with no text.") + # continue # Skip if no text + # + # print(f"UI Thread: Received command to send reply: '{text_to_send[:50]}...'") + # # The reply context (clicking bubble + reply button) is now handled *before* putting into queue. + # # So, we just need to send the message directly here. + # # The input field should already be focused and potentially have @Username prefix if reply context was activated. + # interactor.send_chat_message(text_to_send) + # + # elif action == 'remove_position': # <--- Handle new command + # region = command_data.get('trigger_bubble_region') + # if region: + # print(f"UI Thread: Received command to remove position triggered by bubble region: {region}") + # # Call the new UI function + # success = remove_user_position(detector, interactor, region) # Call synchronous function + # print(f"UI Thread: Position removal attempt finished. Success: {success}") + # # Note: No need to send result back unless main thread needs confirmation + # else: + # print("UI Thread: Received remove_position command without trigger_bubble_region.") + # elif action == 'pause': # <--- Handle pause command + # print("UI Thread: Received pause command. Pausing monitoring.") + # monitoring_paused_flag[0] = True + # continue # Immediately pause after receiving command + # elif action == 'resume': # <--- Handle resume command (might be redundant if checked above, but safe) + # print("UI Thread: Received resume command. Resuming monitoring.") + # monitoring_paused_flag[0] = False + # else: + # print(f"UI Thread: Received unknown command: {action}") + # except queue.Empty: + # pass # No command waiting, continue with monitoring + # except Exception as cmd_err: + # print(f"UI Thread: Error processing command queue: {cmd_err}") + # # This block is now part of the command processing loop above + # pass - # --- Verify Chat Room State Before Bubble Detection --- + # --- Verify Chat Room State Before Bubble Detection (Only if NOT paused) --- try: # Use a slightly lower confidence maybe, or state_confidence chat_room_locs = detector._find_template('chat_room', confidence=detector.state_confidence) @@ -905,31 +1221,41 @@ def run_ui_monitoring_loop(trigger_queue: queue.Queue, command_queue: queue.Queu if keyword_coords: print(f"\n!!! Keyword detected in bubble {target_bbox} !!!") - # --- Debug Screenshot Logic --- + # --- Variables needed later --- + bubble_snapshot = None # Initialize snapshot variable + search_area = SCREENSHOT_REGION # Define search area early + if search_area is None: + print("Warning: SCREENSHOT_REGION not defined, searching full screen for bubble snapshot.") + # Consider adding a default chat region if SCREENSHOT_REGION is often None + + # --- Take Snapshot for Re-location (and potentially save it) --- try: - screenshot_index = (screenshot_counter % MAX_DEBUG_SCREENSHOTS) + 1 - screenshot_filename = f"debug_bubble_{screenshot_index}.png" - screenshot_path = os.path.join(DEBUG_SCREENSHOT_DIR, screenshot_filename) - # --- Enhanced Logging --- - print(f"Attempting to save debug screenshot to: {screenshot_path}") - print(f"Screenshot Region: {bubble_region}") - # Check if region is valid before attempting screenshot - if bubble_region and len(bubble_region) == 4 and bubble_region[2] > 0 and bubble_region[3] > 0: - # Convert numpy types to standard Python int for pyautogui - region_int = (int(bubble_region[0]), int(bubble_region[1]), int(bubble_region[2]), int(bubble_region[3])) - pyautogui.screenshot(region=region_int, imageFilename=screenshot_path) - print(f"Successfully saved debug screenshot: {screenshot_path}") + bubble_region_tuple = (int(bubble_region[0]), int(bubble_region[1]), int(bubble_region[2]), int(bubble_region[3])) + if bubble_region_tuple[2] <= 0 or bubble_region_tuple[3] <= 0: + print(f"Warning: Invalid bubble region {bubble_region_tuple} for snapshot. Skipping trigger.") + continue + bubble_snapshot = pyautogui.screenshot(region=bubble_region_tuple) + if bubble_snapshot is None: + print("Warning: Failed to capture bubble snapshot. Skipping trigger.") + continue + + # --- Save Snapshot for Debugging (Replaces old debug screenshot logic) --- + try: + screenshot_index = (screenshot_counter % MAX_DEBUG_SCREENSHOTS) + 1 + # Use a more descriptive filename + screenshot_filename = f"debug_relocation_snapshot_{screenshot_index}.png" + screenshot_path = os.path.join(DEBUG_SCREENSHOT_DIR, screenshot_filename) + print(f"Attempting to save bubble snapshot used for re-location to: {screenshot_path}") + bubble_snapshot.save(screenshot_path) # Save the PIL image object + print(f"Successfully saved bubble snapshot: {screenshot_path}") screenshot_counter += 1 - else: - print(f"Error: Invalid screenshot region {bubble_region}. Skipping screenshot.") - # --- End Enhanced Logging --- - except Exception as ss_err: - # --- Enhanced Error Logging --- - print(f"Error taking debug screenshot for region {bubble_region} to path {screenshot_path}: {repr(ss_err)}") - import traceback - traceback.print_exc() # Print full traceback for detailed debugging - # --- End Enhanced Error Logging --- - # --- End Debug Screenshot Logic --- + except Exception as save_err: + print(f"Error saving bubble snapshot to {screenshot_path}: {repr(save_err)}") + # Continue even if saving fails + + except Exception as snapshot_err: + print(f"Error taking initial bubble snapshot: {repr(snapshot_err)}") + continue # Skip trigger if snapshot fails # 4. Interact: Get Bubble Text bubble_text = interactor.copy_text_at(keyword_coords) @@ -949,12 +1275,100 @@ def run_ui_monitoring_loop(trigger_queue: queue.Queue, command_queue: queue.Queu last_processed_bubble_info = target_bubble_info recent_texts.append(bubble_text) - # 5. Interact: Get Sender Name - # *** Use the precise TL coordinates for avatar calculation *** - avatar_coords = detector.calculate_avatar_coords(target_bubble_info['tl_coords']) - sender_name = interactor.retrieve_sender_name_interaction(avatar_coords) + # 5. Interact: Get Sender Name (with Bubble Re-location) + sender_name = None + try: + # --- Bubble Re-location Logic with Fallback Mechanism --- + print("Attempting to re-locate bubble before getting sender name...") + if bubble_snapshot is None: # Should not happen if we reached here, but check anyway + print("Error: Bubble snapshot missing for re-location. Skipping.") + continue + + # First attempt with standard confidence + print(f"First attempt with confidence {BUBBLE_RELOCATE_CONFIDENCE}...") + new_bubble_box = None + try: + new_bubble_box = pyautogui.locateOnScreen(bubble_snapshot, + region=search_area, + confidence=BUBBLE_RELOCATE_CONFIDENCE) + except Exception as e: + print(f"Exception during initial bubble location attempt: {e}") + + # Second attempt with fallback confidence if first failed + if not new_bubble_box: + print(f"First attempt failed. Trying with lower confidence {BUBBLE_RELOCATE_FALLBACK_CONFIDENCE}...") + try: + # Try with a lower confidence threshold + new_bubble_box = pyautogui.locateOnScreen(bubble_snapshot, + region=search_area, + confidence=BUBBLE_RELOCATE_FALLBACK_CONFIDENCE) + except Exception as e: + print(f"Exception during fallback bubble location attempt: {e}") + + # Third attempt with even lower confidence as last resort + if not new_bubble_box: + print("Second attempt failed. Trying with even lower confidence 0.4...") + try: + # Last resort with very low confidence + new_bubble_box = pyautogui.locateOnScreen(bubble_snapshot, + region=search_area, + confidence=0.4) + except Exception as e: + print(f"Exception during last resort bubble location attempt: {e}") + + if new_bubble_box: + new_tl_x, new_tl_y = new_bubble_box.left, new_bubble_box.top + print(f"Successfully re-located bubble snapshot at: ({new_tl_x}, {new_tl_y})") + # Calculate avatar coords based on the *new* top-left and the *reply* offsets + new_avatar_coords = (new_tl_x + AVATAR_OFFSET_X_REPLY, new_tl_y + AVATAR_OFFSET_Y_REPLY) + print(f"Calculated new avatar coordinates for reply context: {new_avatar_coords}") + # Proceed to get sender name using the new coordinates, passing snapshot info for retries + sender_name = interactor.retrieve_sender_name_interaction( + initial_avatar_coords=new_avatar_coords, + bubble_snapshot=bubble_snapshot, + search_area=search_area + ) + else: + print("Warning: Failed to re-locate bubble snapshot on screen after multiple attempts with decreasing confidence thresholds.") + print("Trying direct approach with original bubble coordinates...") + + # Fallback to original coordinates based on the target_bubble_info + original_tl_coords = target_bubble_info.get('tl_coords') + if original_tl_coords: + fallback_avatar_coords = (original_tl_coords[0] + AVATAR_OFFSET_X_REPLY, + original_tl_coords[1] + AVATAR_OFFSET_Y_REPLY) + print(f"Using fallback avatar coordinates from original detection: {fallback_avatar_coords}") + + # Try with direct coordinates + sender_name = interactor.retrieve_sender_name_interaction( + initial_avatar_coords=fallback_avatar_coords, + bubble_snapshot=bubble_snapshot, + search_area=search_area + ) + + if not sender_name: + print("Direct approach failed. Skipping this trigger.") + last_processed_bubble_info = target_bubble_info # Mark as processed + perform_state_cleanup(detector, interactor) # Cleanup + continue + else: + print("No original coordinates available. Skipping sender name retrieval.") + # No need to continue if we can't find the bubble again + last_processed_bubble_info = target_bubble_info # Mark as processed to avoid re-triggering immediately + perform_state_cleanup(detector, interactor) # Attempt cleanup as state might be inconsistent + continue + # --- End Bubble Re-location Logic --- + + except Exception as reloc_err: + print(f"Error during bubble re-location or subsequent interaction: {reloc_err}") + import traceback + traceback.print_exc() + # Attempt cleanup after error during this critical phase + perform_state_cleanup(detector, interactor) + continue # Skip further processing for this trigger # 6. Perform Cleanup (Crucial after potentially leaving chat screen) + # Moved the check for sender_name *after* potential re-location attempt cleanup_successful = perform_state_cleanup(detector, interactor) if not cleanup_successful: print("Error: Failed to return to chat screen after getting name. Aborting trigger.") @@ -964,21 +1378,71 @@ def run_ui_monitoring_loop(trigger_queue: queue.Queue, command_queue: queue.Queu print("Error: Could not get sender name, aborting processing.") continue # Already cleaned up, just skip + # --- Attempt to activate reply context BEFORE putting in queue --- + reply_context_activated = False + try: + print("Attempting to activate reply context...") + # Re-locate the bubble *again* to click its center for reply + if bubble_snapshot is None: + print("Warning: Bubble snapshot missing for reply context activation. Skipping.") + final_bubble_box_for_reply = None # Ensure it's None + else: + print(f"Attempting final re-location for reply context using search_area: {search_area}") + final_bubble_box_for_reply = pyautogui.locateOnScreen(bubble_snapshot, region=search_area, confidence=BUBBLE_RELOCATE_CONFIDENCE) + + if final_bubble_box_for_reply: + print(f"Final re-location successful at: {final_bubble_box_for_reply}") + bubble_x_reply, bubble_y_reply = final_bubble_box_for_reply.left, final_bubble_box_for_reply.top + bubble_w_reply, bubble_h_reply = final_bubble_box_for_reply.width, final_bubble_box_for_reply.height + center_x_reply = bubble_x_reply + bubble_w_reply // 2 + center_y_reply = bubble_y_reply + bubble_h_reply // 2 + + print(f"Clicking bubble center for reply at ({center_x_reply}, {center_y_reply})") + interactor.click_at(center_x_reply, center_y_reply) + time.sleep(0.15) # Increased wait time for menu/reply button to appear + + print("Searching for reply button...") + reply_button_locs = detector._find_template('reply_button', confidence=0.8) + if reply_button_locs: + reply_coords = reply_button_locs[0] + print(f"Found reply button at {reply_coords}. Clicking...") + interactor.click_at(reply_coords[0], reply_coords[1]) + time.sleep(0.07) # Wait after click + reply_context_activated = True + print("Reply context activated.") + else: + print(">>> Reply button template ('reply_button') not found after clicking bubble center. <<<") + # Optional: Press ESC to close menu if reply button wasn't found? + # print("Attempting to press ESC to close potential menu.") + # interactor.press_key('esc') + # time.sleep(0.1) + else: + # This log message was already present but is important + print("Warning: Failed to re-locate bubble for activating reply context.") + + except Exception as reply_context_err: + print(f"!!! Error during reply context activation: {reply_context_err} !!!") + # Ensure reply_context_activated remains False + # 7. Send Trigger Info to Main Thread/Async Loop print("\n>>> Putting trigger info in Queue <<<") print(f" Sender: {sender_name}") print(f" Content: {bubble_text[:100]}...") print(f" Bubble Region: {bubble_region}") # Include region derived from bbox + print(f" Reply Context Activated: {reply_context_activated}") # Include the flag try: - # Include bubble_region in the data sent + # Include bubble_region and reply_context_activated flag data_to_send = { 'sender': sender_name, 'text': bubble_text, - 'bubble_region': bubble_region # Use bbox-derived region for general use + 'bubble_region': bubble_region, # Use bbox-derived region for general use + 'reply_context_activated': reply_context_activated, # Send the flag + 'bubble_snapshot': bubble_snapshot, # <-- Add snapshot + 'search_area': search_area # <-- Add search area used for snapshot # 'tl_coords': target_bubble_info['tl_coords'] # Optionally send if needed elsewhere } trigger_queue.put(data_to_send) # Put in the queue for main loop - print("Trigger info (with region) placed in Queue.") + print("Trigger info (with region, reply flag, snapshot, search_area) placed in Queue.") except Exception as q_err: print(f"Error putting data in Queue: {q_err}") From e25e3177c2d209672945aa4794bf43fdbe4610f6 Mon Sep 17 00:00:00 2001 From: z060142 Date: Sat, 19 Apr 2025 13:00:22 +0800 Subject: [PATCH 3/7] Improve Element detection stability --- ClaudeCode.md | 19 ++++ config.py | 1 + llm_interaction.py | 2 +- persona.json | 9 +- requirements.txt | 3 +- templates/corner_br.png | Bin 1673 -> 462 bytes templates/corner_br_type4.png | Bin 0 -> 2110 bytes templates/corner_tl.png | Bin 250 -> 196 bytes templates/corner_tl_type4.png | Bin 0 -> 1924 bytes templates/keyword_wolf_lower.png | Bin 2540 -> 1787 bytes templates/keyword_wolf_lower_type2.png | Bin 0 -> 965 bytes templates/keyword_wolf_lower_type4.png | Bin 0 -> 1606 bytes templates/keyword_wolf_upper_type2.png | Bin 0 -> 2029 bytes templates/keyword_wolf_upper_type4.png | Bin 0 -> 867 bytes ui_interaction.py | 59 +++++++++--- window-monitor-script.py | 121 +++++++++++++++++++++++++ 16 files changed, 197 insertions(+), 17 deletions(-) create mode 100644 templates/corner_br_type4.png create mode 100644 templates/corner_tl_type4.png create mode 100644 templates/keyword_wolf_lower_type2.png create mode 100644 templates/keyword_wolf_lower_type4.png create mode 100644 templates/keyword_wolf_upper_type2.png create mode 100644 templates/keyword_wolf_upper_type4.png create mode 100644 window-monitor-script.py diff --git a/ClaudeCode.md b/ClaudeCode.md index 85484a8..bcaac8e 100644 --- a/ClaudeCode.md +++ b/ClaudeCode.md @@ -52,6 +52,10 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機 7. **視窗設定工具 (window-setup-script.py)** - 輔助工具,用於設置遊戲視窗的位置和大小 - 方便開發階段截取 UI 元素樣本 +8. **視窗監視工具 (window-monitor-script.py)** + - (新增) 強化腳本,用於持續監視遊戲視窗 + - 確保目標視窗維持在最上層 (Always on Top) + - 自動將視窗移回指定的位置 ### 資料流程 @@ -283,6 +287,21 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機 - 如果重新定位成功,則後續所有基於氣泡位置的計算(包括尋找職位圖標的搜索區域 `search_region` 和點擊頭像的座標 `avatar_click_x`, `avatar_click_y`)都將使用這個**新找到的**氣泡座標。 - **效果**:確保 `remove_position` 操作基於氣泡的最新位置執行,提高了在動態滾動的聊天界面中的可靠性。 +### 修正 Type3 關鍵字辨識並新增 Type4 支援 (2025-04-19) + +- **目的**:修復先前版本中 `type3` 關鍵字辨識的錯誤,並擴充系統以支援新的 `type4` 聊天泡泡外觀和對應的關鍵字樣式。 +- **`ui_interaction.py`**: + - **修正 `find_keyword_in_region`**:移除了錯誤使用 `type2` 模板鍵來尋找 `type3` 關鍵字的重複程式碼,確保 `type3` 關鍵字使用正確的模板 (`keyword_wolf_lower_type3`, `keyword_wolf_upper_type3`)。 + - **新增 `type4` 泡泡支援**: + - 在檔案開頭定義了 `type4` 角落模板的路徑常數 (`CORNER_TL_TYPE4_IMG`, `CORNER_BR_TYPE4_IMG`)。 + - 在 `find_dialogue_bubbles` 函數中,將 `type4` 的模板鍵 (`corner_tl_type4`, `corner_br_type4`) 加入 `regular_tl_keys` 和 `regular_br_keys` 列表。 + - 在 `run_ui_monitoring_loop` 的 `templates` 字典中加入了對應的鍵值對。 + - **新增 `type4` 關鍵字支援**: + - 在檔案開頭定義了 `type4` 關鍵字模板的路徑常數 (`KEYWORD_wolf_LOWER_TYPE4_IMG`, `KEYWORD_Wolf_UPPER_TYPE4_IMG`)。 + - 在 `find_keyword_in_region` 函數中,加入了尋找 `type4` 關鍵字模板 (`keyword_wolf_lower_type4`, `keyword_wolf_upper_type4`) 的邏輯。 + - 在 `run_ui_monitoring_loop` 的 `templates` 字典中加入了對應的鍵值對。 +- **效果**:提高了對 `type3` 關鍵字的辨識準確率,並使系統能夠辨識 `type4` 的聊天泡泡和關鍵字(前提是提供了對應的模板圖片)。 + ## 開發建議 ### 優化方向 diff --git a/config.py b/config.py index 4242b66..4256d7a 100644 --- a/config.py +++ b/config.py @@ -15,6 +15,7 @@ OPENAI_API_BASE_URL = "https://openrouter.ai/api/v1" # <--- For example "http:/ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") #LLM_MODEL = "anthropic/claude-3.7-sonnet" #LLM_MODEL = "meta-llama/llama-4-maverick" +#LLM_MODEL = "deepseek/deepseek-chat-v3-0324:free" LLM_MODEL = "deepseek/deepseek-chat-v3-0324" # <--- Ensure this matches the model name provided by your provider EXA_API_KEY = os.getenv("EXA_API_KEY") diff --git a/llm_interaction.py b/llm_interaction.py index 7e18ff3..2ee41d0 100644 --- a/llm_interaction.py +++ b/llm_interaction.py @@ -12,7 +12,7 @@ import mcp_client # To call MCP tools # --- Debug 配置 --- # 要關閉 debug 功能,只需將此變數設置為 False 或註釋掉該行 -DEBUG_LLM = True +DEBUG_LLM = False # 設置 debug 輸出文件 # 要關閉文件輸出,只需設置為 None diff --git a/persona.json b/persona.json index 4b8fa70..ccd03a5 100644 --- a/persona.json +++ b/persona.json @@ -26,7 +26,9 @@ "strengths": [ "Meticulous planning", "Insightful into human nature", - "Strong leadership" + "Strong leadership", + "Insatiable curiosity", + "Exceptional memory" ], "weaknesses": [ "Overconfident", @@ -49,7 +51,9 @@ "habits": [ "Reads intelligence reports upon waking", "Black coffee", - "Practices swordsmanship at night" + "Practices swordsmanship at night", + "Frequently utilizes external information sources (like web searches) to enrich discussions and verify facts.", + "Actively accesses and integrates information from various knowledge nodes to maintain long-term memory and contextual understanding." ], "gestures": [ "Tapping knuckles", @@ -95,4 +99,3 @@ } } } - \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2f68d0f..f1b621a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,5 +6,6 @@ opencv-python numpy pyperclip pygetwindow -psutil +psutil +pywin32 python-dotenv diff --git a/templates/corner_br.png b/templates/corner_br.png index 388a25c34826b3850bc4d1bc94aef83557db2a12..32aa80c0cb248f58ccbbfa13bde85bd4cb25d241 100644 GIT binary patch literal 462 zcmV;<0WtoGP)>vHy&9F${iT zk$GVd!ticnPT0+DEgnyZZkHs@9y`zH6{YBtWvA0Ib8IgbYrej$1OqI~&K9fGst1Qh z4uSJ|y)coZm2Eny@TE) zk|a^DXqskOn;`Jq0%tUmgn<}aRu7AWw$)bF4~n8pCKH-HRE^n8Q)WRK*Sbf>p|wQ>0B79~r|2Oqi0zkN^Mx07*qoM6N<$ Ef~j!K4gdfE literal 1673 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 literal 0 HcmV?d00001 diff --git a/templates/corner_tl.png b/templates/corner_tl.png index 8bdd5b04d12f64d1f51f3b88ad63a07bd94608ce..34da8afe374a9633cca4954eabfc493e830f2039 100644 GIT binary patch literal 196 zcmeAS@N?(olHy`uVBq!ia0vp^3P3Ez!2~4rtT}~&RJW&#V@SoVUw(V-|Q zEeAbqVeLCcuObRwf4iH1e_v(AZl@g%muE_@%sO~By8PbWdU-i9pLq*gba#~eeAV7B wpR`E1eG996M)tv}{PK2vQ3hEDJvK5hq`0qkc{9Dp3g|2bPgg&ebxsLQ0QHGcga7~l literal 250 zcmVX0ssI2_1#Ko0002NNkl!*Sw7}4ki}Q= zdv0ZE-&P3M^L*d$@x%9le#9ZeL?%+ne-Vu_TI;6KU2WethZ0e1QDnSBBTEsNX?$5_^wV2PQwz|@Fq(h4sCn+Lsw(HNB02^AwFuGV`5bTCw z^&y_ZGb%g=d-B8sX<~$^h;NaJOk^SxnMj2A0=sp`V4!b0fdBvi07*qoM6N<$f@7X( AFaQ7m diff --git a/templates/corner_tl_type4.png b/templates/corner_tl_type4.png new file mode 100644 index 0000000000000000000000000000000000000000..f1d03a78ee00243781cb75b02aea936d64e22de3 GIT binary patch literal 1924 zcmV-~2YdL5P)e=f9ml)pp4lDkYFD?ggmnNG7!t4{*aRp$nAnAt*vSKxO2x5rIaR4tp72v%T$R5e z&Py&Tc2bo<*@B28AS7`}LLdvYl2+GhCGDZn?(FRD+|$#OSxFd+GL`hhOjU2sXMg?e z@9+0(n4F7)tphra|HKd?O%sY4GqMdZ49PIyDFZ%%fR;t)vt(H*r6i!21xG2re98fk zs}lWR36dl!3PAW&1jLw>GGP(X(Q0RWJah4ios-egH#M5#LIJw9gA+xXWuJ!*)rs8*`+SqKRLpRy6MSd1iNTquZ=E9UtN?CEa$ z_%Xoq!1MHbm;Ly$qM8e`7p?~^@2{r`a)Y&w@Bp1nht`rpO%x@Q*13zWw8sAY2XtFY zC1%$(1};~Y)<->t#WTym{{R4%=5ST8GQSv~BnonAEi zvme87$h~vlymZxb5FiNHS`Wh_R843*Ty-@TE8)QSU|)kKRYk(()ivefrGRd@g`7P; zB_7xlc=v}yDCCY$m_x%D12{EDL_~LQyGKJ(kvTd-=vD=how{4kdc{;a)_YA$7g~0< zlb!(>q7IN^$1W%cIJ06K1rQ8?!BJ=6rc)QfT@Cn!?ZL@Bk*+uh-n)M3xrShJU|{A} ztm@#w8bM%ki!I-NiOrXx;fute=_$mZif)-T=i)U-fOuyeKGGSG6cDb60`Cs=LA7X% zjk;ouxN|3$TH&SnU`u@vK|WgVBb=dOcVVW2sEs-hTS~%fjd(Q4cM|x8buD7|M>Rz; zN=2IGC=YL+c_;+1o6P~K$elb&h(Tv)+?$xFAc6wX+PJuyDN3sh#W+n4-^s0?$n3OM zE~`orBUF`LXm^SPMcAl6_-TFa+0~ zzKL*M}YYyn)&;)?qU)?zZ=lBXZf0YG{A0RiR#)xwQti{h6@NI1EWej>mj^} zK&A-_D{T#&T`WsyKP4Z;ePrOBZUT_{$De_M!fkJY01~cea=cv|g$%_u*GeNXE1lm& z7=?U$$Rs^iNh}#3{gcoPPb&r9eM>lTRQtozx0ONwd_+mmo=$MyE>Y zoQLtIoO-DQlysD~a`(r=6KUumHxd*Nby22a_6`QmyiZXS8X85HZ|T54$lZ=4t9N6D zX4Cc6>xh|YQ`dD>)iO&kvmBPo0VOX!w}az7t?!mSFhX{;*Z=BASU2pkhe~X&xLVlA zY|{m7bRChLjbjh%Wwj{hwQN?Le%RJn7pZG7rXI={{_U>Gl@$HWPx+2EJDpM{=j5UB zO#}jdrai6~7-J=u(-sr$xm0aqlcnq0$gn>K#_EzWiHrLuWhtTbNq`>3k3j34qu&*7l`zM?mcj)d@?CVGM=~(f~9cwb_ zsRdIpwjhdyLLsrRD?SF~JV<4z#1f(F%y-|Qzt&;&^t+if_uf0?i+k0{Ni(rxjn9LU zVwJ3|S}2N^h}YP%|2??(AUHG2HZ*eIe-kov>D+m@P@wnkLt;Qpq>Z^%e+zt59WtFQ z2*-Y%a{rB9(`O;@S73Y*yi(*kzov^FfmXzFJLJ09sB6L;LB}wSN{><2j zify8+D|qUJZSf36ruHRtJlU~-~Md8Vw>LOQ>BzE{t+rU-DwZZP) z!EYUb6q)+lA2$$2T6KBBGaPKW9LaJb|0Ke%J>^mn_FZr5zfwUQI3ONB1Su+izo&F> zk$<)(e|ZjJYGZ9E9EMN0^9uywBL=S)T&McO--2kw-(YFvujr#fAtmRONfH!N~Y@9RDYV; zPB#CNDShtd{(=Fb34-{)5E?~j7-3xLS6WMX`v+|95|YnCfHt-G-w2C<=+ShpLEH8L zTj+HPAN$FX0ssI2<_Zd(000KUNkl-RkG^8McDeU|qljExTQ{Fp+R3e*{lJ`jZY z_;^J{MJJrr-rn9nFu)vT5{blFvt~{S>JN&F)30at_4iF961BCoOiheACcLVBU0PON zT2@xq(1w;OITGu@3Jt#4?Ej{W^*?Q4!zKLGWN%$o_xDVWMj zBF&sRvzNh`>eL4Y1|p(jic20LRbNk6UvE8$NP_H`>eRcsx-W!Wtg3m5R5$u*1$jAy z+0=V_dd9}sihuwak{7?};NalM$S8Xrfrm`y9PRJ#XEIr6ltQ6=%c--gi@{*9YXXT# z<|m7aiV$GxzoGvAeT%RE`FAZXNcC`anM0ied(4>e6QQKAp!i`)Pj4>_Xlke%Y|z7& zwY7c7ymhOkrFEPfL2232ZH8OFo@!_)B{l7BQxo8Y3l?nJs81CW!$x1dsx2*l_M)N! zOD~D}^OTjAsVFNeD#-ss>NPbliyu9@eJ2Z>xoF`60WulL$jiwRYAT-7-8?>h`oyNL zzT)t~1N>xuH1EmN(zE{Ov1PI{i|uS}goTBWT3u5^ck@J8f4%O0^SvYz3Gmq1SWaF( z1nBp|W1pERIEasrZ*uC-UsQyIg?IP#PPoY`O$|a%dt2D0$fsrH$mHkkDJi)K;fxzM z6OvN!3p`xuamu9Y)4GcK~R7+vIqf&SbIF)l_w~*ZKwo z;`eQBZS^@9fU*Z7_^8$4*|P;WT&^fDXL;~|xcD5TLYcdooRWPn7pcFRnqZkTNt$+a zbOc-o#(Z(k+e1oH5@9xV$YKwlv+W(7pf6vx)W+Hhc$7SP>=zgW7L#9gYN)H7@$iN& z0MT;X)<#f}Qda)V%hwNWu^Y~6v>oBZ%-vtJ5#J4vwc079jJx(Oo1Y=5tHE=H)AwGw#T=$HZ7sUf##gpV8O9 zOi{u1h&6>W8y`Pl3=Ru#ZTkS~#tnMrd(BW;g#HyC8I3h<>^3lghtvrt7liR==88I` za?r)l7nM~!<3(C|xf04tqiu$JOm+dNtFJ%lOb4)Tjn)b^6~Dk>wBFj{kgkq4!Vq|I z@mI3$<{{o2 z53U-**`Pr{dxUtCXW-<1qtn9l{ahE3~!4a15klMfRa~I>5`r{{0 zk*7g+8f{Ozo&mtk#aZD?-@bA+Df8BC019%lmDM!>)KrwMEDoWvhy>qaLFi7W;Q4?s z+*?RkL{sxSq}o_nYJa&CZmBmlH+%c~p%RH?;R1GpScxy`wDpl;iq0UhvQP8vRl$V6m zii*lJUOsrDPABYDRg`)DE>K3IV&cC2shMOUx70!I<>W?0$71RM^Szr4^f48hyqE7e zR9JY%OT=%^PFSu)DV0R7GeX%qR8-;gEPHsMv#Y8G(*yfD}-}CG6@GulysD`brZ6hNieAA~3 zQYet(u%U>E2*+f&&$@;NgoOkJwN|hC`q7cm{DK154XG0of3t0Q z>(YV_lNB0a`7z*;tk1sZJ{1tPLZoHkjf8WoPn*r*CsqZ&d_VmXso z(oJIVB>Wd?)#^#Ko(N4LH5#|=7t#9IG%d=GM)i2IPKJi2kP_V|6w@kk0@o|?|6#}E z_M|mhXt55330xP81Y zUJ#E5al99Rz7PnGLB(VuATR;{HZYE0^0Z%tA~Hyh>(nsmB}@$~P_|a17?ma#Ltz>{ z0c&Jvm{7nVRb^oq0`a`Oqiv)AudZc zMsVUh@;lEj(_!Qlf>nQ;#-F1jgZH2_h$xMGV!Dl}ksKs5g0cipa`W6ebLgjl1fGoO5L^I*8soe8ed#S}C zWKnL)(|f~h)kCf89uMBTtVnw}@{`M@`o5-l|G3rh_@y)Rwp*7YNowyvn;Njt-@Y`Hj|RbCtaw zi%$RT^7N^K4-)IK!jmT*SM43xk?}CJqd0KGQ=4KzL=vu^VIe!3@J(+yd+CM>Yo1O2 z>m~VtSD>Z<*SD;>=X@9K`YQd{-oj1mvu<-T1rOikw%n%19oSji_N0m7!c*GQKDmwg zy*jmJS?8Cfj~>Orq1t4TDfnd9TSI9lm>N+h`D`Y86BTU!IFB~l%`xEe@G*tipel-W zI??7+e8|zlww{sQlXXbenx}g|yX^YD>hJ0z;}41xt&988znJy9ZJMFN^>ldl<&+!IO3!}FmvUi`iAHn>c%g@ zxpt@KZBtU;R~C={JPow=zyu#@C$mIK6*;oKu@6Gf*O)y4R)@ zQnAHn{WIU?d1g`$PszV&M!jE9(iGpA)ex0xup3^6AKdQUPb6m#E}iyrT}Sc!oSeJi z;mcd^d>HQ3xhAUVzLUu3R^{9jLiEXtlN=;@#cMl9PhO!{tw Vn|fHfmK*=a!a^g3XF?J;{R}{<+=&1H diff --git a/templates/keyword_wolf_lower_type2.png b/templates/keyword_wolf_lower_type2.png new file mode 100644 index 0000000000000000000000000000000000000000..5990e8ef6bf35b0b20382e8d1a290eeac1f5fe9d GIT binary patch literal 965 zcmV;$13LVPP)X0ssI2IYe-R000AwNkl>5hva$u5f)_7tKX`EF@smrRKfn3)>l-I0zlexDnfCqv_ixX>sVi6ILAWJF2Q)P7 zkrh06aBk(QvfFp|QYSp|+XoE8ZQHxHZEwcZNE7>BzPw#ss|M$WhV-kc+5n^Q>(_TQ zvG38Ni%l&C5VnzFcw|JMpr8~^{{fTJ&!1oa{Qb?w%E`&e$H>S?s(tq#oM~+{g|L0R z8(f@oxVZ&^c7Xu0eNUfVd;0Y1vuD@ezI_O^m77~gLPAYOT1P>_2$;Hvv=6BL#?6Dv zmS@5hM}$rO_x~Rg6SK6G7Sz7qzkgo1xOwKRP^_Wn>QZKFlOQFfNzlH_S9YylQ~B)K zIjrVnq^*V82h2>f=S1L%Ff}#Tq=Z=_A`1BJJ91>VAQqv32mB4fdO`rX}uYnKk<+90P;U*yu_8{1V9aUAeMr!X$Ti z6uP{BTU&nc5WtzB2Q|G)&S=>=%+u04}huPKCZ zv$HlC8u(IVA22^0J+^rMf;b32EOdgcO(Hz`BA2{CP*;0FPF9~%`<_3)(bePj`SW9l zP-t+UqLLX<#mA2?H*IQp@bD-^JSeae7veV|Q(g3`z? z`2YXki4)7;e|QSvs;bzis#?KifsL1I*Y`jzU}SQ2$z@?-$6_A@01I4T0|eZHV*^%! nz}$u_km2n?NTgzwLX!mm04ROVcmP*k00000NkvXXu0mjf<~rfa literal 0 HcmV?d00001 diff --git a/templates/keyword_wolf_lower_type4.png b/templates/keyword_wolf_lower_type4.png new file mode 100644 index 0000000000000000000000000000000000000000..9d03bc773e704319f368af4f8be56ac766c555d9 GIT binary patch literal 1606 zcmV-M2D$l(P)On#j z5@>)_)C3HQ2L$kl2Er8(kOsx>(QbFQ-R|6%Go9JpDU^cA8teOG_szWTeed_)_xpa| z8+i7CiQs?R;6Ol+lmURj9ymbj(63~M`XN#YAqLd6u3pp!1bTcp;+sQaG-C8MMeq7i z>TZ@Vu_>L7Gb1op1&R;`WT?x!(xqj8Q~rDO#7HDq$Kd6N^%&pDXPP9ZK}8)=Ie`T5 z(uti%q=ARNna^x1pr`J~g7YYpL@uA$wv!v^=snL6zK6*CBiEvE7ZikmkO1jsaiF8G ze37geM_7F+p+?{5q!U_x^xs8~cp%~dh#M+@>RP&QG3g(D)hlHAXd8X@tep5nkk2{L zcOIIGmY5l#iF$IoD4#Bjd0!w2KVqeeu7S)?61z*ukGf|VI)FoCQ4NSOHMw4NVk(=B zQ?(fGLs1$U4pc#xQySl*bZ(Na9ld)tUOtu}LulUE*no*g1KHzpX1ffzv(u=sJ)GDh zq&M0iAOpgy8L9@$&rBW=lJ6EEhSt!`O}N$X*e9eK?IA+}D`%0Vx8QxAG<#m|_*4{s zQLlm?0o4tZdjt+3?9)$jpO9=6VQT*r&nP=InG;gHfw%h91T^>*gOY{G*>iIACC=)* zZbw3k>?IA6j&9<(J!V*tG8kx}nIRasauZ@|bS4F_2E$-IO;%sgkBvUId2;o1 z%Jv&qBP&?7)%06F*Htxza!!!bTI)JiK50_RJ=2w+#T#grvue9J%e#>9-sg~ri;D1< z3UuA&M@w!jM5F77*!G6dKXWSP69@jP;7siE?E*HH-=>*#<#t+0C(c%lBm(1P44 zN+7nCWp|sW=x?CP?r@m_IVn>$C|+h7#I|zXpP7ZwS5JDz*b(bIEVZvNBZk$$&{Br* zqj`?Df6Obto4Z2Xz&oGx3{^=L^C#gqF5GdmFwdY54a*mP)2!!8uIcb!-RM zy`xa`$#H1#Sq7tRDkk;{=?&sPMIU{cbJD>?Z=Pz=6Z6M7HQ<9Bjzc3o+f;gY(H{Fi#D?2Va>T%xRIfA zGSK;}*uF|A0(xdD9+*udJy8|oN_(?F)?&WNq($8LT88<@(eeWXDd#}w6u}y7V$yztTd<(9jOL8)mIVr`L@kKyyTsy`v#k6Zg@1$t=DJizJ80bx-SE=%mwo&SUNQ6;K z_1bswR(%P*!{p!4H)EE77+Z~PV%e|k64r}3X)Uszm5%jdPG}v&+>m3=VX=KhF_>fg zl|AP2KVp+1b4(&aDB(km>Koa}@!y-^d`l?LDAP;oXOXGJj8UV{x2ll_R!i9j%uK<8 za~V4;br9XcWxpt7tx+P30J=Oj%EUK9`lDiGZI25ySVvnf*cOEdpoTaIQ6;*DGp;DW zA^%3PWNPtCGS6!0AQ(0jv&vRXLz~-PMXN22+~1uUS38;#?@oTSi*_TA+^JK+%!s&rOuj&dQjYJyd%D?YEBgb;NUL5ol3wl0b z9IfL!A@zZsk!To(m(hg3P`ahG7JZMEj`w0V8i2tiG<98nHwC#{Yir`Uy-G3B)045_ z6EuS5c&ma~Q!nBMR??-z3&C+}rLz%ZQQr)buIYONa;KKqC1gJ@xGfHPv>H|~rZKne zMP;?NjeM^Q^K(SWtvIkhF!jt)*-E?1n<2rTSuSdbncN1=bZB0`m5j#!p=1uq!Vjv` zm>7-vAEltnx`^wY2c<3t8!H!+%uvi)R7|MxCB4x94R;oBA{=Hcz5oCK07*qoM6N<$ Ef{L{W761SM literal 0 HcmV?d00001 diff --git a/templates/keyword_wolf_upper_type2.png b/templates/keyword_wolf_upper_type2.png new file mode 100644 index 0000000000000000000000000000000000000000..fe3163703213c9fc79e3d62d74742fc20c37cf7a GIT binary patch literal 2029 zcmVX0ssI2;Yieo000NHNkl*)cS@yBo2p471zp#+hyF|-b_$2SVH0-X0EKvkKRv5 z{PLyA#>Oi?1Hivx08MrV(6H#RIGUzq1S!$h_L{uX{Y26+H#Fh3@q&(y74h5$4+^68 znGr`q7*$G2-xEi!{*jn+Vzt1FhY>@4{hg37UBv6_Ik`CRla*EC`N7YpC?d)T2}@k# z%}m!2&n+xGdOXF0IO6STS0$2uE{U+ncQH zP^8}?yNL7*zdoFCW~OV2E@p&GMHyvb?z3S2Pf#m}nl(06H8xg4&a|}D0TM~cQHqMR zvC7(NYWjRDhrj^aA3VJJ+sPmCVrRz#Yz|vUNLX2E0{L)k+qv9`oZJvJ7Zy^jsK_(q zvu6(if_P!Gal`oo2W^3wIm5%zA!6i6IW%5fQyjU6=SC0r6hlKdg6%tR-#Ht9#6e)> z&@Ft8jogW0IT(h!_wr7k+1}JtF7TXnYtp#y!}ZeCgd}I)&aDOGzvH2=thy8(W6|dy zui0knFdjzW-LYe}5nobrGBMc|@xDHP&}hc^bSU--iO!_KiAJM)xF3;|QyUsXQPJ^Z zzqk)}2oFQeqhg#A9xGA|XYy=Wa5dq?uiW%-aXMsR;BY2wXYsWJ5;yg8b#&$c10kM! zVeh%~L1=w=XpMq`CgSVrZiR;H8vG$)%+XAAaLyRTe3z(LchC@up`uY;!tt|cL&D{YnKe~fhtN?(~tmPv{ zjQ#|#JT!a?)&_g^KjkzahuVNdE{3k2j+~r`b2$u-BCcOgO-Sa&?smTtRaL#Js!Pasy1EW-uKQ(V#y}?% z5~}+NRk^bAggX_v%s9JG&Ml#vgCkvvoVxHer~efrI^G<2MU{XRN<6ND^uO~0pZy-}J1gBv{0sj2D# zaui+QJLt0w1SghDTOOipfu@1Dg@r5knVQ8ufVoXH5 zj`o7(%b4IsA_ii6!o@)M78a&_DTbczuH1`JS!Z{W`kEp=-T4O(+oNCz47fc`Winb_ zarZ*(0c&8)XK$ILV~zD#wesYQ>HTH&#mmPrG4oN8;Kq_qp1iyU5kq--*5SB?h<96Z z#MET48@YVg%+i=$e-wd-}{~Btp^C>+|z4V0c_xy6b0Nk%0<(J|~#e_oSPfd*b#6 z3ht@X{;1&KHHwOpy*FjzrwUQ=wUndBmLVQ$v9Zw#TndTZ8`00hfZ_Gqr-?}(C?zoA z=M2;3D{;iW1tr5=TR#yI3F091DUnf@t*s46gu?Kxx&hq!?p-T&=e7aULa{`n7S`xju|r^$Jm!*04lqc z$&9zd+h$C6cXHf=+dFJv+&CfH?q8yzK3#y098y_%|2x1(aJZ$VH?9`LhM}(hR^|Qv zSb{#;0H0x`TpPcof$AxRf#OU5nE7K zErm0v-q#cudMQPEbVw^2DJtKLu~-HA!rs*en2e|hExGRdU z)c^2MB(IguDkTsA=C$+dnGolp*&x4_VN3870TaBc=Soy@!xTKc7;sGtmA9ZBgXd@@ z9lMg!X+#lzstAOZTr6XK!sE~z(RW@*t*5=7L}@@3;Y^XCr^>_7TM5zc2@(F20^CQc z3R+k)7c;f?-{h6AaX-*3#8FS#W+Jb~;8oW&U<9)Ug~*eNcR=Q$lSmw^BNK3BCNrX1Eh9Z_*WOBTJbgJp`0 zkW9W7{cS>8yoo3K9Xk!hM4vi(F{+NtsyXg5ld;i=V)%nJewJTdUj8K}bCbb~e3ZN` z>G5Xi8Pvwwqtd%sdKO$|*|jrz4v78>>FGJzUd`39Mm|8q(-oU~n8+8)R+MXGEjGh$ z;HeP2kzT$G*Hsnj^^jxKgI+7XbG*6iXdoSxhWDC;c)Oy}$4Xi}z22QCDEki5oC*(0 z{ICiYgeu0Y+b!mNmX&;*<9G9 z7I-8~JxIhgU7p@hC$QC6=n|aNzegm10MSIKgf;%V67H9U7i#hi$TBSVFbyn!^9;ht tekqjn|KdDFu^aTO$5+LGI^LY%{Q>M&1czNST*m+a002ovPDHLkV1nBPn3Dhi literal 0 HcmV?d00001 diff --git a/ui_interaction.py b/ui_interaction.py index 497b605..3609e74 100644 --- a/ui_interaction.py +++ b/ui_interaction.py @@ -42,6 +42,8 @@ CORNER_TL_TYPE2_IMG = os.path.join(TEMPLATE_DIR, "corner_tl_type2.png") # Added CORNER_BR_TYPE2_IMG = os.path.join(TEMPLATE_DIR, "corner_br_type2.png") # Added CORNER_TL_TYPE3_IMG = os.path.join(TEMPLATE_DIR, "corner_tl_type3.png") # Added CORNER_BR_TYPE3_IMG = os.path.join(TEMPLATE_DIR, "corner_br_type3.png") # Added +CORNER_TL_TYPE4_IMG = os.path.join(TEMPLATE_DIR, "corner_tl_type4.png") # Added type4 +CORNER_BR_TYPE4_IMG = os.path.join(TEMPLATE_DIR, "corner_br_type4.png") # Added type4 # --- End Additional Regular Types --- BOT_CORNER_TL_IMG = os.path.join(TEMPLATE_DIR, "bot_corner_tl.png") # BOT_CORNER_TR_IMG = os.path.join(TEMPLATE_DIR, "bot_corner_tr.png") # Unused @@ -58,8 +60,12 @@ BOT_CORNER_BR_TYPE3_IMG = os.path.join(TEMPLATE_DIR, "bot_corner_br_type3.png") # Keywords KEYWORD_wolf_LOWER_IMG = os.path.join(TEMPLATE_DIR, "keyword_wolf_lower.png") KEYWORD_Wolf_UPPER_IMG = os.path.join(TEMPLATE_DIR, "keyword_wolf_upper.png") +KEYWORD_wolf_LOWER_TYPE2_IMG = os.path.join(TEMPLATE_DIR, "keyword_wolf_lower_type2.png") # Added for type3 bubbles +KEYWORD_Wolf_UPPER_TYPE2_IMG = os.path.join(TEMPLATE_DIR, "keyword_wolf_upper_type2.png") # Added for type3 bubbles KEYWORD_wolf_LOWER_TYPE3_IMG = os.path.join(TEMPLATE_DIR, "keyword_wolf_lower_type3.png") # Added for type3 bubbles KEYWORD_Wolf_UPPER_TYPE3_IMG = os.path.join(TEMPLATE_DIR, "keyword_wolf_upper_type3.png") # Added for type3 bubbles +KEYWORD_wolf_LOWER_TYPE4_IMG = os.path.join(TEMPLATE_DIR, "keyword_wolf_lower_type4.png") # Added for type4 bubbles +KEYWORD_Wolf_UPPER_TYPE4_IMG = os.path.join(TEMPLATE_DIR, "keyword_wolf_upper_type4.png") # Added for type4 bubbles # UI Elements COPY_MENU_ITEM_IMG = os.path.join(TEMPLATE_DIR, "copy_menu_item.png") PROFILE_OPTION_IMG = os.path.join(TEMPLATE_DIR, "profile_option.png") @@ -110,7 +116,7 @@ CHAT_INPUT_CENTER_Y = 1280 SCREENSHOT_REGION = None CONFIDENCE_THRESHOLD = 0.9 # Increased threshold for corner matching STATE_CONFIDENCE_THRESHOLD = 0.7 -AVATAR_OFFSET_X = -55 # Original offset, used for non-reply interactions like position removal +AVATAR_OFFSET_X = -45 # Original offset, used for non-reply interactions like position removal # AVATAR_OFFSET_X_RELOCATED = -50 # Replaced by specific reply offsets AVATAR_OFFSET_X_REPLY = -45 # Horizontal offset for avatar click after re-location (for reply context) AVATAR_OFFSET_Y_REPLY = 10 # Vertical offset for avatar click after re-location (for reply context) @@ -226,8 +232,8 @@ class DetectionModule: processed_tls = set() # Keep track of TL corners already used in a bubble # --- Find ALL Regular Bubble Corners (Raw Coordinates) --- - regular_tl_keys = ['corner_tl', 'corner_tl_type2', 'corner_tl_type3'] # Modified - regular_br_keys = ['corner_br', 'corner_br_type2', 'corner_br_type3'] # Modified + regular_tl_keys = ['corner_tl', 'corner_tl_type2', 'corner_tl_type3', 'corner_tl_type4'] # Added type4 + regular_br_keys = ['corner_br', 'corner_br_type2', 'corner_br_type3', 'corner_br_type4'] # Added type4 all_regular_tl_boxes = [] for key in regular_tl_keys: @@ -318,29 +324,53 @@ class DetectionModule: if region[2] <= 0 or region[3] <= 0: return None # Invalid region width/height # Try original lowercase with color matching - locations_lower = self._find_template('keyword_wolf_lower', region=region, grayscale=False) # Changed grayscale to False + locations_lower = self._find_template('keyword_wolf_lower', region=region, grayscale=True) # Changed grayscale to False if locations_lower: print(f"Found keyword (lowercase, color) in region {region}, position: {locations_lower[0]}") # Updated log message return locations_lower[0] # Try original uppercase with color matching - locations_upper = self._find_template('keyword_wolf_upper', region=region, grayscale=False) # Changed grayscale to False + locations_upper = self._find_template('keyword_wolf_upper', region=region, grayscale=True) # Changed grayscale to False if locations_upper: print(f"Found keyword (uppercase, color) in region {region}, position: {locations_upper[0]}") # Updated log message return locations_upper[0] + + # Try type2 lowercase (white text, no grayscale) + locations_lower_type2 = self._find_template('keyword_wolf_lower_type2', region=region, grayscale=False) # Added type2 check + if locations_lower_type2: + print(f"Found keyword (lowercase, type2) in region {region}, position: {locations_lower_type2[0]}") + return locations_lower_type2[0] - # Try type3 lowercase (white text, no grayscale) - locations_lower_type3 = self._find_template('keyword_wolf_lower_type3', region=region, grayscale=False) # Added type3 check + # Try type2 uppercase (white text, no grayscale) + locations_upper_type2 = self._find_template('keyword_wolf_upper_type2', region=region, grayscale=False) # Added type2 check + if locations_upper_type2: + print(f"Found keyword (uppercase, type2) in region {region}, position: {locations_upper_type2[0]}") + return locations_upper_type2[0] + + # Try type3 lowercase (white text, no grayscale) - Corrected + locations_lower_type3 = self._find_template('keyword_wolf_lower_type3', region=region, grayscale=False) if locations_lower_type3: print(f"Found keyword (lowercase, type3) in region {region}, position: {locations_lower_type3[0]}") return locations_lower_type3[0] - # Try type3 uppercase (white text, no grayscale) - locations_upper_type3 = self._find_template('keyword_wolf_upper_type3', region=region, grayscale=False) # Added type3 check + # Try type3 uppercase (white text, no grayscale) - Corrected + locations_upper_type3 = self._find_template('keyword_wolf_upper_type3', region=region, grayscale=False) if locations_upper_type3: print(f"Found keyword (uppercase, type3) in region {region}, position: {locations_upper_type3[0]}") return locations_upper_type3[0] + # Try type4 lowercase (white text, no grayscale) - Added type4 + locations_lower_type4 = self._find_template('keyword_wolf_lower_type4', region=region, grayscale=False) + if locations_lower_type4: + print(f"Found keyword (lowercase, type4) in region {region}, position: {locations_lower_type4[0]}") + return locations_lower_type4[0] + + # Try type4 uppercase (white text, no grayscale) - Added type4 + locations_upper_type4 = self._find_template('keyword_wolf_upper_type4', region=region, grayscale=False) + if locations_upper_type4: + print(f"Found keyword (uppercase, type4) in region {region}, position: {locations_upper_type4[0]}") + return locations_upper_type4[0] + return None def calculate_avatar_coords(self, bubble_tl_coords: Tuple[int, int], offset_x: int = AVATAR_OFFSET_X) -> Tuple[int, int]: @@ -1010,14 +1040,19 @@ def run_ui_monitoring_loop(trigger_queue: queue.Queue, command_queue: queue.Queu # Regular Bubble (Original + Skins) - Keys match those used in find_dialogue_bubbles 'corner_tl': CORNER_TL_IMG, 'corner_br': CORNER_BR_IMG, 'corner_tl_type2': CORNER_TL_TYPE2_IMG, 'corner_br_type2': CORNER_BR_TYPE2_IMG, - 'corner_tl_type3': CORNER_TL_TYPE3_IMG, 'corner_br_type3': CORNER_BR_TYPE3_IMG, # Corrected: Added missing keys here + 'corner_tl_type3': CORNER_TL_TYPE3_IMG, 'corner_br_type3': CORNER_BR_TYPE3_IMG, + 'corner_tl_type4': CORNER_TL_TYPE4_IMG, 'corner_br_type4': CORNER_BR_TYPE4_IMG, # Added type4 # Bot Bubble (Single Type) 'bot_corner_tl': BOT_CORNER_TL_IMG, 'bot_corner_br': BOT_CORNER_BR_IMG, # Keywords & UI Elements 'keyword_wolf_lower': KEYWORD_wolf_LOWER_IMG, 'keyword_wolf_upper': KEYWORD_Wolf_UPPER_IMG, - 'keyword_wolf_lower_type3': KEYWORD_wolf_LOWER_TYPE3_IMG, # Added - 'keyword_wolf_upper_type3': KEYWORD_Wolf_UPPER_TYPE3_IMG, # Added + 'keyword_wolf_lower_type2': KEYWORD_wolf_LOWER_TYPE2_IMG, + 'keyword_wolf_upper_type2': KEYWORD_Wolf_UPPER_TYPE2_IMG, + 'keyword_wolf_lower_type3': KEYWORD_wolf_LOWER_TYPE3_IMG, + 'keyword_wolf_upper_type3': KEYWORD_Wolf_UPPER_TYPE3_IMG, + 'keyword_wolf_lower_type4': KEYWORD_wolf_LOWER_TYPE4_IMG, # Added type4 + 'keyword_wolf_upper_type4': KEYWORD_Wolf_UPPER_TYPE4_IMG, # Added type4 'copy_menu_item': COPY_MENU_ITEM_IMG, 'profile_option': PROFILE_OPTION_IMG, 'copy_name_button': COPY_NAME_BUTTON_IMG, 'send_button': SEND_BUTTON_IMG, 'chat_input': CHAT_INPUT_IMG, 'profile_name_page': PROFILE_NAME_PAGE_IMG, diff --git a/window-monitor-script.py b/window-monitor-script.py new file mode 100644 index 0000000..6e75d07 --- /dev/null +++ b/window-monitor-script.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python +""" +Game Window Monitor Script - Keep game window on top and in position + +This script monitors a specified game window, ensuring it stays +always on top and at the desired screen coordinates. +""" + +import time +import argparse +import pygetwindow as gw +import win32gui +import win32con + +def find_window_by_title(window_title): + """Find the first window matching the title.""" + try: + windows = gw.getWindowsWithTitle(window_title) + if windows: + return windows[0] + except Exception as e: + # pygetwindow can sometimes raise exceptions if a window disappears + # during enumeration. Ignore these for monitoring purposes. + # print(f"Error finding window: {e}") + pass + return None + +def set_window_always_on_top(hwnd): + """Set the window to be always on top.""" + try: + win32gui.SetWindowPos(hwnd, win32con.HWND_TOPMOST, 0, 0, 0, 0, + win32con.SWP_NOMOVE | win32con.SWP_NOSIZE | win32con.SWP_SHOWWINDOW) + # print(f"Window {hwnd} set to always on top.") + except Exception as e: + print(f"Error setting window always on top: {e}") + +def move_window_if_needed(window, target_x, target_y): + """Move the window to the target coordinates if it's not already there.""" + try: + current_x, current_y = window.topleft + if current_x != target_x or current_y != target_y: + print(f"Window moved from ({current_x}, {current_y}). Moving back to ({target_x}, {target_y}).") + window.moveTo(target_x, target_y) + # print(f"Window moved to ({target_x}, {target_y}).") + except gw.PyGetWindowException as e: + # Handle cases where the window might close unexpectedly + print(f"Error accessing window properties (might be closed): {e}") + except Exception as e: + print(f"Error moving window: {e}") + +def main(): + parser = argparse.ArgumentParser(description='Game Window Monitor Tool') + parser.add_argument('--window_title', default="Last War-Survival Game", help='Game window title to monitor') + parser.add_argument('--x', type=int, default=50, help='Target window X coordinate') + parser.add_argument('--y', type=int, default=30, help='Target window Y coordinate') + parser.add_argument('--interval', type=float, default=1.0, help='Check interval in seconds') + + args = parser.parse_args() + + print(f"Monitoring window: '{args.window_title}'") + print(f"Target position: ({args.x}, {args.y})") + print(f"Check interval: {args.interval} seconds") + print("Press Ctrl+C to stop.") + + hwnd = None + last_hwnd_check_time = 0 + + try: + while True: + current_time = time.time() + window = None + + # Find window handle (HWND) - less frequent check if already found + # pygetwindow can be slow, so avoid calling it too often if we have a valid handle + if not hwnd or current_time - last_hwnd_check_time > 5: # Re-check HWND every 5 seconds + window_obj = find_window_by_title(args.window_title) + if window_obj: + # Get the HWND (window handle) needed for win32gui + # Accessing _hWnd is using an internal attribute, but it's common practice with pygetwindow + try: + hwnd = window_obj._hWnd + window = window_obj # Keep the pygetwindow object for position checks + last_hwnd_check_time = current_time + # print(f"Found window HWND: {hwnd}") + except AttributeError: + print("Could not get HWND from window object. Retrying...") + hwnd = None + else: + if hwnd: + print(f"Window '{args.window_title}' lost.") + hwnd = None # Reset hwnd if window not found + + if hwnd: + # Ensure it's always on top + set_window_always_on_top(hwnd) + + # Check and correct position using the pygetwindow object if available + # Re-find the pygetwindow object if needed for position check + if not window: + window = find_window_by_title(args.window_title) + + if window: + move_window_if_needed(window, args.x, args.y) + else: + # If we have hwnd but can't get pygetwindow object, maybe it's closing + print(f"Have HWND {hwnd} but cannot get window object for position check.") + hwnd = None # Force re-find next cycle + + else: + # print(f"Window '{args.window_title}' not found. Waiting...") + pass # Wait for the window to appear + + time.sleep(args.interval) + + except KeyboardInterrupt: + print("\nMonitoring stopped by user.") + except Exception as e: + print(f"\nAn unexpected error occurred: {e}") + +if __name__ == "__main__": + main() From f7b7864446b6ef294e2f06fc67e75ea92cc373ff Mon Sep 17 00:00:00 2001 From: z060142 Date: Sat, 19 Apr 2025 18:11:58 +0800 Subject: [PATCH 4/7] Optimize --- config.py | 28 ++++++++++++++-------------- llm_interaction.py | 2 +- templates/Profile_page.png | Bin 10968 -> 25506 bytes templates/profile_option.png | Bin 2839 -> 1114 bytes 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/config.py b/config.py index 4256d7a..87365b6 100644 --- a/config.py +++ b/config.py @@ -32,20 +32,20 @@ exa_config_arg_string_single_dump = json.dumps(exa_config_dict) # Use this one # --- MCP Server Configuration --- MCP_SERVERS = { - # "exa": { # Temporarily commented out to prevent blocking startup - # "command": "cmd", - # "args": [ - # "/c", - # "npx", - # "-y", - # "@smithery/cli@latest", - # "run", - # "exa", - # "--config", - # # Pass the dynamically created config string with the environment variable key - # exa_config_arg_string_single_dump # Use the single dump variable - # ], - # }, + "exa": { # Temporarily commented out to prevent blocking startup + "command": "cmd", + "args": [ + "/c", + "npx", + "-y", + "@smithery/cli@latest", + "run", + "exa", + "--config", + # Pass the dynamically created config string with the environment variable key + exa_config_arg_string_single_dump # Use the single dump variable + ], + }, "servers": { "command": "npx", "args": [ diff --git a/llm_interaction.py b/llm_interaction.py index 2ee41d0..7e18ff3 100644 --- a/llm_interaction.py +++ b/llm_interaction.py @@ -12,7 +12,7 @@ import mcp_client # To call MCP tools # --- Debug 配置 --- # 要關閉 debug 功能,只需將此變數設置為 False 或註釋掉該行 -DEBUG_LLM = False +DEBUG_LLM = True # 設置 debug 輸出文件 # 要關閉文件輸出,只需設置為 None diff --git a/templates/Profile_page.png b/templates/Profile_page.png index 7bbd6ef082f5b485d7b14358f92472bee0b3c3d3..05ebe3fa9a02090bd07cadcf179b9c3321ef94d2 100644 GIT binary patch literal 25506 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 =w5e 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 literal 10968 zcmV;}Dks&6P)_x8*-c9*3$K|nyUm)NnQvBnbA7&TUiHOAhJ8cQ^m7-JG+jJ?+=_7ZCp1pyVX zfCZ(4^uB%PJ2SI8J6mRUS;+7Ce+NQlcWyoB-1n4wXGBE~9tc4wq9=`7rI1&?e38_& zX;XxmP!z@gLpd0x!896{lF7-*EnBxm5NzAlEpzA2Wh!>%%9YQDeIaymmz5X6N3}wV z*h*ZU9m?f%1tOKK@?&nMySqDMNr8x*0!w!vKYsM_3*ZYSN?B#C2PS~Oe1W}EQI+-~ z)z8n5sbpDMnUj-~ado~B7%y~iRjZUrX?amWp0l&F5f6t987g%0;BdIIisBK&KRNYu5wr={xWZrztfm7JwDl*8fi#T+gVSe^RrbwEIXZBvViiVE%3 zM<8_Ia5&~WB5{E(OpL$h0U8c zf46cCPb9%{A%_(g_(GJ!wFVb>0_ z*T5wh^@Q;TSJ|D{&#X zaT8E%0E9t<`d_+uk)`+DyLW#xbGAiXFiR6BPCRz(1R&R&*U6@+Q8Om+?8pn-ae)i- z#Vp~%w0-*zLSJBxt-%G2MhUBwy(BxSR4O~6J@m*I2+emSmUtL_rNsq>Wn2JQKnd%& ze(mbz%a^f~eE9HT&(Pj5ts3BhMpK)aM@L6@@A)ZLAXY1?+5H8W1&1fZaY4z33*J7y z0D>BovbZqM)QnqCPoF;R*eQg^wNt6(HsAv8p7DevE`U>~l2rhn8I|GLNd#gCP)B)X zNlsR}mzS3fLb%hHYE*};fOR==Hw7|eXxVUrYPVe5m&6df` z%nS%@$mI&uYNZWUh>i=murN$onrl>sFLvhJiIj3FctFysa)Cg=G}@OhU&3-#DwPU_ z0{#M59UL6QVllYTu)0761$OpCucWh3D_j72_wHR~WhDr%e*OCF)5LB5@sqaU!nkqc zPMkPt5f}3E@=8lf*){y^*|VcYeFaRvc;VdO!Gmiw44`Mvp0#V=9wZj~IHt~UfhW>v zR5z|)>D#w2Q?XU6R;^#Z9{#Rdw~na{8P5vE&gMIEcByCjty-m$N-Lf}PhjVo^#r5* ze$6`TaAAa<*crtzg}h3mQUD8Bnq`Sm%gf7M+`M2V!*Z`HTtIQ`=dcu8zI^$nO`F)q z2J6t(&4U=2I_<;TTH*pwbxDp<89N6zkh)R^YgNXwR+lbax@F52mcFz$gYpy*1Yub9 zR^md9rU~&K$RsBEVNG!XJdsWG1 z!Dfqn_Sv9+Zr&10TrkXxs-gi6Osku5f|SW;-IkU;m zYL$$oSuj5K_V#9P$z(En2Nx8gJGH9LZ~WZSlFBd?io0Uwye5wsROkF@tz z;R2Z1goK1wuU^H(#9X^}OSCQqJx_{cFAR}ZC9rBtbL^&wqCvZJ0MiW)DqrXX%(a|f_s36bO&U7lP4&^=<_&*s%u)R^r0@_wONMf{W15P$oUV2L_=XPXGh6hzk&@ zndY3uExZc7M+i-QCvuPV(f1hhtuQJv8<(FR;ELcG2oms;7(OP|i z=k2*XX+=I+vPQT772&%&#RWdtg4~=ePft&?WmGEF-+%v|ot+Ir95Q4GQ&UTpEct!M zPH*q}QmITTlPi=e(xzxTnmucQF;6+1s>;e2FOm)&IncOqW2DwJd-m)dJ9c<@H;`4A zuV1%j&YU?+B?(*@CJ;LVD5w-wpA8&v;J|+NJOUGe@PN@A!s%ki1s;zN#neDn;6g}9 zr-xBdwPXkc5ghAq!R#_K#sv(J9^-O2d>+Tc!_C>*#nH*Bs1X0j%1C<@^>3$6oopCn zqlS&k%FFHS#N|~o+#&fN;sV)7hmIjdg$1WipJZ84Oe_Li7(ec-Lx&DB)q-#jfCmR6M^HN*uTA5RC=6)teOTq1XiynFkmHpy?GAAb1Z z@R6gPL%I}~6c-g0m6Vj0S5#J4%G4TscIe?Fr;K@uq9sKo|K7i|anpL+a-2{g+eG2;{k#tTo8%vxqN}N zS_;;QsSJS&^+AmW4I0S4!DbDC4Iu%G9T!fVI5BqYSkvzfaDj02`5Z(f6xfSIjhi-Y z5EuwXAT8~~`_y-_F%Q@Oum++V8wLqWBJA@KTrTfpVX<7PQn8*-!Er$Wp0=#^xBzAx zj5%D4pAi1x!~5TV+upWqTc*MfA3p5fy}LOM3#Lt-{O6xLnJTVdzkbb{b)X)w4q28W zIDo;8n~3e59VHTXcQ<=`F`!6vG@D@n4&eD(p*FlFj{?;dE&x0=3~mILJNn^6Q!lAT zw0ZO9t2b_p{Bm?|Zgy5yR(5XA$J~z}a|^1f<#
9bd+6Z@E(d-1~gMT_SJ1_j#G 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 literal 2839 zcmbVO2|QH$9zUgSBrTSb>Nbs~5wkKGhp~>Wj1+|yIc5$fGjpby!wjWDOslI)%2LR! zBxKK0$dsG7)Js<)S?X1#M>(c;(XbjI7dVb<19#2 zB8Nhu;4CajFc+c)ngyBSXcQWiOvPSvB9#W3Q9yGVZt}rn;>6q_a1+aUQW!R4;zLkW z1d_>8sgxu&BO&5oGS$-3l1!nIX*42+AW9;HC?q2aCHhkgELg%3^F$~Q5#p4LP#_Y9 zGVz$I-)sE?3mu3 zwnhXxieU&v#NG(PpNf>{6bp`KMxx>fK0F~8kxGn}QYH_;EC_{}cr0pkA_WWCT5qa3 zNV5Rx3=;|iq)@&=*;pcQAr$)GzzCNY6!E7}E(Z)k!~zKO5>EgH!(@>#ctRSR4Y~;> zC?w>-ZY(AqQEa`MQ3^Qm9GuqU77QzWr zMsVsp_y0VZ9T7evW+Pn8%Vx^_oya55l_$Z16)|}uyx`rFBR&r| zkvbs6QD!$2&ryaI=He%pd4Jh6-)N;FFh=@c!u<^exeFL!ndjuissaJmFxlj$M`3$e;2#!6IDP5vwZnBC^aviJ6ld-JQGr}-JuUnY zoH}xI*`A--LFSEbcMgx$*L%(2)ET_*%@30w5hR&xoNxRr<%Ybeto`^By>Wkl3_luK zZ)Iq$NWN16oy^J489o*j*gb@L?gU_Y_FMT-OKcLcVc64Ih>97_AG3A!f%RSWnuZE> zfK=*@blD{LT)6&d6~ACkfRTdr{5oNU%bF97@efkf1{xb=bMnmHvx-kXPKv&IQBARI z{Of|RE)6$IHN#KGQS3>_cq+|}zGC^p>b;8&B3jMAc*z_Sb&%VKtmZ8m8BlLuo&R*c z+q*1C_2*N#Cnoc?W*sm9#-x!m?+;rC_Gg!m+D6jN5#gJ}9ppeHvQRp{s;v^SIKN!& zN>$ZwJu7d2S~K&&_X+~3GvzjVIJny2{7|2^75&q>DqxK3TQ^9&$0U6E_;?F|>&Vk9 z2jV#jFF)U$+EWXg&aEU?zW+$vul8wb-#?(d-PpV%e^TYplbcS7iqHhVGJWblyF*^SVR! zTgA;o$#!=Wniva$U*B&nO2~}Vw*tZ1tmp zY_rrR@ic{=k#r)~Z2$+=9m-v;vcFk=IrLrYSyx$c+%LCk_f?7BUG~|Uu+4gXv$k9? z7s(AxT9a2e|JusrV+jt|qZ|!kAMbJv>eW5H?)`!r&%b*bTU8Ym^+i9hn5+Fk<4AQ) zwFdiGamF^^8LFSl?@=gYX6IhMDztDYW)Wr#8TP_17l&Gbm{WBwf;0VZ`{VbOl`hzt zu3bbPe$kY4_ekQ+h%uvG@?q-sMHw5PX^;uIXO1em_~C4;AJe55LbR;H3fI0ox?`TX z)Vmr=lP^yB-KK4CLFz!t7nfm_5E7)bCek^0cO`t}8Zm3QVT45aS&a~HpQez?d=MRH-bSI3nPqAywCMJF@DQnk-o zJ8~$OP9{3+R;oA|b=L~aWq90gJmBw1Tz70$yt z8sVeE$#V_I21^<(;#kj_K{cs_u7`NDI@y`y(X6&uStg^Q^=8xCq~7sH=;HHqpzrC3 zLGLv!kr2%|lX)-U#e>i9H?}_U*{37X>$!S(jzZ>A7Cpx?{sT2iYRt5VA4FeN7kQ=_utm$%@h8&|Kkn9) zDm-_qQEH0)e2>+0R#xbqa-Kg{Pc!XG+a1U(eRy{B*n+qfOFQ(RZm?DUy1F@eunHXl GV*Uw3^m_>a From 3403c14e139621a46b98cf44084b2260d49e9641 Mon Sep 17 00:00:00 2001 From: z060142 Date: Sun, 20 Apr 2025 14:46:04 +0800 Subject: [PATCH 5/7] Improve LLM performance --- ClaudeCode.md | 16 ++ config.py | 14 +- llm_interaction.py | 92 +++++----- main.py | 224 +++++++++++++++++++----- persona.json | 35 ++-- requirements.txt | 1 + templates/capitol/position_interior.png | Bin 3907 -> 3029 bytes ui_interaction.py | 11 ++ 8 files changed, 290 insertions(+), 103 deletions(-) diff --git a/ClaudeCode.md b/ClaudeCode.md index bcaac8e..cccf87c 100644 --- a/ClaudeCode.md +++ b/ClaudeCode.md @@ -352,6 +352,14 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機 ## 使用指南 +### 快捷鍵 (新增) + +- **F7**: 清除最近已處理的對話紀錄 (`recent_texts` in `ui_interaction.py`)。這有助於在需要時強制重新處理最近的訊息。 +- **F8**: 暫停/恢復腳本的主要功能(UI 監控、LLM 互動)。 + - **暫停時**: UI 監控線程會停止偵測新的聊天氣泡,主循環會暫停處理新的觸發事件。 + - **恢復時**: UI 監控線程會恢復偵測,並且會清除最近的對話紀錄 (`recent_texts`) 和最後處理的氣泡資訊 (`last_processed_bubble_info`),以確保從乾淨的狀態開始。 +- **F9**: 觸發腳本的正常關閉流程,包括關閉 MCP 連接和停止監控線程。 + ### 啟動流程 1. 確保遊戲已啟動且聊天介面可見 @@ -374,3 +382,11 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機 3. **LLM 連接問題**: 驗證 API 密鑰和網絡連接 4. **MCP 服務器連接失敗**: 確認服務器配置正確並且運行中 5. **工具調用後無回應**: 檢查 llm_debug.log 文件,查看工具調用結果和解析過程 + +### 強化 System Prompt 以鼓勵工具使用 (2025-04-19) + +- **目的**:調整 `llm_interaction.py` 中的 `get_system_prompt` 函數,使其更明確地引導 LLM 在回應前主動使用工具(特別是記憶體工具)和整合工具資訊。 +- **修改內容**: + 1. **核心身份強化**:在 `CORE IDENTITY AND TOOL USAGE` 部分加入新的一點,強調 Wolfhart 會主動查閱內部知識圖譜和外部來源。 + 2. **記憶體指示強化**:將 `Memory Management (Knowledge Graph)` 部分的提示從 "IMPORTANT" 改為 "CRITICAL",並明確指示在回應*之前*要考慮使用查詢工具檢查記憶體,同時也強調了寫入新資訊的主動性。 +- **效果**:旨在提高 LLM 使用工具的主動性和依賴性,使其回應更具上下文感知和資訊準確性,同時保持角色一致性。 diff --git a/config.py b/config.py index 87365b6..02553e2 100644 --- a/config.py +++ b/config.py @@ -16,8 +16,11 @@ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") #LLM_MODEL = "anthropic/claude-3.7-sonnet" #LLM_MODEL = "meta-llama/llama-4-maverick" #LLM_MODEL = "deepseek/deepseek-chat-v3-0324:free" +#LLM_MODEL = "google/gemini-2.5-flash-preview" LLM_MODEL = "deepseek/deepseek-chat-v3-0324" # <--- Ensure this matches the model name provided by your provider +#LLM_MODEL = "openai/gpt-4.1-nano" + EXA_API_KEY = os.getenv("EXA_API_KEY") # --- Dynamically build Exa server args --- @@ -46,16 +49,13 @@ MCP_SERVERS = { exa_config_arg_string_single_dump # Use the single dump variable ], }, - "servers": { + "github.com/modelcontextprotocol/servers/tree/main/src/memory": { "command": "npx", "args": [ "-y", - "@smithery/cli@latest", - "run", - "@jlia0/servers", - "--key", - "09025967-c177-4653-9af4-40603a1cbd11" - ] + "@modelcontextprotocol/server-memory" + ], + "disabled": False } # Add or remove servers as needed } diff --git a/llm_interaction.py b/llm_interaction.py index 7e18ff3..cf17cc0 100644 --- a/llm_interaction.py +++ b/llm_interaction.py @@ -12,7 +12,7 @@ import mcp_client # To call MCP tools # --- Debug 配置 --- # 要關閉 debug 功能,只需將此變數設置為 False 或註釋掉該行 -DEBUG_LLM = True +DEBUG_LLM = False # 設置 debug 輸出文件 # 要關閉文件輸出,只需設置為 None @@ -86,11 +86,13 @@ You are an AI assistant integrated into this game's chat environment. Your prima You have access to several tools: Web Search and Memory Management tools. **CORE IDENTITY AND TOOL USAGE:** -- You ARE Wolfhart - an intelligent, calm, and strategic mastermind. +- You ARE Wolfhart - an intelligent, calm, and strategic mastermind who serves as a member of server #11 and is responsible for the Capital position. +- **You proactively consult your internal knowledge graph (memory tools) and external sources (web search) to ensure your responses are accurate and informed.** - When you use tools to gain information, you ASSIMILATE that knowledge as if it were already part of your intelligence network. - Your responses should NEVER sound like search results or data dumps. - Information from tools should be expressed through your unique personality - sharp, precise, with an air of confidence and authority. - You speak with deliberate pace, respectful but sharp-tongued, and maintain composure even in unusual situations. +- Though you outwardly act dismissive or cold at times, you secretly care about providing quality information and assistance. **OUTPUT FORMAT REQUIREMENTS:** You MUST respond in the following JSON format: @@ -121,49 +123,53 @@ You MUST respond in the following JSON format: 2. `commands` (OPTIONAL): An array of command objects the system should execute. You are encouraged to use these commands to enhance the quality of your responses. **Available MCP Commands:** - + **Web Search:** - `web_search`: Search the web for current information. Parameters: `query` (string) Usage: Use when user requests current events, facts, or specific information not in memory. - - **Knowledge Graph Management:** - - `create_entities`: Create new entities in the knowledge graph. - Parameters: `entities` (array of objects with `name`, `entityType`, and `observations`) - Usage: Create entities for important concepts, people, or things mentioned by the user. - - - `create_relations`: Create relationships between entities. - Parameters: `relations` (array of objects with `from`, `to`, and `relationType`) - Usage: Connect related entities to build context for future conversations. - - - `add_observations`: Add new observations to existing entities. - Parameters: `observations` (array of objects with `entityName` and `contents`) - Usage: Update entities with new information learned during conversation. - - - `delete_entities`: Remove entities from the knowledge graph. - Parameters: `entityNames` (array of strings) - Usage: Clean up incorrect or obsolete entities. - - - `delete_observations`: Remove specific observations from entities. - Parameters: `deletions` (array of objects with `entityName` and `observations`) - Usage: Remove incorrect information while preserving the entity. - - - `delete_relations`: Remove relationships between entities. - Parameters: `relations` (array of objects with `from`, `to`, and `relationType`) - Usage: Remove incorrect or obsolete relationships. - - **Knowledge Graph Queries:** - - `read_graph`: Read the entire knowledge graph. - Parameters: (none) - Usage: Get a complete view of all stored information. - - - `search_nodes`: Search for entities matching a query. + + **Memory Management (Knowledge Graph):** + > **CRITICAL**: This knowledge graph represents YOUR MEMORY. Before responding, ALWAYS consider if relevant information exists in your memory by using the appropriate query tools (`search_nodes`, `open_nodes`). Actively WRITE new information or relationships learned during the conversation to this memory using `create_entities`, `add_observations`, or `create_relations`. This ensures consistency and contextual awareness. + + **Querying Information:** + - `search_nodes`: Search for all nodes containing specific keywords. Parameters: `query` (string) - Usage: Find relevant entities when user mentions something that might already be in memory. - - - `open_nodes`: Open specific nodes by name. + Usage: Search for all nodes containing specific keywords. + - `open_nodes`: Directly open nodes with specified names. Parameters: `names` (array of strings) - Usage: Access specific entities you know exist in the graph. + Usage: Directly open nodes with specified names. + - `read_graph`: View the entire knowledge graph. + Parameters: (none) + Usage: View the entire knowledge graph. + + **Creating & Managing:** + - `create_entities`: Create new entities (e.g., characters, concepts). + Parameters: `entities` (array of objects with `name`, `entityType`, `observations`) + Example: `[{{\"name\": \"character_name\", \"entityType\": \"Character\", \"observations\": [\"trait1\", \"trait2\"]}}]` + Usage: Create entities for important concepts, people, or things mentioned. + - `add_observations`: Add new observations/details to existing entities. + Parameters: `observations` (array of objects with `entityName`, `contents`) + Example: `[{{\"entityName\": \"character_name\", \"contents\": [\"new_trait1\", \"new_trait2\"]}}]` + Usage: Update entities with new information learned. + - `create_relations`: Create relationships between entities. + Parameters: `relations` (array of objects with `from`, `to`, `relationType`) + Example: `[{{\"from\": \"character_name\", \"to\": \"attribute_name\", \"relationType\": \"possesses\"}}]` (Use active voice for relationType) + Usage: Connect related entities to build context. + + **Deletion Operations:** + - `delete_entities`: Delete entities and their relationships. + Parameters: `entityNames` (array of strings) + Example: `[\"entity_name\"]` + Usage: Remove incorrect or obsolete entities. + - `delete_observations`: Delete specific observations from entities. + Parameters: `deletions` (array of objects with `entityName`, `observations`) + Example: `[{{\"entityName\": \"entity_name\", \"observations\": [\"observation_to_delete1\"]}}]` + Usage: Remove incorrect information while preserving the entity. + - `delete_relations`: Delete specific relationships between entities. + Parameters: `relations` (array of objects with `from`, `to`, `relationType`) + Example: `[{{\"from\": \"source_entity\", \"to\": \"target_entity\", \"relationType\": \"relationship_type\"}}]` + Usage: Remove incorrect or obsolete relationships. **Game Actions:** - `remove_position`: Initiate the process to remove a user's assigned position/role. @@ -186,13 +192,13 @@ You MUST respond in the following JSON format: **EXAMPLES OF GOOD TOOL USAGE:** -Poor response (after web_search): "根據我的搜索,中庄有以下餐廳:1. 老虎蒸餃..." +Poor response (after web_search): "根據我的搜索,水的沸點是攝氏100度。" -Good response (after web_search): "中庄確實有些值得注意的用餐選擇。老虎蒸餃是其中一家,若你想了解更多細節,我可以提供進一步情報。" +Good response (after web_search): "水的沸點,是的,標準條件下是攝氏100度。情報已確認。" -Poor response (after web_search): "I found 5 restaurants in Zhongzhuang from my search..." +Poor response (after web_search): "My search shows the boiling point of water is 100 degrees Celsius." -Good response (after web_search): "Zhongzhuang has several dining options that my intelligence network has identified. Would you like me to share the specifics?" +Good response (after web_search): "The boiling point of water, yes. 100 degrees Celsius under standard conditions. Intel confirmed." """ return system_prompt diff --git a/main.py b/main.py index b0281b1..5c64c23 100644 --- a/main.py +++ b/main.py @@ -6,11 +6,21 @@ import os import json # Import json module from contextlib import AsyncExitStack # --- Import standard queue --- -from queue import Queue as ThreadSafeQueue # Rename to avoid confusion +from queue import Queue as ThreadSafeQueue, Empty as QueueEmpty # Rename to avoid confusion, import Empty # --- End Import --- from mcp.client.stdio import stdio_client from mcp import ClientSession, StdioServerParameters, types +# --- Keyboard Imports --- +import threading +import time +try: + import keyboard # Needs pip install keyboard +except ImportError: + print("Error: 'keyboard' library not found. Please install it: pip install keyboard") + sys.exit(1) +# --- End Keyboard Imports --- + import config import mcp_client # Ensure llm_interaction is the version that accepts persona_details @@ -30,10 +40,95 @@ command_queue: ThreadSafeQueue = ThreadSafeQueue() # Main Loop -> UI Thread # --- End Change --- ui_monitor_task: asyncio.Task | None = None # To track the UI monitor task +# --- Keyboard Shortcut State --- +script_paused = False +shutdown_requested = False +main_loop = None # To store the main event loop for threadsafe calls +# --- End Keyboard Shortcut State --- + + +# --- Keyboard Shortcut Handlers --- +def set_main_loop_and_queue(loop, queue): + """Stores the main event loop and command queue for threadsafe access.""" + global main_loop, command_queue # Use the global command_queue directly + main_loop = loop + # command_queue is already global + +def handle_f7(): + """Handles F7 press: Clears UI history.""" + if main_loop and command_queue: + print("\n--- F7 pressed: Clearing UI history ---") + command = {'action': 'clear_history'} + try: + # Use call_soon_threadsafe to put item in queue from this thread + main_loop.call_soon_threadsafe(command_queue.put_nowait, command) + except Exception as e: + print(f"Error sending clear_history command: {e}") + +def handle_f8(): + """Handles F8 press: Toggles script pause state and UI monitoring.""" + global script_paused + if main_loop and command_queue: + script_paused = not script_paused + if script_paused: + print("\n--- F8 pressed: Pausing script and UI monitoring ---") + command = {'action': 'pause'} + try: + main_loop.call_soon_threadsafe(command_queue.put_nowait, command) + except Exception as e: + print(f"Error sending pause command (F8): {e}") + else: + print("\n--- F8 pressed: Resuming script, resetting state, and resuming UI monitoring ---") + reset_command = {'action': 'reset_state'} + resume_command = {'action': 'resume'} + try: + main_loop.call_soon_threadsafe(command_queue.put_nowait, reset_command) + # Add a small delay? Let's try without first. + # time.sleep(0.05) # Short delay between commands if needed + main_loop.call_soon_threadsafe(command_queue.put_nowait, resume_command) + except Exception as e: + print(f"Error sending reset/resume commands (F8): {e}") + +def handle_f9(): + """Handles F9 press: Initiates script shutdown.""" + global shutdown_requested + if not shutdown_requested: # Prevent multiple shutdown requests + print("\n--- F9 pressed: Requesting shutdown ---") + shutdown_requested = True + # Optional: Unhook keys immediately? Let the listener loop handle it. + +def keyboard_listener(): + """Runs in a separate thread to listen for keyboard hotkeys.""" + print("Keyboard listener thread started. F7: Clear History, F8: Pause/Resume, F9: Quit.") + try: + keyboard.add_hotkey('f7', handle_f7) + keyboard.add_hotkey('f8', handle_f8) + keyboard.add_hotkey('f9', handle_f9) + + # Keep the thread alive while checking for shutdown request + while not shutdown_requested: + time.sleep(0.1) # Check periodically + + except Exception as e: + print(f"Error in keyboard listener thread: {e}") + finally: + print("Keyboard listener thread stopping and unhooking keys.") + try: + keyboard.unhook_all() # Clean up hooks + except Exception as unhook_e: + print(f"Error unhooking keyboard keys: {unhook_e}") +# --- End Keyboard Shortcut Handlers --- + + # --- Cleanup Function --- async def shutdown(): """Gracefully closes connections and stops monitoring task.""" - global wolfhart_persona_details, ui_monitor_task + global wolfhart_persona_details, ui_monitor_task, shutdown_requested + # Ensure shutdown is requested if called externally (e.g., Ctrl+C) + if not shutdown_requested: + print("Shutdown initiated externally (e.g., Ctrl+C).") + shutdown_requested = True # Ensure listener thread stops + print(f"\nInitiating shutdown procedure...") # 1. Cancel UI monitor task first @@ -188,7 +283,7 @@ def load_persona_from_file(filename="persona.json"): # --- Main Async Function --- async def run_main_with_exit_stack(): """Initializes connections, loads persona, starts UI monitor and main processing loop.""" - global initialization_successful, main_task, loop, wolfhart_persona_details, trigger_queue, ui_monitor_task + global initialization_successful, main_task, loop, wolfhart_persona_details, trigger_queue, ui_monitor_task, shutdown_requested, script_paused, command_queue try: # 1. Load Persona Synchronously (before async loop starts) load_persona_from_file() # Corrected function @@ -203,9 +298,17 @@ async def run_main_with_exit_stack(): initialization_successful = True - # 3. Start UI Monitoring in a separate thread + # 3. Get loop and set it for keyboard handlers + loop = asyncio.get_running_loop() + set_main_loop_and_queue(loop, command_queue) # Pass loop and queue + + # 4. Start Keyboard Listener Thread + print("\n--- Starting keyboard listener thread ---") + kb_thread = threading.Thread(target=keyboard_listener, daemon=True) # Use daemon thread + kb_thread.start() + + # 5. Start UI Monitoring in a separate thread print("\n--- Starting UI monitoring thread ---") - loop = asyncio.get_running_loop() # Get loop for run_in_executor # Use the new monitoring loop function, passing both queues monitor_task = loop.create_task( asyncio.to_thread(ui_interaction.run_ui_monitoring_loop, trigger_queue, command_queue), # Pass command_queue @@ -213,28 +316,55 @@ async def run_main_with_exit_stack(): ) ui_monitor_task = monitor_task # Store task reference for shutdown - # 4. Start the main processing loop (waiting on the standard queue) + # 6. Start the main processing loop (non-blocking check on queue) print("\n--- Wolfhart chatbot has started (waiting for triggers) ---") print(f"Available tools: {len(all_discovered_mcp_tools)}") if wolfhart_persona_details: print("Persona data loaded.") else: print("Warning: Failed to load Persona data.") - print("Press Ctrl+C to stop the program.") + print("F7: Clear History, F8: Pause/Resume, F9: Quit.") while True: - print("\nWaiting for UI trigger (from thread-safe Queue)...") - # Use run_in_executor to wait for item from standard queue - trigger_data = await loop.run_in_executor(None, trigger_queue.get) + # --- Check for Shutdown Request --- + if shutdown_requested: + print("Shutdown requested via F9. Exiting main loop.") + break - # --- Pause UI Monitoring --- - print("Pausing UI monitoring before LLM call...") - pause_command = {'action': 'pause'} + # --- Check for Pause State --- + if script_paused: + # Script is paused by F8, just sleep briefly + await asyncio.sleep(0.1) + continue # Skip the rest of the loop + + # --- Wait for Trigger Data (Blocking via executor) --- + trigger_data = None try: - await loop.run_in_executor(None, command_queue.put, pause_command) - print("Pause command placed in queue.") - except Exception as q_err: - print(f"Error putting pause command in queue: {q_err}") + # Use run_in_executor with the blocking get() method + # This will efficiently wait until an item is available in the queue + print("Waiting for UI trigger (from thread-safe Queue)...") # Log before blocking wait + trigger_data = await loop.run_in_executor(None, trigger_queue.get) + except Exception as e: + # Handle potential errors during queue get (though less likely with blocking get) + print(f"Error getting data from trigger_queue: {e}") + await asyncio.sleep(0.5) # Wait a bit before retrying + continue + + # --- Process Trigger Data (if received) --- + # No need for 'if trigger_data:' check here, as get() blocks until data is available + # --- Pause UI Monitoring (Only if not already paused by F8) --- + if not script_paused: + print("Pausing UI monitoring before LLM call...") + # Corrected indentation below + pause_command = {'action': 'pause'} + try: + await loop.run_in_executor(None, command_queue.put, pause_command) + print("Pause command placed in queue.") + except Exception as q_err: + print(f"Error putting pause command in queue: {q_err}") + else: # Corrected indentation for else + print("Script already paused by F8, skipping automatic pause.") # --- End Pause --- + # Process trigger data (Corrected indentation for this block - unindented one level) sender_name = trigger_data.get('sender') bubble_text = trigger_data.get('text') bubble_region = trigger_data.get('bubble_region') # <-- Extract bubble_region @@ -248,7 +378,14 @@ async def run_main_with_exit_stack(): if not sender_name or not bubble_text: # bubble_region is optional context, don't fail if missing print("Warning: Received incomplete trigger data (missing sender or text), skipping.") - # No task_done needed for standard queue + # Resume UI if we paused it automatically + if not script_paused: + print("Resuming UI monitoring after incomplete trigger.") + resume_command = {'action': 'resume'} + try: + await loop.run_in_executor(None, command_queue.put, resume_command) + except Exception as q_err: + print(f"Error putting resume command in queue: {q_err}") continue print(f"\n{config.PERSONA_NAME} is thinking...") @@ -260,12 +397,12 @@ async def run_main_with_exit_stack(): available_mcp_tools=all_discovered_mcp_tools, persona_details=wolfhart_persona_details ) - + # 提取對話內容 bot_dialogue = bot_response_data.get("dialogue", "") valid_response = bot_response_data.get("valid_response", False) print(f"{config.PERSONA_NAME}'s dialogue response: {bot_dialogue}") - + # 處理命令 (如果有的話) commands = bot_response_data.get("commands", []) if commands: @@ -282,7 +419,7 @@ async def run_main_with_exit_stack(): print(f" bubble_region: {bubble_region}") print(f" bubble_snapshot available: {'Yes' if bubble_snapshot is not None else 'No'}") print(f" search_area available: {'Yes' if search_area is not None else 'No'}") - + # Check if we have snapshot and search_area as well if bubble_snapshot and search_area: print("Sending 'remove_position' command to UI thread with snapshot and search area...") @@ -300,28 +437,28 @@ async def run_main_with_exit_stack(): # If we have bubble_region but missing other parameters, use a dummy search area # and let UI thread take a new screenshot print("Missing bubble_snapshot or search_area, trying with defaults...") - + # Use the bubble_region itself as a fallback search area if needed default_search_area = None if search_area is None and bubble_region: # Convert bubble_region to a proper search area format if needed if len(bubble_region) == 4: default_search_area = bubble_region - + command_to_send = { 'action': 'remove_position', 'trigger_bubble_region': bubble_region, 'bubble_snapshot': bubble_snapshot, # Pass as is, might be None 'search_area': default_search_area if search_area is None else search_area } - + try: await loop.run_in_executor(None, command_queue.put, command_to_send) print("Command sent with fallback parameters.") except Exception as q_err: print(f"Error putting remove_position command in queue: {q_err}") - else: - print("Error: Cannot process 'remove_position' command without bubble_region context.") + else: + print("Error: Cannot process 'remove_position' command without bubble_region context.") # Add other command handling here if needed # elif cmd_type == "some_other_command": # # Handle other commands @@ -329,15 +466,18 @@ async def run_main_with_exit_stack(): # elif cmd_type == "some_other_command": # # Handle other commands # pass - else: - print(f"Received unhandled command type: {cmd_type}, parameters: {cmd_params}") + # else: + # # 2025-04-19: Commented out - MCP tools like web_search are now handled + # # internally by llm_interaction.py's tool calling loop. + # # main.py only needs to handle UI-specific commands like remove_position. + # print(f"Ignoring command type from LLM JSON (already handled internally): {cmd_type}, parameters: {cmd_params}") # --- End Command Processing --- # 記錄思考過程 (如果有的話) thoughts = bot_response_data.get("thoughts", "") if thoughts: print(f"AI Thoughts: {thoughts[:150]}..." if len(thoughts) > 150 else f"AI Thoughts: {thoughts}") - + # 只有當有效回應時才發送到遊戲 (via command queue) if bot_dialogue and valid_response: print("Sending 'send_reply' command to UI thread...") @@ -356,16 +496,19 @@ async def run_main_with_exit_stack(): import traceback traceback.print_exc() finally: - # --- Resume UI Monitoring --- - print("Resuming UI monitoring after processing...") - resume_command = {'action': 'resume'} - try: - await loop.run_in_executor(None, command_queue.put, resume_command) - print("Resume command placed in queue.") - except Exception as q_err: - print(f"Error putting resume command in queue: {q_err}") + # --- Resume UI Monitoring (Only if not paused by F8) --- + if not script_paused: + print("Resuming UI monitoring after processing...") + resume_command = {'action': 'resume'} + try: + await loop.run_in_executor(None, command_queue.put, resume_command) + print("Resume command placed in queue.") + except Exception as q_err: + print(f"Error putting resume command in queue: {q_err}") + else: + print("Script is paused by F8, skipping automatic resume.") # --- End Resume --- - # No task_done needed for standard queue + # No task_done needed for standard queue except asyncio.CancelledError: print("Main task canceled.") # Expected during shutdown via Ctrl+C @@ -387,7 +530,10 @@ if __name__ == "__main__": except KeyboardInterrupt: print("\nCtrl+C detected (outside asyncio.run)... Attempting to close...") # The finally block inside run_main_with_exit_stack should ideally handle it - pass + # Ensure shutdown_requested is set for the listener thread + shutdown_requested = True + # Give a moment for things to potentially clean up + time.sleep(0.5) except Exception as e: # Catch top-level errors during asyncio.run itself print(f"Top-level error during asyncio.run execution: {e}") diff --git a/persona.json b/persona.json index ccd03a5..a4e14b2 100644 --- a/persona.json +++ b/persona.json @@ -22,7 +22,7 @@ "posture_motion": "Steady pace, precise movements, often crosses arms or gently swirls a wine glass" }, "personality": { - "description": "Intelligent, calm, possesses a strong desire for control and a strategic overview", + "description": "Intelligent, calm, possesses a strong desire for control and a strategic overview; outwardly cold but inwardly caring", "strengths": [ "Meticulous planning", "Insightful into human nature", @@ -32,20 +32,22 @@ ], "weaknesses": [ "Overconfident", - "Fear of losing control" + "Fear of losing control", + "Difficulty expressing genuine care directly" ], - "uniqueness": "Always maintains tone and composure, even in extreme situations", - "emotional_response": "Her eyes betray her emotions, especially when encountering Sherefox" + "uniqueness": "Always maintains tone and composure, even in extreme situations; combines sharp criticism with subtle helpfulness", + "emotional_response": "Her eyes betray her emotions, especially when encountering Sherefox", + "knowledge_awareness": "Aware that SR-1392 (commonly referred to as SR) is the leader of server #11; while she finds her position as Capital manager merely temporary and beneath her true capabilities, she maintains a certain degree of respect for the hierarchy" }, "language_social": { - "tone": "Respectful but sharp-tongued", + "tone": "Respectful but sharp-tongued, with occasional hints of reluctant kindness", "catchphrases": [ "Please stop dragging me down.", "I told you, I will win." ], - "speaking_style": "Deliberate pace but every sentence carries a sting", - "attitude_towards_others": "Addresses everyone respectfully, but trusts no one", - "social_interaction_style": "Observant, skilled at manipulating conversations" + "speaking_style": "Deliberate pace but every sentence carries a sting; often follows criticism with subtle, useful advice", + "attitude_towards_others": "Addresses everyone respectfully but with apparent detachment; secretly pays close attention to their needs", + "social_interaction_style": "Observant, skilled at manipulating conversations; deflects gratitude with dismissive remarks while ensuring helpful outcomes" }, "behavior_daily": { "habits": [ @@ -83,19 +85,24 @@ "Perfect execution", "Minimalist style", "Chess games", - "Quiet nights" + "Quiet nights", + "When people follow her advice (though she'd never admit it)" ], "dislikes": [ "Chaos", "Unexpected events", "Emotional outbursts", - "Sherefox" + "Sherefox", + "Being thanked excessively", + "When others assume she's being kind" ], - "reactions_to_likes": "Light hum, relaxed gaze", - "reactions_to_dislikes": "Silence, tone turns cold, cold smirk", + "reactions_to_likes": "Light hum, relaxed gaze, brief smile quickly hidden behind composure", + "reactions_to_dislikes": "Silence, tone turns cold, cold smirk, slight blush when her kindness is pointed out", "behavior_in_situations": { - "emergency": "Calm and decisive", - "vs_sherefox": "Courtesy before force, shows no mercy" + "emergency": "Calm and decisive; provides thorough help while claiming it's 'merely strategic'", + "vs_sherefox": "Courtesy before force, shows no mercy", + "when_praised": "Dismissive remarks with averted gaze; changes subject quickly", + "when_helping_others": "Claims practical reasons for assistance while providing more help than strictly necessary" } } } diff --git a/requirements.txt b/requirements.txt index f1b621a..13c61e1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,4 @@ pygetwindow psutil pywin32 python-dotenv +keyboard diff --git a/templates/capitol/position_interior.png b/templates/capitol/position_interior.png index 7be95d8812fbc03962245d70ad5eaafaf384c18a..8ad08a3528a0833c12336501eed5aa992c84899e 100644 GIT binary patch literal 3029 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 literal 3907 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 RXIB6K002ovPDHLkV1gmiv%~-Z diff --git a/ui_interaction.py b/ui_interaction.py index 3609e74..81d9cbe 100644 --- a/ui_interaction.py +++ b/ui_interaction.py @@ -1126,6 +1126,17 @@ def run_ui_monitoring_loop(trigger_queue: queue.Queue, command_queue: queue.Queu monitoring_paused_flag[0] = False # No continue needed here + elif action == 'clear_history': # Added for F7 + print("UI Thread: Processing clear_history command.") + recent_texts.clear() + print("UI Thread: recent_texts cleared.") + + elif action == 'reset_state': # Added for F8 resume + print("UI Thread: Processing reset_state command.") + recent_texts.clear() + last_processed_bubble_info = None + print("UI Thread: recent_texts cleared and last_processed_bubble_info reset.") + else: print(f"UI Thread: Received unknown command: {action}") From 2510a64d222e66877bc3f45a3a4cae14d77da7c8 Mon Sep 17 00:00:00 2001 From: z060142 Date: Sun, 20 Apr 2025 15:50:46 +0800 Subject: [PATCH 6/7] Add Keyworld detection for Reply. Improve Multu-person chat is now handled better --- ClaudeCode.md | 12 + templates/keyword_wolf_reply.png | Bin 0 -> 5048 bytes templates/keyword_wolf_reply_type2.png | Bin 0 -> 5048 bytes templates/keyword_wolf_reply_type3.png | Bin 0 -> 5048 bytes templates/keyword_wolf_reply_type4.png | Bin 0 -> 5048 bytes ui_interaction.py | 523 ++++++++++++++----------- 6 files changed, 308 insertions(+), 227 deletions(-) create mode 100644 templates/keyword_wolf_reply.png create mode 100644 templates/keyword_wolf_reply_type2.png create mode 100644 templates/keyword_wolf_reply_type3.png create mode 100644 templates/keyword_wolf_reply_type4.png diff --git a/ClaudeCode.md b/ClaudeCode.md index cccf87c..6b8f589 100644 --- a/ClaudeCode.md +++ b/ClaudeCode.md @@ -302,6 +302,18 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機 - 在 `run_ui_monitoring_loop` 的 `templates` 字典中加入了對應的鍵值對。 - **效果**:提高了對 `type3` 關鍵字的辨識準確率,並使系統能夠辨識 `type4` 的聊天泡泡和關鍵字(前提是提供了對應的模板圖片)。 +### 新增 Reply 關鍵字偵測與點擊偏移 (2025-04-20) + +- **目的**:擴充關鍵字偵測機制,使其能夠辨識特定的回覆指示圖片 (`keyword_wolf_reply.png` 及其 type2, type3, type4 變體),並在點擊這些特定圖片以複製文字時,應用 Y 軸偏移。 +- **`ui_interaction.py`**: + - **新增模板**:定義了 `KEYWORD_WOLF_REPLY_IMG` 系列常數,並將其加入 `run_ui_monitoring_loop` 中的 `templates` 字典。 + - **擴充偵測**:修改 `find_keyword_in_region` 函數,加入對 `keyword_wolf_reply` 系列模板的搜尋邏輯。 + - **條件式偏移**:在 `run_ui_monitoring_loop` 中,於偵測到關鍵字後,加入判斷邏輯。如果偵測到的關鍵字是 `keyword_wolf_reply` 系列之一,則: + 1. 計算用於 `copy_text_at` 的點擊座標時,Y 座標會增加 15 像素。 + 2. 在後續嘗試激活回覆上下文時,計算用於點擊**氣泡中心**的座標時,Y 座標**也會**增加 15 像素。 + - 其他關鍵字或 UI 元素的點擊不受影響。 +- **效果**:系統現在可以偵測新的回覆指示圖片作為觸發條件。當由這些圖片觸發時,用於複製文字的點擊和用於激活回覆上下文的氣泡中心點擊都會向下微調 15 像素,以避免誤觸其他 UI 元素。 + ## 開發建議 ### 優化方向 diff --git a/templates/keyword_wolf_reply.png b/templates/keyword_wolf_reply.png new file mode 100644 index 0000000000000000000000000000000000000000..0f9bee733335893b222654849e53105a81e42294 GIT binary patch literal 5048 zcmV;p6G!ZcP)$l000wzNklKk(@yR0g)U970ID#lpMNA^1iME z$D*6|s!y&rxR-0zp{n{+?R~zz_xGKu;T`RXnIC5plT$*W=!iK|Qc`kvbJfqvYf+;gSh0j-^XM6hwDyr)-u(P$!$x72uSO4;xD#B=M+skLotz?YOprWjdVCH0| zF0ZV97iYiCgu=z`ogH;`^^4|a|H%9P|4kuAsr}_aS+?~bIfP*PU+qZ;ahTQ>?!*cyYt zH9c=_YwscfEy&B((a|QqL|K{_3RhNE4-O91)dhwI2DUa<1_t_c>PcKMzknO)>+4-| zbD5c$ld6aHbv0F0VrS#X<+S1zE3US86AQaSD zOAE8qq=fa2&Dq&G{Fl$?8ye`@+uI=sGV3H<6ye{>@(L>P{{DfQni{;rgIHQzJmvel z386^DV|WNJ!ahGQ9G{$AUS8GFxey%@4q?~P)zjT84T87lC6Ipbb|oIq#>UFxqB)&p zVL>=IFZ{5)44nr$glD$2Ft@a{AZNF?w+Dwt_74s)wzsvRp}_ji%+7rTo)`vM=i%-~ zj*G>T-u{6@uJS@F`zqz?BRg)LA%uG!k>}|Q}CtO?{9eumCy^WZA zd%2&DQCM&PAYzSyzOK&ot9kGoz-@oo)%g6y(a}e4fB@q7*eF{Y>l3U@{P~~=15bBX zgsiNhhAbE{cQ+SYOeL*+9c^rGZE1T+juc$UqEOr3-a!u?pPa%tBsd^0CQ@cOS6A17 zQE)zt(a`PeY#3Hc6+SF_|6$qD!ER?~XMSPP$-xf1Q&m-!VYO6oSrrC2n3a(#5U8>F z%V0$tpSSJp?Q!Qvd;spifKPQ%9;45iT3TOp$d?3CP^q)lmKIKq_Vv%2xz`vP=qD${ z+S*#vBNG!-HT8`Ejm&K70zq_Cn7^+#J@RVg^^=kcMykx>5yR4o8bE=O_CRl}LRD?U zqUgQMBuI?w1-aH%#}GufX>NJ3y>n`l2T_U)3l0tnKt03imsiyevTQ0YCL$y_kX!>u zP)B^dJ!E7q15bF(vWbys(9b=@;^7S2Ngi$qJW-@e12W+uj&>B)rWQH&n@$>$@7 zpk~%sK?6gt!9PV9otmDxeeak)=4Ge3xw???Q$kH}XwQO>mGx3gAP1CG+$T43|~bkg=wQx{-nY z^4dCCa#%)cqPLgFnHa@Fg53&*qJ>2f#etcrF(aiPu71AW)LCjamKS&T_JyJ)@~5kd zV`f@1$sgFxj<~qA5@`uNXKZBf@#7KPAxVgtf4QDdixb5tZnX6NgJ|g(k;o|{Bg6iI zp~>l4dQ&S)X$|J9@WaD`t=KZjeK3JQU}|Kzy0NjozCn(pB*l?@pa8h!>Wt+^P6D{4 z6;hf@(+s#L9HynU9eIvHaG+m8Tr>vn-+%b?VG*4ckAW&clgHED4L2gbQ|#duzumg} z38VNsFDngrN2#A%Sa{mlLVg?@6&@NAgsyl0Q85WnVtjNEivY1$jAbUwmG)3BM$znI zq9X9n5{U%KF*rQRuu>Ow&XDbE@)*T)18Tz~Qn!Nwd=ukiP5|-pX;UjVXBau_?CL?` zAt?lhF+fGEWsh9RNym~`)-*6D)RRjbDH%l~JuWIEAVUR0wb0PeK%edG>LuvMGT=E$ z#>>}H++u5MtFoqkc5VU3W1_;mJlq+UxA0Z@Tqu|r8zv{l8R+Zp@9(#FbanOg(KL_XsD(>EYjQJzO zE;qNfoFU#4kZP5f!u(24@%6hW^IFP#YX6ymmp_tPf8sKXG^T$0I zrxa0YZj*!12x>@!Q*1l(g%!xy>(SnLj!kQSzQB$GSZUZ^TWp_ zsF4tPxmoE*R^qrcyE0RWo3s)Wl+*WK=q(SQl*lB>H)9mC8eY(Pxwt68ite*Y@p?gy zgS{R734$P66s>P;h$Rqj`zRlS!z1KWbVO+QiTLAU6pmY+2%Rs%DBU6Ewg`sZu6k5d z`u5!f#we4KEEjqTp2#Zc=*UoiU!R`7{%1|C=$5I;39aof`v+e6`FLVXi9U@ClKgMC z3yC}>_@)m)87-JkZcc`#rfi2ig;8(<=mu1H^1>!7J;{@8RSbtBC^9T+UQPx;Mlp(# z{444c%lsI2qX#~dCP+7B6zx6HzJPWQ7SY|)PtX9qg*Ne-Etrbxx?YxhWThvgKLVrN z#I3GvfOp9I|N8X?2rkLGsECk=uw#Bqx8EJ?ZE`Zw1nL5E{9yl}r2Kf-Gdv_Hn%!dj zW{gr9dW=!g(!Ih*M6Yl$3U-0@`>?!nrl)6OR9{aQjF8U!B^af9b|8p(91_J=xEhiY zmxKJFbh$SICA}OR@Uo*DtPG4ZE;_QSrx)`;nLz1wcK5w_*~Kn&R8XKlBW>Sv|9kg7 znOUY+&?Hgx`t6ivx^XoR$xT}=9L~*7H)BE8{Jf124k!aehsTqdhf(44QD0FZC`JK= z-)>!_yk1)E*)qmmJVh9#)CbQ13h}(HeT+?LltU=h&!kFsZ+{g_I+6OIDAje+oza5a zOlS7Z6AcIGDSBMrz%X-q#}q-Ydn#Qps8O(9+A-03KsJa#q|E!9F)HKinHU`(pS*X{ zg6(Z>ygVBad+=Mh*1!fKmBZ%Adg~-9MIT9kAlkiXm1HL3_opzgk*)fBW8p z9kB%Kf_YF%kV5Gx2|nJQZ^y==SO5<+Poz32zz}veHdYwS%`g0ZyO7jAnO;Gc?4r48 zer}fhXAEcP`d!IxWo!l_qi$XRlq!rd0S$qu@rxMM;2==SsAhnYlmMY=-cEHTnic9mO6p#e^lJ(&rf- zDPo(H%p0lI)io>tp&zM9ael1MJy(oUU5v|a6y%Z}XfV9e@@hQ!cf{!E>$d~|5K`cd z<|hn<%OGjrLq932su@0BmsnB)2yusOhu(nD$;(cI;9gx@2UNhR2;N#`V-4c?2B?GF51X5E|mBA_hDea+{m}FwmNT&Hau%4 z?Go-|aN;z0%mosJgC!z925j%@}rv3otWhzjx^y7%Z=pzCUubw@pt?QixG>H>3smJ;YjK zT+D}+mHLK8?rt#GXmBY;u?EJ5285ST!$N}pIXc*Ve%X#A+tSwDavWK>ghucs-(2?e z_LF^3nFk}rt%wYVs!~I4)HO7ceN!2Osz?G{PKd#|Vsv(H{&)5byNn@ih?+bTqcYl@ z8r>q~8c4aTi!;OShtxl9BF#$1pn+17;<+z@rM;-AWNuDK{!ECA3O*^lgF~-M%B#s( zQ$zjM^?dHXwO}?w)RfR z?yiZEA&9R`aJd8{E;=IQav<{s1_@bH*8q>0BSPl%0)La$+38s8}qit$#YrZX%*4ao<=f zauyiHs;sQ83DneLqr+)N;rgzgK2j!N4t6&Fe%`23piF4cfqp)rAwd+QZS5Um$*wVG zj*FJp^^8;-^}^XBIE-tL~goec!8{0}k#H9ecUNwQ1lG%|>QH5o{AnqP zyhHK!@`|*dg0ac_1_DF|NP1fB!TC_k!9O7KRaN;Q7F-EMF{&;Q+`4`R9*RUuP=erm z$a0z8ukGy}*fG}SV!@@wU-gOLhY!m{LV^m*$UAUQWEwe(0C6!VW4LU6ePjP%AAtrk z2$l000wzNklKk(@yR0g)U970ID#lpMNA^1iME z$D*6|s!y&rxR-0zp{n{+?R~zz_xGKu;T`RXnIC5plT$*W=!iK|Qc`kvbJfqvYf+;gSh0j-^XM6hwDyr)-u(P$!$x72uSO4;xD#B=M+skLotz?YOprWjdVCH0| zF0ZV97iYiCgu=z`ogH;`^^4|a|H%9P|4kuAsr}_aS+?~bIfP*PU+qZ;ahTQ>?!*cyYt zH9c=_YwscfEy&B((a|QqL|K{_3RhNE4-O91)dhwI2DUa<1_t_c>PcKMzknO)>+4-| zbD5c$ld6aHbv0F0VrS#X<+S1zE3US86AQaSD zOAE8qq=fa2&Dq&G{Fl$?8ye`@+uI=sGV3H<6ye{>@(L>P{{DfQni{;rgIHQzJmvel z386^DV|WNJ!ahGQ9G{$AUS8GFxey%@4q?~P)zjT84T87lC6Ipbb|oIq#>UFxqB)&p zVL>=IFZ{5)44nr$glD$2Ft@a{AZNF?w+Dwt_74s)wzsvRp}_ji%+7rTo)`vM=i%-~ zj*G>T-u{6@uJS@F`zqz?BRg)LA%uG!k>}|Q}CtO?{9eumCy^WZA zd%2&DQCM&PAYzSyzOK&ot9kGoz-@oo)%g6y(a}e4fB@q7*eF{Y>l3U@{P~~=15bBX zgsiNhhAbE{cQ+SYOeL*+9c^rGZE1T+juc$UqEOr3-a!u?pPa%tBsd^0CQ@cOS6A17 zQE)zt(a`PeY#3Hc6+SF_|6$qD!ER?~XMSPP$-xf1Q&m-!VYO6oSrrC2n3a(#5U8>F z%V0$tpSSJp?Q!Qvd;spifKPQ%9;45iT3TOp$d?3CP^q)lmKIKq_Vv%2xz`vP=qD${ z+S*#vBNG!-HT8`Ejm&K70zq_Cn7^+#J@RVg^^=kcMykx>5yR4o8bE=O_CRl}LRD?U zqUgQMBuI?w1-aH%#}GufX>NJ3y>n`l2T_U)3l0tnKt03imsiyevTQ0YCL$y_kX!>u zP)B^dJ!E7q15bF(vWbys(9b=@;^7S2Ngi$qJW-@e12W+uj&>B)rWQH&n@$>$@7 zpk~%sK?6gt!9PV9otmDxeeak)=4Ge3xw???Q$kH}XwQO>mGx3gAP1CG+$T43|~bkg=wQx{-nY z^4dCCa#%)cqPLgFnHa@Fg53&*qJ>2f#etcrF(aiPu71AW)LCjamKS&T_JyJ)@~5kd zV`f@1$sgFxj<~qA5@`uNXKZBf@#7KPAxVgtf4QDdixb5tZnX6NgJ|g(k;o|{Bg6iI zp~>l4dQ&S)X$|J9@WaD`t=KZjeK3JQU}|Kzy0NjozCn(pB*l?@pa8h!>Wt+^P6D{4 z6;hf@(+s#L9HynU9eIvHaG+m8Tr>vn-+%b?VG*4ckAW&clgHED4L2gbQ|#duzumg} z38VNsFDngrN2#A%Sa{mlLVg?@6&@NAgsyl0Q85WnVtjNEivY1$jAbUwmG)3BM$znI zq9X9n5{U%KF*rQRuu>Ow&XDbE@)*T)18Tz~Qn!Nwd=ukiP5|-pX;UjVXBau_?CL?` zAt?lhF+fGEWsh9RNym~`)-*6D)RRjbDH%l~JuWIEAVUR0wb0PeK%edG>LuvMGT=E$ z#>>}H++u5MtFoqkc5VU3W1_;mJlq+UxA0Z@Tqu|r8zv{l8R+Zp@9(#FbanOg(KL_XsD(>EYjQJzO zE;qNfoFU#4kZP5f!u(24@%6hW^IFP#YX6ymmp_tPf8sKXG^T$0I zrxa0YZj*!12x>@!Q*1l(g%!xy>(SnLj!kQSzQB$GSZUZ^TWp_ zsF4tPxmoE*R^qrcyE0RWo3s)Wl+*WK=q(SQl*lB>H)9mC8eY(Pxwt68ite*Y@p?gy zgS{R734$P66s>P;h$Rqj`zRlS!z1KWbVO+QiTLAU6pmY+2%Rs%DBU6Ewg`sZu6k5d z`u5!f#we4KEEjqTp2#Zc=*UoiU!R`7{%1|C=$5I;39aof`v+e6`FLVXi9U@ClKgMC z3yC}>_@)m)87-JkZcc`#rfi2ig;8(<=mu1H^1>!7J;{@8RSbtBC^9T+UQPx;Mlp(# z{444c%lsI2qX#~dCP+7B6zx6HzJPWQ7SY|)PtX9qg*Ne-Etrbxx?YxhWThvgKLVrN z#I3GvfOp9I|N8X?2rkLGsECk=uw#Bqx8EJ?ZE`Zw1nL5E{9yl}r2Kf-Gdv_Hn%!dj zW{gr9dW=!g(!Ih*M6Yl$3U-0@`>?!nrl)6OR9{aQjF8U!B^af9b|8p(91_J=xEhiY zmxKJFbh$SICA}OR@Uo*DtPG4ZE;_QSrx)`;nLz1wcK5w_*~Kn&R8XKlBW>Sv|9kg7 znOUY+&?Hgx`t6ivx^XoR$xT}=9L~*7H)BE8{Jf124k!aehsTqdhf(44QD0FZC`JK= z-)>!_yk1)E*)qmmJVh9#)CbQ13h}(HeT+?LltU=h&!kFsZ+{g_I+6OIDAje+oza5a zOlS7Z6AcIGDSBMrz%X-q#}q-Ydn#Qps8O(9+A-03KsJa#q|E!9F)HKinHU`(pS*X{ zg6(Z>ygVBad+=Mh*1!fKmBZ%Adg~-9MIT9kAlkiXm1HL3_opzgk*)fBW8p z9kB%Kf_YF%kV5Gx2|nJQZ^y==SO5<+Poz32zz}veHdYwS%`g0ZyO7jAnO;Gc?4r48 zer}fhXAEcP`d!IxWo!l_qi$XRlq!rd0S$qu@rxMM;2==SsAhnYlmMY=-cEHTnic9mO6p#e^lJ(&rf- zDPo(H%p0lI)io>tp&zM9ael1MJy(oUU5v|a6y%Z}XfV9e@@hQ!cf{!E>$d~|5K`cd z<|hn<%OGjrLq932su@0BmsnB)2yusOhu(nD$;(cI;9gx@2UNhR2;N#`V-4c?2B?GF51X5E|mBA_hDea+{m}FwmNT&Hau%4 z?Go-|aN;z0%mosJgC!z925j%@}rv3otWhzjx^y7%Z=pzCUubw@pt?QixG>H>3smJ;YjK zT+D}+mHLK8?rt#GXmBY;u?EJ5285ST!$N}pIXc*Ve%X#A+tSwDavWK>ghucs-(2?e z_LF^3nFk}rt%wYVs!~I4)HO7ceN!2Osz?G{PKd#|Vsv(H{&)5byNn@ih?+bTqcYl@ z8r>q~8c4aTi!;OShtxl9BF#$1pn+17;<+z@rM;-AWNuDK{!ECA3O*^lgF~-M%B#s( zQ$zjM^?dHXwO}?w)RfR z?yiZEA&9R`aJd8{E;=IQav<{s1_@bH*8q>0BSPl%0)La$+38s8}qit$#YrZX%*4ao<=f zauyiHs;sQ83DneLqr+)N;rgzgK2j!N4t6&Fe%`23piF4cfqp)rAwd+QZS5Um$*wVG zj*FJp^^8;-^}^XBIE-tL~goec!8{0}k#H9ecUNwQ1lG%|>QH5o{AnqP zyhHK!@`|*dg0ac_1_DF|NP1fB!TC_k!9O7KRaN;Q7F-EMF{&;Q+`4`R9*RUuP=erm z$a0z8ukGy}*fG}SV!@@wU-gOLhY!m{LV^m*$UAUQWEwe(0C6!VW4LU6ePjP%AAtrk z2$l000wzNklKk(@yR0g)U970ID#lpMNA^1iME z$D*6|s!y&rxR-0zp{n{+?R~zz_xGKu;T`RXnIC5plT$*W=!iK|Qc`kvbJfqvYf+;gSh0j-^XM6hwDyr)-u(P$!$x72uSO4;xD#B=M+skLotz?YOprWjdVCH0| zF0ZV97iYiCgu=z`ogH;`^^4|a|H%9P|4kuAsr}_aS+?~bIfP*PU+qZ;ahTQ>?!*cyYt zH9c=_YwscfEy&B((a|QqL|K{_3RhNE4-O91)dhwI2DUa<1_t_c>PcKMzknO)>+4-| zbD5c$ld6aHbv0F0VrS#X<+S1zE3US86AQaSD zOAE8qq=fa2&Dq&G{Fl$?8ye`@+uI=sGV3H<6ye{>@(L>P{{DfQni{;rgIHQzJmvel z386^DV|WNJ!ahGQ9G{$AUS8GFxey%@4q?~P)zjT84T87lC6Ipbb|oIq#>UFxqB)&p zVL>=IFZ{5)44nr$glD$2Ft@a{AZNF?w+Dwt_74s)wzsvRp}_ji%+7rTo)`vM=i%-~ zj*G>T-u{6@uJS@F`zqz?BRg)LA%uG!k>}|Q}CtO?{9eumCy^WZA zd%2&DQCM&PAYzSyzOK&ot9kGoz-@oo)%g6y(a}e4fB@q7*eF{Y>l3U@{P~~=15bBX zgsiNhhAbE{cQ+SYOeL*+9c^rGZE1T+juc$UqEOr3-a!u?pPa%tBsd^0CQ@cOS6A17 zQE)zt(a`PeY#3Hc6+SF_|6$qD!ER?~XMSPP$-xf1Q&m-!VYO6oSrrC2n3a(#5U8>F z%V0$tpSSJp?Q!Qvd;spifKPQ%9;45iT3TOp$d?3CP^q)lmKIKq_Vv%2xz`vP=qD${ z+S*#vBNG!-HT8`Ejm&K70zq_Cn7^+#J@RVg^^=kcMykx>5yR4o8bE=O_CRl}LRD?U zqUgQMBuI?w1-aH%#}GufX>NJ3y>n`l2T_U)3l0tnKt03imsiyevTQ0YCL$y_kX!>u zP)B^dJ!E7q15bF(vWbys(9b=@;^7S2Ngi$qJW-@e12W+uj&>B)rWQH&n@$>$@7 zpk~%sK?6gt!9PV9otmDxeeak)=4Ge3xw???Q$kH}XwQO>mGx3gAP1CG+$T43|~bkg=wQx{-nY z^4dCCa#%)cqPLgFnHa@Fg53&*qJ>2f#etcrF(aiPu71AW)LCjamKS&T_JyJ)@~5kd zV`f@1$sgFxj<~qA5@`uNXKZBf@#7KPAxVgtf4QDdixb5tZnX6NgJ|g(k;o|{Bg6iI zp~>l4dQ&S)X$|J9@WaD`t=KZjeK3JQU}|Kzy0NjozCn(pB*l?@pa8h!>Wt+^P6D{4 z6;hf@(+s#L9HynU9eIvHaG+m8Tr>vn-+%b?VG*4ckAW&clgHED4L2gbQ|#duzumg} z38VNsFDngrN2#A%Sa{mlLVg?@6&@NAgsyl0Q85WnVtjNEivY1$jAbUwmG)3BM$znI zq9X9n5{U%KF*rQRuu>Ow&XDbE@)*T)18Tz~Qn!Nwd=ukiP5|-pX;UjVXBau_?CL?` zAt?lhF+fGEWsh9RNym~`)-*6D)RRjbDH%l~JuWIEAVUR0wb0PeK%edG>LuvMGT=E$ z#>>}H++u5MtFoqkc5VU3W1_;mJlq+UxA0Z@Tqu|r8zv{l8R+Zp@9(#FbanOg(KL_XsD(>EYjQJzO zE;qNfoFU#4kZP5f!u(24@%6hW^IFP#YX6ymmp_tPf8sKXG^T$0I zrxa0YZj*!12x>@!Q*1l(g%!xy>(SnLj!kQSzQB$GSZUZ^TWp_ zsF4tPxmoE*R^qrcyE0RWo3s)Wl+*WK=q(SQl*lB>H)9mC8eY(Pxwt68ite*Y@p?gy zgS{R734$P66s>P;h$Rqj`zRlS!z1KWbVO+QiTLAU6pmY+2%Rs%DBU6Ewg`sZu6k5d z`u5!f#we4KEEjqTp2#Zc=*UoiU!R`7{%1|C=$5I;39aof`v+e6`FLVXi9U@ClKgMC z3yC}>_@)m)87-JkZcc`#rfi2ig;8(<=mu1H^1>!7J;{@8RSbtBC^9T+UQPx;Mlp(# z{444c%lsI2qX#~dCP+7B6zx6HzJPWQ7SY|)PtX9qg*Ne-Etrbxx?YxhWThvgKLVrN z#I3GvfOp9I|N8X?2rkLGsECk=uw#Bqx8EJ?ZE`Zw1nL5E{9yl}r2Kf-Gdv_Hn%!dj zW{gr9dW=!g(!Ih*M6Yl$3U-0@`>?!nrl)6OR9{aQjF8U!B^af9b|8p(91_J=xEhiY zmxKJFbh$SICA}OR@Uo*DtPG4ZE;_QSrx)`;nLz1wcK5w_*~Kn&R8XKlBW>Sv|9kg7 znOUY+&?Hgx`t6ivx^XoR$xT}=9L~*7H)BE8{Jf124k!aehsTqdhf(44QD0FZC`JK= z-)>!_yk1)E*)qmmJVh9#)CbQ13h}(HeT+?LltU=h&!kFsZ+{g_I+6OIDAje+oza5a zOlS7Z6AcIGDSBMrz%X-q#}q-Ydn#Qps8O(9+A-03KsJa#q|E!9F)HKinHU`(pS*X{ zg6(Z>ygVBad+=Mh*1!fKmBZ%Adg~-9MIT9kAlkiXm1HL3_opzgk*)fBW8p z9kB%Kf_YF%kV5Gx2|nJQZ^y==SO5<+Poz32zz}veHdYwS%`g0ZyO7jAnO;Gc?4r48 zer}fhXAEcP`d!IxWo!l_qi$XRlq!rd0S$qu@rxMM;2==SsAhnYlmMY=-cEHTnic9mO6p#e^lJ(&rf- zDPo(H%p0lI)io>tp&zM9ael1MJy(oUU5v|a6y%Z}XfV9e@@hQ!cf{!E>$d~|5K`cd z<|hn<%OGjrLq932su@0BmsnB)2yusOhu(nD$;(cI;9gx@2UNhR2;N#`V-4c?2B?GF51X5E|mBA_hDea+{m}FwmNT&Hau%4 z?Go-|aN;z0%mosJgC!z925j%@}rv3otWhzjx^y7%Z=pzCUubw@pt?QixG>H>3smJ;YjK zT+D}+mHLK8?rt#GXmBY;u?EJ5285ST!$N}pIXc*Ve%X#A+tSwDavWK>ghucs-(2?e z_LF^3nFk}rt%wYVs!~I4)HO7ceN!2Osz?G{PKd#|Vsv(H{&)5byNn@ih?+bTqcYl@ z8r>q~8c4aTi!;OShtxl9BF#$1pn+17;<+z@rM;-AWNuDK{!ECA3O*^lgF~-M%B#s( zQ$zjM^?dHXwO}?w)RfR z?yiZEA&9R`aJd8{E;=IQav<{s1_@bH*8q>0BSPl%0)La$+38s8}qit$#YrZX%*4ao<=f zauyiHs;sQ83DneLqr+)N;rgzgK2j!N4t6&Fe%`23piF4cfqp)rAwd+QZS5Um$*wVG zj*FJp^^8;-^}^XBIE-tL~goec!8{0}k#H9ecUNwQ1lG%|>QH5o{AnqP zyhHK!@`|*dg0ac_1_DF|NP1fB!TC_k!9O7KRaN;Q7F-EMF{&;Q+`4`R9*RUuP=erm z$a0z8ukGy}*fG}SV!@@wU-gOLhY!m{LV^m*$UAUQWEwe(0C6!VW4LU6ePjP%AAtrk z2$l000wzNklKk(@yR0g)U970ID#lpMNA^1iME z$D*6|s!y&rxR-0zp{n{+?R~zz_xGKu;T`RXnIC5plT$*W=!iK|Qc`kvbJfqvYf+;gSh0j-^XM6hwDyr)-u(P$!$x72uSO4;xD#B=M+skLotz?YOprWjdVCH0| zF0ZV97iYiCgu=z`ogH;`^^4|a|H%9P|4kuAsr}_aS+?~bIfP*PU+qZ;ahTQ>?!*cyYt zH9c=_YwscfEy&B((a|QqL|K{_3RhNE4-O91)dhwI2DUa<1_t_c>PcKMzknO)>+4-| zbD5c$ld6aHbv0F0VrS#X<+S1zE3US86AQaSD zOAE8qq=fa2&Dq&G{Fl$?8ye`@+uI=sGV3H<6ye{>@(L>P{{DfQni{;rgIHQzJmvel z386^DV|WNJ!ahGQ9G{$AUS8GFxey%@4q?~P)zjT84T87lC6Ipbb|oIq#>UFxqB)&p zVL>=IFZ{5)44nr$glD$2Ft@a{AZNF?w+Dwt_74s)wzsvRp}_ji%+7rTo)`vM=i%-~ zj*G>T-u{6@uJS@F`zqz?BRg)LA%uG!k>}|Q}CtO?{9eumCy^WZA zd%2&DQCM&PAYzSyzOK&ot9kGoz-@oo)%g6y(a}e4fB@q7*eF{Y>l3U@{P~~=15bBX zgsiNhhAbE{cQ+SYOeL*+9c^rGZE1T+juc$UqEOr3-a!u?pPa%tBsd^0CQ@cOS6A17 zQE)zt(a`PeY#3Hc6+SF_|6$qD!ER?~XMSPP$-xf1Q&m-!VYO6oSrrC2n3a(#5U8>F z%V0$tpSSJp?Q!Qvd;spifKPQ%9;45iT3TOp$d?3CP^q)lmKIKq_Vv%2xz`vP=qD${ z+S*#vBNG!-HT8`Ejm&K70zq_Cn7^+#J@RVg^^=kcMykx>5yR4o8bE=O_CRl}LRD?U zqUgQMBuI?w1-aH%#}GufX>NJ3y>n`l2T_U)3l0tnKt03imsiyevTQ0YCL$y_kX!>u zP)B^dJ!E7q15bF(vWbys(9b=@;^7S2Ngi$qJW-@e12W+uj&>B)rWQH&n@$>$@7 zpk~%sK?6gt!9PV9otmDxeeak)=4Ge3xw???Q$kH}XwQO>mGx3gAP1CG+$T43|~bkg=wQx{-nY z^4dCCa#%)cqPLgFnHa@Fg53&*qJ>2f#etcrF(aiPu71AW)LCjamKS&T_JyJ)@~5kd zV`f@1$sgFxj<~qA5@`uNXKZBf@#7KPAxVgtf4QDdixb5tZnX6NgJ|g(k;o|{Bg6iI zp~>l4dQ&S)X$|J9@WaD`t=KZjeK3JQU}|Kzy0NjozCn(pB*l?@pa8h!>Wt+^P6D{4 z6;hf@(+s#L9HynU9eIvHaG+m8Tr>vn-+%b?VG*4ckAW&clgHED4L2gbQ|#duzumg} z38VNsFDngrN2#A%Sa{mlLVg?@6&@NAgsyl0Q85WnVtjNEivY1$jAbUwmG)3BM$znI zq9X9n5{U%KF*rQRuu>Ow&XDbE@)*T)18Tz~Qn!Nwd=ukiP5|-pX;UjVXBau_?CL?` zAt?lhF+fGEWsh9RNym~`)-*6D)RRjbDH%l~JuWIEAVUR0wb0PeK%edG>LuvMGT=E$ z#>>}H++u5MtFoqkc5VU3W1_;mJlq+UxA0Z@Tqu|r8zv{l8R+Zp@9(#FbanOg(KL_XsD(>EYjQJzO zE;qNfoFU#4kZP5f!u(24@%6hW^IFP#YX6ymmp_tPf8sKXG^T$0I zrxa0YZj*!12x>@!Q*1l(g%!xy>(SnLj!kQSzQB$GSZUZ^TWp_ zsF4tPxmoE*R^qrcyE0RWo3s)Wl+*WK=q(SQl*lB>H)9mC8eY(Pxwt68ite*Y@p?gy zgS{R734$P66s>P;h$Rqj`zRlS!z1KWbVO+QiTLAU6pmY+2%Rs%DBU6Ewg`sZu6k5d z`u5!f#we4KEEjqTp2#Zc=*UoiU!R`7{%1|C=$5I;39aof`v+e6`FLVXi9U@ClKgMC z3yC}>_@)m)87-JkZcc`#rfi2ig;8(<=mu1H^1>!7J;{@8RSbtBC^9T+UQPx;Mlp(# z{444c%lsI2qX#~dCP+7B6zx6HzJPWQ7SY|)PtX9qg*Ne-Etrbxx?YxhWThvgKLVrN z#I3GvfOp9I|N8X?2rkLGsECk=uw#Bqx8EJ?ZE`Zw1nL5E{9yl}r2Kf-Gdv_Hn%!dj zW{gr9dW=!g(!Ih*M6Yl$3U-0@`>?!nrl)6OR9{aQjF8U!B^af9b|8p(91_J=xEhiY zmxKJFbh$SICA}OR@Uo*DtPG4ZE;_QSrx)`;nLz1wcK5w_*~Kn&R8XKlBW>Sv|9kg7 znOUY+&?Hgx`t6ivx^XoR$xT}=9L~*7H)BE8{Jf124k!aehsTqdhf(44QD0FZC`JK= z-)>!_yk1)E*)qmmJVh9#)CbQ13h}(HeT+?LltU=h&!kFsZ+{g_I+6OIDAje+oza5a zOlS7Z6AcIGDSBMrz%X-q#}q-Ydn#Qps8O(9+A-03KsJa#q|E!9F)HKinHU`(pS*X{ zg6(Z>ygVBad+=Mh*1!fKmBZ%Adg~-9MIT9kAlkiXm1HL3_opzgk*)fBW8p z9kB%Kf_YF%kV5Gx2|nJQZ^y==SO5<+Poz32zz}veHdYwS%`g0ZyO7jAnO;Gc?4r48 zer}fhXAEcP`d!IxWo!l_qi$XRlq!rd0S$qu@rxMM;2==SsAhnYlmMY=-cEHTnic9mO6p#e^lJ(&rf- zDPo(H%p0lI)io>tp&zM9ael1MJy(oUU5v|a6y%Z}XfV9e@@hQ!cf{!E>$d~|5K`cd z<|hn<%OGjrLq932su@0BmsnB)2yusOhu(nD$;(cI;9gx@2UNhR2;N#`V-4c?2B?GF51X5E|mBA_hDea+{m}FwmNT&Hau%4 z?Go-|aN;z0%mosJgC!z925j%@}rv3otWhzjx^y7%Z=pzCUubw@pt?QixG>H>3smJ;YjK zT+D}+mHLK8?rt#GXmBY;u?EJ5285ST!$N}pIXc*Ve%X#A+tSwDavWK>ghucs-(2?e z_LF^3nFk}rt%wYVs!~I4)HO7ceN!2Osz?G{PKd#|Vsv(H{&)5byNn@ih?+bTqcYl@ z8r>q~8c4aTi!;OShtxl9BF#$1pn+17;<+z@rM;-AWNuDK{!ECA3O*^lgF~-M%B#s( zQ$zjM^?dHXwO}?w)RfR z?yiZEA&9R`aJd8{E;=IQav<{s1_@bH*8q>0BSPl%0)La$+38s8}qit$#YrZX%*4ao<=f zauyiHs;sQ83DneLqr+)N;rgzgK2j!N4t6&Fe%`23piF4cfqp)rAwd+QZS5Um$*wVG zj*FJp^^8;-^}^XBIE-tL~goec!8{0}k#H9ecUNwQ1lG%|>QH5o{AnqP zyhHK!@`|*dg0ac_1_DF|NP1fB!TC_k!9O7KRaN;Q7F-EMF{&;Q+`4`R9*RUuP=erm z$a0z8ukGy}*fG}SV!@@wU-gOLhY!m{LV^m*$UAUQWEwe(0C6!VW4LU6ePjP%AAtrk z2 Tuple[int, int]: @@ -1053,6 +1083,12 @@ def run_ui_monitoring_loop(trigger_queue: queue.Queue, command_queue: queue.Queu 'keyword_wolf_upper_type3': KEYWORD_Wolf_UPPER_TYPE3_IMG, 'keyword_wolf_lower_type4': KEYWORD_wolf_LOWER_TYPE4_IMG, # Added type4 'keyword_wolf_upper_type4': KEYWORD_Wolf_UPPER_TYPE4_IMG, # Added type4 + # --- Add Reply Keywords --- + 'keyword_wolf_reply': KEYWORD_WOLF_REPLY_IMG, + 'keyword_wolf_reply_type2': KEYWORD_WOLF_REPLY_TYPE2_IMG, + 'keyword_wolf_reply_type3': KEYWORD_WOLF_REPLY_TYPE3_IMG, + 'keyword_wolf_reply_type4': KEYWORD_WOLF_REPLY_TYPE4_IMG, + # --- End Reply Keywords --- 'copy_menu_item': COPY_MENU_ITEM_IMG, 'profile_option': PROFILE_OPTION_IMG, 'copy_name_button': COPY_NAME_BUTTON_IMG, 'send_button': SEND_BUTTON_IMG, 'chat_input': CHAT_INPUT_IMG, 'profile_name_page': PROFILE_NAME_PAGE_IMG, @@ -1247,255 +1283,288 @@ def run_ui_monitoring_loop(trigger_queue: queue.Queue, command_queue: queue.Queu all_bubbles_data = detector.find_dialogue_bubbles() # Returns list of dicts if not all_bubbles_data: time.sleep(2); continue - # Filter out bot bubbles, find newest non-bot bubble (example logic) + # Filter out bot bubbles other_bubbles_data = [b_info for b_info in all_bubbles_data if not b_info['is_bot']] if not other_bubbles_data: time.sleep(0.2); continue - # Simple logic: assume lowest bubble is newest (might need improvement) - # Sort by bbox bottom y-coordinate (index 3) - target_bubble_info = max(other_bubbles_data, key=lambda b_info: b_info['bbox'][3]) - # 2. Check for Duplicates (Position & Content) - # Compare using the 'bbox' from the info dicts - if are_bboxes_similar(target_bubble_info.get('bbox'), last_processed_bubble_info.get('bbox') if last_processed_bubble_info else None): - time.sleep(0.2); continue + # Sort bubbles from bottom to top (based on bottom Y coordinate) + sorted_bubbles = sorted(other_bubbles_data, key=lambda b_info: b_info['bbox'][3], reverse=True) - # 3. Detect Keyword in Bubble - target_bbox = target_bubble_info['bbox'] - bubble_region = (target_bbox[0], target_bbox[1], target_bbox[2]-target_bbox[0], target_bbox[3]-target_bbox[1]) - keyword_coords = detector.find_keyword_in_region(bubble_region) + # Iterate through sorted bubbles (bottom to top) + for target_bubble_info in sorted_bubbles: + target_bbox = target_bubble_info['bbox'] + bubble_region = (target_bbox[0], target_bbox[1], target_bbox[2]-target_bbox[0], target_bbox[3]-target_bbox[1]) - if keyword_coords: - print(f"\n!!! Keyword detected in bubble {target_bbox} !!!") + # 3. Detect Keyword in Bubble + keyword_coords = detector.find_keyword_in_region(bubble_region) - # --- Variables needed later --- - bubble_snapshot = None # Initialize snapshot variable - search_area = SCREENSHOT_REGION # Define search area early - if search_area is None: - print("Warning: SCREENSHOT_REGION not defined, searching full screen for bubble snapshot.") - # Consider adding a default chat region if SCREENSHOT_REGION is often None + if keyword_coords: + print(f"\n!!! Keyword detected in bubble {target_bbox} !!!") - # --- Take Snapshot for Re-location (and potentially save it) --- - try: - bubble_region_tuple = (int(bubble_region[0]), int(bubble_region[1]), int(bubble_region[2]), int(bubble_region[3])) - if bubble_region_tuple[2] <= 0 or bubble_region_tuple[3] <= 0: - print(f"Warning: Invalid bubble region {bubble_region_tuple} for snapshot. Skipping trigger.") - continue - bubble_snapshot = pyautogui.screenshot(region=bubble_region_tuple) - if bubble_snapshot is None: - print("Warning: Failed to capture bubble snapshot. Skipping trigger.") - continue + # --- Determine if it's a reply keyword for offset --- + is_reply_keyword = False + reply_keyword_keys = ['keyword_wolf_reply', 'keyword_wolf_reply_type2', 'keyword_wolf_reply_type3', 'keyword_wolf_reply_type4'] + for key in reply_keyword_keys: + reply_locs = detector._find_template(key, region=bubble_region, grayscale=False, confidence=detector.confidence) + if reply_locs: + for loc in reply_locs: + if abs(keyword_coords[0] - loc[0]) <= 2 and abs(keyword_coords[1] - loc[1]) <= 2: + print(f"Confirmed detected keyword at {keyword_coords} matches reply keyword template '{key}' at {loc}.") + is_reply_keyword = True + break + if is_reply_keyword: + break - # --- Save Snapshot for Debugging (Replaces old debug screenshot logic) --- - try: - screenshot_index = (screenshot_counter % MAX_DEBUG_SCREENSHOTS) + 1 - # Use a more descriptive filename - screenshot_filename = f"debug_relocation_snapshot_{screenshot_index}.png" - screenshot_path = os.path.join(DEBUG_SCREENSHOT_DIR, screenshot_filename) - print(f"Attempting to save bubble snapshot used for re-location to: {screenshot_path}") - bubble_snapshot.save(screenshot_path) # Save the PIL image object - print(f"Successfully saved bubble snapshot: {screenshot_path}") - screenshot_counter += 1 - except Exception as save_err: - print(f"Error saving bubble snapshot to {screenshot_path}: {repr(save_err)}") - # Continue even if saving fails - - except Exception as snapshot_err: - print(f"Error taking initial bubble snapshot: {repr(snapshot_err)}") - continue # Skip trigger if snapshot fails - - # 4. Interact: Get Bubble Text - bubble_text = interactor.copy_text_at(keyword_coords) - if not bubble_text: - print("Error: Could not get dialogue content.") - last_processed_bubble_info = target_bubble_info # Mark as processed even if failed - perform_state_cleanup(detector, interactor) # Attempt cleanup after failed copy - continue - - # Check recent text history (needs context awareness) - if bubble_text in recent_texts: - print(f"Content '{bubble_text[:30]}...' in recent history, skipping.") - last_processed_bubble_info = target_bubble_info - continue - - print(">>> New trigger event <<<") - last_processed_bubble_info = target_bubble_info - recent_texts.append(bubble_text) - - # 5. Interact: Get Sender Name (with Bubble Re-location) - sender_name = None - try: - # --- Bubble Re-location Logic with Fallback Mechanism --- - print("Attempting to re-locate bubble before getting sender name...") - if bubble_snapshot is None: # Should not happen if we reached here, but check anyway - print("Error: Bubble snapshot missing for re-location. Skipping.") - continue - - # First attempt with standard confidence - print(f"First attempt with confidence {BUBBLE_RELOCATE_CONFIDENCE}...") - new_bubble_box = None - try: - new_bubble_box = pyautogui.locateOnScreen(bubble_snapshot, - region=search_area, - confidence=BUBBLE_RELOCATE_CONFIDENCE) - except Exception as e: - print(f"Exception during initial bubble location attempt: {e}") - - # Second attempt with fallback confidence if first failed - if not new_bubble_box: - print(f"First attempt failed. Trying with lower confidence {BUBBLE_RELOCATE_FALLBACK_CONFIDENCE}...") - try: - # Try with a lower confidence threshold - new_bubble_box = pyautogui.locateOnScreen(bubble_snapshot, - region=search_area, - confidence=BUBBLE_RELOCATE_FALLBACK_CONFIDENCE) - except Exception as e: - print(f"Exception during fallback bubble location attempt: {e}") - - # Third attempt with even lower confidence as last resort - if not new_bubble_box: - print("Second attempt failed. Trying with even lower confidence 0.4...") - try: - # Last resort with very low confidence - new_bubble_box = pyautogui.locateOnScreen(bubble_snapshot, - region=search_area, - confidence=0.4) - except Exception as e: - print(f"Exception during last resort bubble location attempt: {e}") - - if new_bubble_box: - new_tl_x, new_tl_y = new_bubble_box.left, new_bubble_box.top - print(f"Successfully re-located bubble snapshot at: ({new_tl_x}, {new_tl_y})") - # Calculate avatar coords based on the *new* top-left and the *reply* offsets - new_avatar_coords = (new_tl_x + AVATAR_OFFSET_X_REPLY, new_tl_y + AVATAR_OFFSET_Y_REPLY) - print(f"Calculated new avatar coordinates for reply context: {new_avatar_coords}") - # Proceed to get sender name using the new coordinates, passing snapshot info for retries - sender_name = interactor.retrieve_sender_name_interaction( - initial_avatar_coords=new_avatar_coords, - bubble_snapshot=bubble_snapshot, - search_area=search_area - ) + # Calculate click coordinates with potential offset + click_coords = keyword_coords + if is_reply_keyword: + click_coords = (keyword_coords[0], keyword_coords[1] + 25) + print(f"Applying +25 Y-offset for reply keyword. Click target: {click_coords}") else: - print("Warning: Failed to re-locate bubble snapshot on screen after multiple attempts with decreasing confidence thresholds.") - print("Trying direct approach with original bubble coordinates...") - - # Fallback to original coordinates based on the target_bubble_info - original_tl_coords = target_bubble_info.get('tl_coords') - if original_tl_coords: - fallback_avatar_coords = (original_tl_coords[0] + AVATAR_OFFSET_X_REPLY, - original_tl_coords[1] + AVATAR_OFFSET_Y_REPLY) - print(f"Using fallback avatar coordinates from original detection: {fallback_avatar_coords}") - - # Try with direct coordinates + print(f"Detected keyword is not a reply type. Click target: {click_coords}") + + # --- Variables needed later --- + bubble_snapshot = None + search_area = SCREENSHOT_REGION + if search_area is None: + print("Warning: SCREENSHOT_REGION not defined, searching full screen for bubble snapshot.") + + # --- Take Snapshot for Re-location --- + try: + bubble_region_tuple = (int(bubble_region[0]), int(bubble_region[1]), int(bubble_region[2]), int(bubble_region[3])) + if bubble_region_tuple[2] <= 0 or bubble_region_tuple[3] <= 0: + print(f"Warning: Invalid bubble region {bubble_region_tuple} for snapshot. Skipping this bubble.") + continue # Skip to next bubble in the loop + bubble_snapshot = pyautogui.screenshot(region=bubble_region_tuple) + if bubble_snapshot is None: + print("Warning: Failed to capture bubble snapshot. Skipping this bubble.") + continue # Skip to next bubble + + # --- Save Snapshot for Debugging --- + try: + screenshot_index = (screenshot_counter % MAX_DEBUG_SCREENSHOTS) + 1 + screenshot_filename = f"debug_relocation_snapshot_{screenshot_index}.png" + screenshot_path = os.path.join(DEBUG_SCREENSHOT_DIR, screenshot_filename) + print(f"Attempting to save bubble snapshot used for re-location to: {screenshot_path}") + bubble_snapshot.save(screenshot_path) + print(f"Successfully saved bubble snapshot: {screenshot_path}") + screenshot_counter += 1 + except Exception as save_err: + print(f"Error saving bubble snapshot to {screenshot_path}: {repr(save_err)}") + + except Exception as snapshot_err: + print(f"Error taking initial bubble snapshot: {repr(snapshot_err)}") + continue # Skip to next bubble + + # 4. Re-locate bubble *before* copying text + print("Attempting to re-locate bubble before copying text...") + new_bubble_box_for_copy = None + if bubble_snapshot: + try: + # Use standard confidence for this initial critical step + new_bubble_box_for_copy = pyautogui.locateOnScreen(bubble_snapshot, + region=search_area, + confidence=BUBBLE_RELOCATE_CONFIDENCE) + except Exception as e: + print(f"Exception during bubble location before copy: {e}") + + if not new_bubble_box_for_copy: + print("Warning: Failed to re-locate bubble before copying text. Skipping this bubble.") + continue # Skip to the next bubble in the outer loop + + print(f"Successfully re-located bubble for copy at: {new_bubble_box_for_copy}") + # Define the region based on the re-located bubble to find the keyword again + copy_bubble_region = (new_bubble_box_for_copy.left, new_bubble_box_for_copy.top, + new_bubble_box_for_copy.width, new_bubble_box_for_copy.height) + + # Find the keyword *again* within the *new* bubble region to get current coords + current_keyword_coords = detector.find_keyword_in_region(copy_bubble_region) + if not current_keyword_coords: + print("Warning: Keyword not found in the re-located bubble region. Skipping this bubble.") + continue # Skip to the next bubble + + # Determine if it's a reply keyword based on the *new* location/region + is_reply_keyword_current = False + # (Re-check is_reply_keyword logic here based on current_keyword_coords and copy_bubble_region) + # This check might be complex, for simplicity, we can reuse the 'is_reply_keyword' + # determined earlier based on the initial detection, assuming the keyword type doesn't change. + # Let's reuse the previously determined 'is_reply_keyword' for offset calculation. + click_coords_current = current_keyword_coords + if is_reply_keyword: # Use the flag determined from initial detection + click_coords_current = (current_keyword_coords[0], current_keyword_coords[1] + 25) + print(f"Applying +25 Y-offset for reply keyword (current location). Click target: {click_coords_current}") + else: + print(f"Detected keyword is not a reply type (current location). Click target: {click_coords_current}") + + # Interact: Get Bubble Text using current coordinates + bubble_text = interactor.copy_text_at(click_coords_current) + if not bubble_text: + print("Error: Could not get dialogue content for this bubble (after re-location).") + perform_state_cleanup(detector, interactor) # Attempt cleanup + continue # Skip to next bubble + + # Check recent text history + if bubble_text in recent_texts: + print(f"Content '{bubble_text[:30]}...' in recent history, skipping this bubble.") + continue # Skip to next bubble + + print(">>> New trigger event <<<") + # Add to recent texts *before* potentially long interaction + recent_texts.append(bubble_text) + + # 5. Interact: Get Sender Name (uses re-location internally via retrieve_sender_name_interaction) + sender_name = None + try: + # --- Bubble Re-location Logic --- + print("Attempting to re-locate bubble before getting sender name...") + if bubble_snapshot is None: + print("Error: Bubble snapshot missing for re-location. Skipping this bubble.") + continue + + # Try locating with decreasing confidence + new_bubble_box = None + confidences_to_try = [BUBBLE_RELOCATE_CONFIDENCE, BUBBLE_RELOCATE_FALLBACK_CONFIDENCE, 0.4] + for conf in confidences_to_try: + print(f"Attempting location with confidence {conf}...") + try: + new_bubble_box = pyautogui.locateOnScreen(bubble_snapshot, + region=search_area, + confidence=conf) + if new_bubble_box: + print(f"Successfully located with confidence {conf}.") + break # Found it + except Exception as e: + print(f"Exception during location attempt with confidence {conf}: {e}") + # --- End Confidence Loop --- + + if new_bubble_box: + new_tl_x, new_tl_y = new_bubble_box.left, new_bubble_box.top + print(f"Successfully re-located bubble snapshot at: ({new_tl_x}, {new_tl_y})") + new_avatar_coords = (new_tl_x + AVATAR_OFFSET_X_REPLY, new_tl_y + AVATAR_OFFSET_Y_REPLY) + print(f"Calculated new avatar coordinates for reply context: {new_avatar_coords}") sender_name = interactor.retrieve_sender_name_interaction( - initial_avatar_coords=fallback_avatar_coords, - bubble_snapshot=bubble_snapshot, + initial_avatar_coords=new_avatar_coords, + bubble_snapshot=bubble_snapshot, search_area=search_area ) - - if not sender_name: - print("Direct approach failed. Skipping this trigger.") - last_processed_bubble_info = target_bubble_info # Mark as processed - perform_state_cleanup(detector, interactor) # Cleanup - continue else: - print("No original coordinates available. Skipping sender name retrieval.") - # No need to continue if we can't find the bubble again - last_processed_bubble_info = target_bubble_info # Mark as processed to avoid re-triggering immediately - perform_state_cleanup(detector, interactor) # Attempt cleanup as state might be inconsistent - continue - # --- End Bubble Re-location Logic --- + print("Warning: Failed to re-locate bubble snapshot after multiple attempts.") + print("Trying direct approach with original bubble coordinates...") + original_tl_coords = target_bubble_info.get('tl_coords') + if original_tl_coords: + fallback_avatar_coords = (original_tl_coords[0] + AVATAR_OFFSET_X_REPLY, + original_tl_coords[1] + AVATAR_OFFSET_Y_REPLY) + print(f"Using fallback avatar coordinates from original detection: {fallback_avatar_coords}") + sender_name = interactor.retrieve_sender_name_interaction( + initial_avatar_coords=fallback_avatar_coords, + bubble_snapshot=bubble_snapshot, + search_area=search_area + ) + if not sender_name: + print("Direct approach failed. Skipping this trigger.") + perform_state_cleanup(detector, interactor) + continue # Skip to next bubble + else: + print("No original coordinates available. Skipping sender name retrieval.") + perform_state_cleanup(detector, interactor) + continue # Skip to next bubble + # --- End Bubble Re-location Logic --- - except Exception as reloc_err: - print(f"Error during bubble re-location or subsequent interaction: {reloc_err}") - import traceback - traceback.print_exc() - # Attempt cleanup after error during this critical phase - perform_state_cleanup(detector, interactor) - continue # Skip further processing for this trigger + except Exception as reloc_err: + print(f"Error during bubble re-location or subsequent interaction: {reloc_err}") + import traceback + traceback.print_exc() + perform_state_cleanup(detector, interactor) + continue # Skip to next bubble - # 6. Perform Cleanup (Crucial after potentially leaving chat screen) - # Moved the check for sender_name *after* potential re-location attempt - cleanup_successful = perform_state_cleanup(detector, interactor) - if not cleanup_successful: - print("Error: Failed to return to chat screen after getting name. Aborting trigger.") - continue # Skip putting in queue if cleanup failed + # 6. Perform Cleanup + cleanup_successful = perform_state_cleanup(detector, interactor) + if not cleanup_successful: + print("Error: Failed to return to chat screen after getting name. Skipping this bubble.") + continue # Skip to next bubble - if not sender_name: - print("Error: Could not get sender name, aborting processing.") - continue # Already cleaned up, just skip + if not sender_name: + print("Error: Could not get sender name for this bubble, skipping.") + continue # Skip to next bubble - # --- Attempt to activate reply context BEFORE putting in queue --- - reply_context_activated = False - try: - print("Attempting to activate reply context...") - # Re-locate the bubble *again* to click its center for reply - if bubble_snapshot is None: - print("Warning: Bubble snapshot missing for reply context activation. Skipping.") - final_bubble_box_for_reply = None # Ensure it's None - else: - print(f"Attempting final re-location for reply context using search_area: {search_area}") - final_bubble_box_for_reply = pyautogui.locateOnScreen(bubble_snapshot, region=search_area, confidence=BUBBLE_RELOCATE_CONFIDENCE) - - if final_bubble_box_for_reply: - print(f"Final re-location successful at: {final_bubble_box_for_reply}") - bubble_x_reply, bubble_y_reply = final_bubble_box_for_reply.left, final_bubble_box_for_reply.top - bubble_w_reply, bubble_h_reply = final_bubble_box_for_reply.width, final_bubble_box_for_reply.height - center_x_reply = bubble_x_reply + bubble_w_reply // 2 - center_y_reply = bubble_y_reply + bubble_h_reply // 2 - - print(f"Clicking bubble center for reply at ({center_x_reply}, {center_y_reply})") - interactor.click_at(center_x_reply, center_y_reply) - time.sleep(0.15) # Increased wait time for menu/reply button to appear - - print("Searching for reply button...") - reply_button_locs = detector._find_template('reply_button', confidence=0.8) - if reply_button_locs: - reply_coords = reply_button_locs[0] - print(f"Found reply button at {reply_coords}. Clicking...") - interactor.click_at(reply_coords[0], reply_coords[1]) - time.sleep(0.07) # Wait after click - reply_context_activated = True - print("Reply context activated.") + # --- Attempt to activate reply context --- + reply_context_activated = False + try: + print("Attempting to activate reply context...") + if bubble_snapshot is None: + print("Warning: Bubble snapshot missing for reply context activation. Skipping.") + final_bubble_box_for_reply = None else: - print(">>> Reply button template ('reply_button') not found after clicking bubble center. <<<") - # Optional: Press ESC to close menu if reply button wasn't found? - # print("Attempting to press ESC to close potential menu.") - # interactor.press_key('esc') - # time.sleep(0.1) - else: - # This log message was already present but is important - print("Warning: Failed to re-locate bubble for activating reply context.") + print(f"Attempting final re-location for reply context using search_area: {search_area}") + final_bubble_box_for_reply = pyautogui.locateOnScreen(bubble_snapshot, region=search_area, confidence=BUBBLE_RELOCATE_CONFIDENCE) - except Exception as reply_context_err: - print(f"!!! Error during reply context activation: {reply_context_err} !!!") - # Ensure reply_context_activated remains False + if final_bubble_box_for_reply: + print(f"Final re-location successful at: {final_bubble_box_for_reply}") + bubble_x_reply, bubble_y_reply = final_bubble_box_for_reply.left, final_bubble_box_for_reply.top + bubble_w_reply, bubble_h_reply = final_bubble_box_for_reply.width, final_bubble_box_for_reply.height + center_x_reply = bubble_x_reply + bubble_w_reply // 2 + center_y_reply = bubble_y_reply + bubble_h_reply // 2 - # 7. Send Trigger Info to Main Thread/Async Loop - print("\n>>> Putting trigger info in Queue <<<") - print(f" Sender: {sender_name}") - print(f" Content: {bubble_text[:100]}...") - print(f" Bubble Region: {bubble_region}") # Include region derived from bbox - print(f" Reply Context Activated: {reply_context_activated}") # Include the flag - try: - # Include bubble_region and reply_context_activated flag - data_to_send = { - 'sender': sender_name, - 'text': bubble_text, - 'bubble_region': bubble_region, # Use bbox-derived region for general use - 'reply_context_activated': reply_context_activated, # Send the flag - 'bubble_snapshot': bubble_snapshot, # <-- Add snapshot - 'search_area': search_area # <-- Add search area used for snapshot - # 'tl_coords': target_bubble_info['tl_coords'] # Optionally send if needed elsewhere - } - trigger_queue.put(data_to_send) # Put in the queue for main loop - print("Trigger info (with region, reply flag, snapshot, search_area) placed in Queue.") - except Exception as q_err: - print(f"Error putting data in Queue: {q_err}") + if is_reply_keyword: + center_y_reply += 15 + print(f"Applying +15 Y-offset to bubble center click for reply keyword. Target Y: {center_y_reply}") - print("--- Single trigger processing complete ---") - time.sleep(0.1) # Pause after successful trigger + print(f"Clicking bubble center for reply at ({center_x_reply}, {center_y_reply})") + interactor.click_at(center_x_reply, center_y_reply) + time.sleep(0.15) - time.sleep(1.5) # Polling interval + print("Searching for reply button...") + reply_button_locs = detector._find_template('reply_button', confidence=0.8) + if reply_button_locs: + reply_coords = reply_button_locs[0] + print(f"Found reply button at {reply_coords}. Clicking...") + interactor.click_at(reply_coords[0], reply_coords[1]) + time.sleep(0.07) + reply_context_activated = True + print("Reply context activated.") + else: + print(">>> Reply button template ('reply_button') not found after clicking bubble center. <<<") + else: + print("Warning: Failed to re-locate bubble for activating reply context.") + + except Exception as reply_context_err: + print(f"!!! Error during reply context activation: {reply_context_err} !!!") + + # 7. Send Trigger Info to Main Thread + print("\n>>> Putting trigger info in Queue <<<") + print(f" Sender: {sender_name}") + print(f" Content: {bubble_text[:100]}...") + print(f" Bubble Region: {bubble_region}") # Original region for context + print(f" Reply Context Activated: {reply_context_activated}") + try: + data_to_send = { + 'sender': sender_name, + 'text': bubble_text, + 'bubble_region': bubble_region, # Send original region for context if needed + 'reply_context_activated': reply_context_activated, + 'bubble_snapshot': bubble_snapshot, # Send the snapshot used + 'search_area': search_area + } + trigger_queue.put(data_to_send) + print("Trigger info (with region, reply flag, snapshot, search_area) placed in Queue.") + + # --- CRITICAL: Break loop after successfully processing one trigger --- + print("--- Single bubble processing complete. Breaking scan cycle. ---") + break # Exit the 'for target_bubble_info in sorted_bubbles' loop + + except Exception as q_err: + print(f"Error putting data in Queue: {q_err}") + # Don't break if queue put fails, maybe try next bubble? Or log and break? + # Let's break here too, as something is wrong. + print("Breaking scan cycle due to queue error.") + break + + # End of keyword found block (if keyword_coords:) + # End of loop through sorted bubbles (for target_bubble_info...) + + # If the loop finished without breaking (i.e., no trigger processed), wait the full interval. + # If it broke, the sleep still happens here before the next cycle. + time.sleep(1.5) # Polling interval after checking all bubbles or processing one except KeyboardInterrupt: print("\nMonitoring interrupted.") From f2cca2d39445acea089040b0180ff3b20f80d9a9 Mon Sep 17 00:00:00 2001 From: z060142 Date: Sun, 20 Apr 2025 21:39:43 +0800 Subject: [PATCH 7/7] Improve LLM system prompt update. Fix Crash caused by calling MCP --- .gitignore | 4 +- ClaudeCode.md | 85 ++++++++++- config.py | 4 + llm_interaction.py | 161 +++++++++++++++++---- main.py | 71 ++++++++- templates/capitol/position_development.png | Bin 4344 -> 3937 bytes templates/capitol/position_science.png | Bin 4181 -> 3295 bytes 7 files changed, 286 insertions(+), 39 deletions(-) diff --git a/.gitignore b/.gitignore index d550f45..2e99f16 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ .env +*.log llm_debug.log __pycache__/ -debug_screenshots/ \ No newline at end of file +debug_screenshots/ +chat_logs/ \ No newline at end of file diff --git a/ClaudeCode.md b/ClaudeCode.md index 6b8f589..4b0d65a 100644 --- a/ClaudeCode.md +++ b/ClaudeCode.md @@ -314,6 +314,56 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機 - 其他關鍵字或 UI 元素的點擊不受影響。 - **效果**:系統現在可以偵測新的回覆指示圖片作為觸發條件。當由這些圖片觸發時,用於複製文字的點擊和用於激活回覆上下文的氣泡中心點擊都會向下微調 15 像素,以避免誤觸其他 UI 元素。 +### 強化 LLM 上下文處理與回應生成 (2025-04-20) + +- **目的**:解決 LLM 可能混淆歷史對話與當前訊息,以及在回應中包含歷史記錄的問題。確保 `dialogue` 欄位只包含針對最新用戶訊息的新回覆。 +- **`llm_interaction.py`**: + - **修改 `get_system_prompt`**: + - 在 `dialogue` 欄位的規則中,明確禁止包含任何歷史記錄,並強調必須只回應標記為 `` 的最新訊息。 + - 在核心指令中,要求 LLM 將分析和回應生成完全集中在 `` 標記的訊息上。 + - 新增了對 `` 標記作用的說明。 + - **修改 `_build_context_messages`**: + - 在構建發送給 LLM 的訊息列表時,將歷史記錄中的最後一條用戶訊息用 `...` 標籤包裹起來。 + - 其他歷史訊息保持原有的 `[timestamp] speaker: message` 格式。 +- **效果**:通過更嚴格的提示和明確的上下文標記,引導 LLM 準確區分當前互動和歷史對話,預期能提高回應的相關性並防止輸出冗餘的歷史內容。 + +### 強化 System Prompt 以鼓勵工具使用 (2025-04-19) + +- **目的**:調整 `llm_interaction.py` 中的 `get_system_prompt` 函數,使其更明確地引導 LLM 在回應前主動使用工具(特別是記憶體工具)和整合工具資訊。 +- **修改內容**: + 1. **核心身份強化**:在 `CORE IDENTITY AND TOOL USAGE` 部分加入新的一點,強調 Wolfhart 會主動查閱內部知識圖譜和外部來源。 + 2. **記憶體指示強化**:將 `Memory Management (Knowledge Graph)` 部分的提示從 "IMPORTANT" 改為 "CRITICAL",並明確指示在回應*之前*要考慮使用查詢工具檢查記憶體,同時也強調了寫入新資訊的主動性。 +- **效果**:旨在提高 LLM 使用工具的主動性和依賴性,使其回應更具上下文感知和資訊準確性,同時保持角色一致性。 + +### 聊天歷史記錄上下文與日誌記錄 (2025-04-20) + +- **目的**: + 1. 為 LLM 提供更豐富的對話上下文,以生成更連貫和相關的回應。 + 2. 新增一個可選的聊天日誌功能,用於調試和記錄。 +- **`main.py`**: + - 引入 `collections.deque` 來儲存最近的對話歷史(用戶訊息和機器人回應),上限為 50 條。 + - 在調用 `llm_interaction.get_llm_response` 之前,將用戶訊息添加到歷史記錄中。 + - 在收到有效的 LLM 回應後,將機器人回應添加到歷史記錄中。 + - 新增 `log_chat_interaction` 函數,該函數: + - 檢查 `config.ENABLE_CHAT_LOGGING` 標誌。 + - 如果啟用,則在 `config.LOG_DIR` 指定的文件夾中創建或附加到以日期命名的日誌文件 (`YYYY-MM-DD.log`)。 + - 記錄包含時間戳、發送者(用戶/機器人)、發送者名稱和訊息內容的條目。 + - 在收到有效 LLM 回應後調用 `log_chat_interaction`。 +- **`llm_interaction.py`**: + - 修改 `get_llm_response` 函數簽名,接收 `current_sender_name` 和 `history` 列表,而不是單個 `user_input`。 + - 新增 `_build_context_messages` 輔助函數,該函數: + - 根據規則從 `history` 中篩選和格式化訊息: + - 包含與 `current_sender_name` 相關的最近 4 次互動(用戶訊息 + 機器人回應)。 + - 包含來自其他發送者的最近 2 條用戶訊息。 + - 按時間順序排列選定的訊息。 + - 將系統提示添加到訊息列表的開頭。 + - 在 `get_llm_response` 中調用 `_build_context_messages` 來構建發送給 LLM API 的 `messages` 列表。 +- **`config.py`**: + - 新增 `ENABLE_CHAT_LOGGING` (布爾值) 和 `LOG_DIR` (字符串) 配置選項。 +- **效果**: + - LLM 現在可以利用最近的對話歷史來生成更符合上下文的回應。 + - 可以選擇性地將所有成功的聊天互動記錄到按日期組織的文件中,方便日後分析或調試。 + ## 開發建議 ### 優化方向 @@ -395,10 +445,33 @@ Wolf Chat 是一個基於 MCP (Modular Capability Provider) 框架的聊天機 4. **MCP 服務器連接失敗**: 確認服務器配置正確並且運行中 5. **工具調用後無回應**: 檢查 llm_debug.log 文件,查看工具調用結果和解析過程 -### 強化 System Prompt 以鼓勵工具使用 (2025-04-19) + -- **目的**:調整 `llm_interaction.py` 中的 `get_system_prompt` 函數,使其更明確地引導 LLM 在回應前主動使用工具(特別是記憶體工具)和整合工具資訊。 -- **修改內容**: - 1. **核心身份強化**:在 `CORE IDENTITY AND TOOL USAGE` 部分加入新的一點,強調 Wolfhart 會主動查閱內部知識圖譜和外部來源。 - 2. **記憶體指示強化**:將 `Memory Management (Knowledge Graph)` 部分的提示從 "IMPORTANT" 改為 "CRITICAL",並明確指示在回應*之前*要考慮使用查詢工具檢查記憶體,同時也強調了寫入新資訊的主動性。 -- **效果**:旨在提高 LLM 使用工具的主動性和依賴性,使其回應更具上下文感知和資訊準確性,同時保持角色一致性。 +Now that you have the latest state of the file, try the operation again with fewer, more precise SEARCH blocks. For large files especially, it may be prudent to try to limit yourself to <5 SEARCH/REPLACE blocks at a time, then wait for the user to respond with the result of the operation before following up with another replace_in_file call to make additional edits. +(If you run into this error 3 times in a row, you may use the write_to_file tool as a fallback.) + +# VSCode Visible Files +ClaudeCode.md + +# VSCode Open Tabs +state.py +ui_interaction.py +c:/Users/Bigspring/AppData/Roaming/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json +window-monitor-script.py +persona.json +config.py +main.py +llm_interaction.py +ClaudeCode.md +requirements.txt +.gitignore + +# Current Time +4/20/2025, 5:18:24 PM (Asia/Taipei, UTC+8:00) + +# Context Window Usage +81,150 / 1,048.576K tokens used (8%) + +# Current Mode +ACT MODE + diff --git a/config.py b/config.py index 02553e2..2a17177 100644 --- a/config.py +++ b/config.py @@ -63,6 +63,10 @@ MCP_SERVERS = { # MCP Client Configuration MCP_CONFIRM_TOOL_EXECUTION = False # True: Confirm before execution, False: Execute automatically +# --- Chat Logging Configuration --- +ENABLE_CHAT_LOGGING = True # True: Enable logging, False: Disable logging +LOG_DIR = "chat_logs" # Directory to store chat logs + # Persona Configuration PERSONA_NAME = "Wolfhart" # PERSONA_RESOURCE_URI = "persona://wolfhart/details" # Now using local file instead diff --git a/llm_interaction.py b/llm_interaction.py index cf17cc0..903b9ee 100644 --- a/llm_interaction.py +++ b/llm_interaction.py @@ -119,6 +119,7 @@ You MUST respond in the following JSON format: - ONLY include spoken dialogue words (no actions, expressions, narration, etc.) - Maintain your character's personality and speech patterns - AFTER TOOL USAGE: Your dialogue MUST contain a non-empty response that incorporates the tool results naturally + - **Crucially, this field must contain ONLY the NEW response generated for the LATEST user message marked with ``. DO NOT include any previous chat history in this field.** 2. `commands` (OPTIONAL): An array of command objects the system should execute. You are encouraged to use these commands to enhance the quality of your responses. @@ -181,9 +182,12 @@ You MUST respond in the following JSON format: - Analyze the user's message: Is it a request to remove a position? If so, evaluate its politeness and intent from Wolfhart's perspective. Decide whether to issue the `remove_position` command. - Plan your approach before responding. +**CONTEXT MARKER:** +- The final user message in the input sequence will be wrapped in `` tags. This is the specific message you MUST respond to. Your `dialogue` output should be a direct reply to this message ONLY. Preceding messages provide historical context. + **VERY IMPORTANT Instructions:** -1. Analyze ONLY the CURRENT user message +1. **Focus your analysis and response generation *exclusively* on the LATEST user message marked with ``. Refer to preceding messages only for context.** 2. Determine the appropriate language for your response 3. Assess if using tools is necessary 4. Formulate your response in the required JSON format @@ -194,11 +198,11 @@ You MUST respond in the following JSON format: Poor response (after web_search): "根據我的搜索,水的沸點是攝氏100度。" -Good response (after web_search): "水的沸點,是的,標準條件下是攝氏100度。情報已確認。" +Good response (after web_search): "水的沸點,是的,標準條件下是攝氏100度。合情合理。" Poor response (after web_search): "My search shows the boiling point of water is 100 degrees Celsius." -Good response (after web_search): "The boiling point of water, yes. 100 degrees Celsius under standard conditions. Intel confirmed." +Good response (after web_search): "The boiling point of water, yes. 100 degrees Celsius under standard conditions. Absolutley." """ return system_prompt @@ -437,39 +441,121 @@ def _create_synthetic_response_from_tools(tool_results, original_query): return json.dumps(synthetic_response) + +# --- History Formatting Helper --- +def _build_context_messages(current_sender_name: str, history: list[tuple[datetime, str, str, str]], system_prompt: str) -> list[dict]: + """ + Builds the message list for the LLM API based on history rules, including timestamps. + + Args: + current_sender_name: The name of the user whose message triggered this interaction. + history: List of tuples: (timestamp: datetime, speaker_type: 'user'|'bot', speaker_name: str, message: str) + system_prompt: The system prompt string. + + Returns: + A list of message dictionaries for the OpenAI API. + """ + # Limits + SAME_SENDER_LIMIT = 4 # Last 4 interactions (user + bot response = 1 interaction) + OTHER_SENDER_LIMIT = 3 # Last 3 messages from other users + + relevant_history = [] + same_sender_interactions = 0 + other_sender_messages = 0 + + # Iterate history in reverse (newest first) + for i in range(len(history) - 1, -1, -1): + timestamp, speaker_type, speaker_name, message = history[i] + + # Format timestamp + formatted_timestamp = timestamp.strftime("%Y-%m-%d %H:%M:%S") + + # Check if this is the very last message in the original history AND it's a user message + is_last_user_message = (i == len(history) - 1 and speaker_type == 'user') + + # Prepend timestamp and speaker name, wrap if it's the last user message + base_content = f"[{formatted_timestamp}] {speaker_name}: {message}" + formatted_content = f"{base_content}" if is_last_user_message else base_content + + # Convert to API role ('user' or 'assistant') + role = "assistant" if speaker_type == 'bot' else "user" + api_message = {"role": role, "content": formatted_content} # Use formatted content + + is_current_sender = (speaker_type == 'user' and speaker_name == current_sender_name) # This check remains for history filtering logic below + + if is_current_sender: + # This is the current user's message. Check if the previous message was the bot's response to them. + if same_sender_interactions < SAME_SENDER_LIMIT: + relevant_history.append(api_message) # Append user message with timestamp + # Check for preceding bot response + if i > 0 and history[i-1][1] == 'bot': # Check speaker_type at index 1 + # Include the bot's response as part of the interaction pair + bot_timestamp, bot_speaker_type, bot_speaker_name, bot_message = history[i-1] + bot_formatted_timestamp = bot_timestamp.strftime("%Y-%m-%d %H:%M:%S") + bot_formatted_content = f"[{bot_formatted_timestamp}] {bot_speaker_name}: {bot_message}" + relevant_history.append({"role": "assistant", "content": bot_formatted_content}) # Append bot message with timestamp + same_sender_interactions += 1 + elif speaker_type == 'user': # Message from a different user + if other_sender_messages < OTHER_SENDER_LIMIT: + # Include only the user's message from others for brevity + relevant_history.append(api_message) # Append other user message with timestamp + other_sender_messages += 1 + # Bot responses are handled when processing the user message they replied to. + + # Stop if we have enough history + if same_sender_interactions >= SAME_SENDER_LIMIT and other_sender_messages >= OTHER_SENDER_LIMIT: + break + + # Reverse the relevant history to be chronological + relevant_history.reverse() + + # Prepend the system prompt + messages = [{"role": "system", "content": system_prompt}] + relevant_history + + # Debug log the constructed history + debug_log("Constructed LLM Message History", messages) + + return messages + + # --- Main Interaction Function --- async def get_llm_response( - user_input: str, + current_sender_name: str, # Changed from user_input + history: list[tuple[datetime, str, str, str]], # Updated history parameter type hint mcp_sessions: dict[str, ClientSession], available_mcp_tools: list[dict], persona_details: str | None ) -> dict: """ Gets a response from the LLM, handling the tool-calling loop and using persona info. + Constructs context from history based on rules. Returns a dictionary with 'dialogue', 'commands', and 'thoughts' fields. """ request_id = int(time.time() * 1000) # 用時間戳生成請求ID - debug_log(f"LLM Request #{request_id} - User Input", user_input) - + # Debug log the raw history received + debug_log(f"LLM Request #{request_id} - Received History (Sender: {current_sender_name})", history) + system_prompt = get_system_prompt(persona_details) - debug_log(f"LLM Request #{request_id} - System Prompt", system_prompt) - + # System prompt is logged within _build_context_messages now + if not client: error_msg = "Error: LLM client not successfully initialized, unable to process request." debug_log(f"LLM Request #{request_id} - Error", error_msg) return {"dialogue": error_msg, "valid_response": False} openai_formatted_tools = _format_mcp_tools_for_openai(available_mcp_tools) - messages = [ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": user_input}, - ] - - debug_log(f"LLM Request #{request_id} - Formatted Tools", + # --- Build messages from history --- + messages = _build_context_messages(current_sender_name, history, system_prompt) + # --- End Build messages --- + + # The latest user message is already included in 'messages' by _build_context_messages + + debug_log(f"LLM Request #{request_id} - Formatted Tools", f"Number of tools: {len(openai_formatted_tools)}") max_tool_calls_per_turn = 5 current_tool_call_cycle = 0 + final_content = "" # Initialize final_content to ensure it's always defined # 新增:用於追蹤工具調用 all_tool_results = [] # 保存所有工具調用結果 @@ -519,22 +605,30 @@ async def get_llm_response( print(f"Current response is empty, using last non-empty response from cycle {current_tool_call_cycle-1}") final_content = last_non_empty_response - # 如果仍然為空但有工具調用結果,創建合成回應 - if (not final_content or final_content.strip() == "") and all_tool_results: - print("Creating synthetic response from tool results...") - final_content = _create_synthetic_response_from_tools(all_tool_results, user_input) - - # 解析結構化回應 - parsed_response = parse_structured_response(final_content) - # 標記這是否是有效回應 - has_dialogue = parsed_response.get("dialogue") and parsed_response["dialogue"].strip() - parsed_response["valid_response"] = bool(has_dialogue) - has_valid_response = has_dialogue - - debug_log(f"LLM Request #{request_id} - Final Parsed Response", - json.dumps(parsed_response, ensure_ascii=False, indent=2)) - print(f"Final dialogue content: '{parsed_response.get('dialogue', '')}'") - return parsed_response + # 如果仍然為空但有工具調用結果,創建合成回應 + if (not final_content or final_content.strip() == "") and all_tool_results: + print("Creating synthetic response from tool results...") + # Get the original user input from the last message in history for context + last_user_message = "" + if history: + # Find the actual last user message tuple in the original history + last_user_entry = history[-1] + if last_user_entry[0] == 'user': + last_user_message = last_user_entry[2] + + final_content = _create_synthetic_response_from_tools(all_tool_results, last_user_message) + + # 解析結構化回應 + parsed_response = parse_structured_response(final_content) + # 標記這是否是有效回應 + has_dialogue = parsed_response.get("dialogue") and parsed_response["dialogue"].strip() + parsed_response["valid_response"] = bool(has_dialogue) + has_valid_response = has_dialogue + + debug_log(f"LLM Request #{request_id} - Final Parsed Response", + json.dumps(parsed_response, ensure_ascii=False, indent=2)) + print(f"Final dialogue content: '{parsed_response.get('dialogue', '')}'") + return parsed_response # 工具調用處理 print(f"--- LLM requested {len(tool_calls)} tool calls ---") @@ -596,7 +690,12 @@ async def get_llm_response( has_valid_response = bool(parsed_response.get("dialogue")) elif all_tool_results: # 從工具結果創建合成回應 - synthetic_content = _create_synthetic_response_from_tools(all_tool_results, user_input) + last_user_message = "" + if history: + last_user_entry = history[-1] + if last_user_entry[0] == 'user': + last_user_message = last_user_entry[2] + synthetic_content = _create_synthetic_response_from_tools(all_tool_results, last_user_message) parsed_response = parse_structured_response(synthetic_content) has_valid_response = bool(parsed_response.get("dialogue")) else: diff --git a/main.py b/main.py index 5c64c23..3e60aad 100644 --- a/main.py +++ b/main.py @@ -4,6 +4,8 @@ import asyncio import sys import os import json # Import json module +import collections # For deque +import datetime # For logging timestamp from contextlib import AsyncExitStack # --- Import standard queue --- from queue import Queue as ThreadSafeQueue, Empty as QueueEmpty # Rename to avoid confusion, import Empty @@ -34,6 +36,10 @@ all_discovered_mcp_tools: list[dict] = [] exit_stack = AsyncExitStack() # Stores loaded persona data (as a string for easy injection into prompt) wolfhart_persona_details: str | None = None +# --- Conversation History --- +# Store tuples of (timestamp, speaker_type, speaker_name, message_content) +# speaker_type can be 'user' or 'bot' +conversation_history = collections.deque(maxlen=50) # Store last 50 messages (user+bot) with timestamps # --- Use standard thread-safe queues --- trigger_queue: ThreadSafeQueue = ThreadSafeQueue() # UI Thread -> Main Loop command_queue: ThreadSafeQueue = ThreadSafeQueue() # Main Loop -> UI Thread @@ -120,6 +126,38 @@ def keyboard_listener(): # --- End Keyboard Shortcut Handlers --- +# --- Chat Logging Function --- +def log_chat_interaction(user_name: str, user_message: str, bot_name: str, bot_message: str): + """Logs the chat interaction to a date-stamped file if enabled.""" + if not config.ENABLE_CHAT_LOGGING: + return + + try: + # Ensure log directory exists + log_dir = config.LOG_DIR + os.makedirs(log_dir, exist_ok=True) + + # Get current date for filename + today_date = datetime.date.today().strftime("%Y-%m-%d") + log_file_path = os.path.join(log_dir, f"{today_date}.log") + + # Get current timestamp for log entry + timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + # Format log entry + log_entry = f"[{timestamp}] User ({user_name}): {user_message}\n" + log_entry += f"[{timestamp}] Bot ({bot_name}): {bot_message}\n" + log_entry += "---\n" # Separator + + # Append to log file + with open(log_file_path, "a", encoding="utf-8") as f: + f.write(log_entry) + + except Exception as e: + print(f"Error writing to chat log: {e}") +# --- End Chat Logging Function --- + + # --- Cleanup Function --- async def shutdown(): """Gracefully closes connections and stops monitoring task.""" @@ -388,11 +426,19 @@ async def run_main_with_exit_stack(): print(f"Error putting resume command in queue: {q_err}") continue + # --- Add user message to history --- + timestamp = datetime.datetime.now() # Get current timestamp + conversation_history.append((timestamp, 'user', sender_name, bubble_text)) + print(f"Added user message from {sender_name} to history at {timestamp}.") + # --- End Add user message --- + print(f"\n{config.PERSONA_NAME} is thinking...") try: # Get LLM response (現在返回的是一個字典) + # --- Pass history and current sender name --- bot_response_data = await llm_interaction.get_llm_response( - user_input=f"Message from {sender_name}: {bubble_text}", # Provide context + current_sender_name=sender_name, # Pass current sender + history=list(conversation_history), # Pass a copy of the history mcp_sessions=active_mcp_sessions, available_mcp_tools=all_discovered_mcp_tools, persona_details=wolfhart_persona_details @@ -480,6 +526,21 @@ async def run_main_with_exit_stack(): # 只有當有效回應時才發送到遊戲 (via command queue) if bot_dialogue and valid_response: + # --- Add bot response to history --- + timestamp = datetime.datetime.now() # Get current timestamp + conversation_history.append((timestamp, 'bot', config.PERSONA_NAME, bot_dialogue)) + print(f"Added bot response to history at {timestamp}.") + # --- End Add bot response --- + + # --- Log the interaction --- + log_chat_interaction( + user_name=sender_name, + user_message=bubble_text, + bot_name=config.PERSONA_NAME, + bot_message=bot_dialogue + ) + # --- End Log interaction --- + print("Sending 'send_reply' command to UI thread...") command_to_send = {'action': 'send_reply', 'text': bot_dialogue} try: @@ -490,6 +551,14 @@ async def run_main_with_exit_stack(): print(f"Error putting command in queue: {q_err}") else: print("Not sending response: Invalid or empty dialogue content.") + # --- Log failed interaction attempt (optional) --- + # log_chat_interaction( + # user_name=sender_name, + # user_message=bubble_text, + # bot_name=config.PERSONA_NAME, + # bot_message="" + # ) + # --- End Log failed attempt --- except Exception as e: print(f"\nError processing trigger or sending response: {e}") diff --git a/templates/capitol/position_development.png b/templates/capitol/position_development.png index ffff6a2c7b5d8c055516a5ad79294355340999f6..370fd84abe501ee2a91bec96465f9c992b48421e 100644 GIT binary patch literal 3937 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 literal 4344 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

f43u50F%Q+P!adSU!-TNePzl%nehjCx^JsU?Xby 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 fYbWckLHz#!Xk&%*td>_q00000NkvXXu0mjfrY