From 26b22c6e8f56e1ba0d661402cca4e3ab08a4feb0 Mon Sep 17 00:00:00 2001 From: HuangHai <10402852@qq.com> Date: Mon, 26 Jan 2026 20:05:00 +0800 Subject: [PATCH] 'commit' --- WeiXin/T2_ChatMonitor.py | 30 +++++++ WeiXin/WxUtil.py | 85 +++++++++++------- .../T2_ChatMonitor.cpython-310.pyc | Bin 0 -> 8846 bytes WeiXin/__pycache__/WxUtil.cpython-310.pyc | Bin 23816 -> 24432 bytes 4 files changed, 85 insertions(+), 30 deletions(-) create mode 100644 WeiXin/__pycache__/T2_ChatMonitor.cpython-310.pyc diff --git a/WeiXin/T2_ChatMonitor.py b/WeiXin/T2_ChatMonitor.py index e4a0979..ba51a2e 100644 --- a/WeiXin/T2_ChatMonitor.py +++ b/WeiXin/T2_ChatMonitor.py @@ -190,6 +190,36 @@ class ChatMonitorBot: logger.info("-" * 20) logger.info("="*50 + "\n") + # --- LLM 总结 --- + logger.info("🤖 正在请求 LLM 生成对话摘要...") + chat_history_text = "" + for msg in self.dialogue_log: + sender = msg.get('sender', '未知') + content = msg.get('content', '') + type_str = "[语音]" if msg.get('type') == 'voice' else "[文字]" + chat_history_text += f"{sender}{type_str}: {content}\n" + + prompt = ( + "请根据以下微信对话记录,总结归纳双方交流的主要信息点。\n" + "要求:\n" + "1. 简明扼要,分点列出。\n" + "2. 明确指出双方达成的一致或待解决的问题。\n" + "3. 忽略无关的寒暄。\n\n" + f"对话记录:\n{chat_history_text}" + ) + + try: + full_response = "" + async for chunk in get_llm_response(prompt, stream=True): + full_response += chunk + + logger.info("\n" + "="*20 + " 对话摘要 (LLM) " + "="*20) + logger.info(full_response) + logger.info("="*55 + "\n") + + except Exception as e: + logger.error(f"LLM 摘要生成失败: {e}") + # 初始化最后处理的消息哈希,避免重复回复第一条 last_msg = self.dialogue_log[-1] # last_msg 是字典,需要转字符串再 encode diff --git a/WeiXin/WxUtil.py b/WeiXin/WxUtil.py index 1f69cc5..ce1c05d 100644 --- a/WeiXin/WxUtil.py +++ b/WeiXin/WxUtil.py @@ -1,6 +1,7 @@ # coding=utf-8 import uiautomator2 as u2 import time +import asyncio import logging import sys import os @@ -392,6 +393,9 @@ async def analyze_chat_image(image_path, output_path, device=None, target_name=" # 记录 Peek-and-Restore 过程中抓取到的语音内容 {y_coord: content} captured_voice_contents = {} + # 初始化异步任务列表 + analyze_chat_image._ocr_tasks = [] + while loop_count < MAX_LOOPS: loop_count += 1 logger.info(f"--- 分析循环 第 {loop_count} 次 ---") @@ -490,40 +494,40 @@ async def analyze_chat_image(image_path, output_path, device=None, target_name=" logger.info("等待语音转文字完成...") time.sleep(3.0) # 缩短等待时间 (原5.0s) - # --- Peek-and-Restore 逻辑 --- + # --- Peek-and-Restore 逻辑 (异步优化版) --- - # 1. 截图读取内容 + # 1. 截图 (但不立即 OCR,而是丢给异步任务) peek_shot = get_next_debug_path("step_peek_content") d.screenshot(peek_shot) - logger.info("正在读取转换后的语音内容...") - peek_messages, _, _ = _scan_chat_messages(peek_shot) + logger.info("已截图,启动异步OCR任务以提取内容...") - # 2. 查找并保存内容 - found_content = None - current_voice_in_peek = None - for pm in peek_messages: - if pm['type'] == 'voice' and pm.get('is_converted'): - # 简单匹配:Y坐标接近 (容差 50px) - # 注意:如果文字展开,下方元素会被推下去,但当前语音本身的位置变化取决于展开方向 - # 通常语音条下方展开文字,语音条本身Y坐标变化不大 - if abs(pm['y'] - vy) < 50: - found_content = pm.get('content') - current_voice_in_peek = pm - break - - if found_content: - logger.info(f"✅ [Peek] 成功抓取语音内容: {found_content}") - captured_voice_contents[target['y']] = found_content - else: - logger.warning("⚠️ [Peek] 未能抓取到语音内容 (可能识别失败)") - - # 3. 还原状态 (取消转文字) - logger.info("准备还原状态 (取消转文字)...") - click_x, click_y = vx, vy - if current_voice_in_peek: - click_x, click_y = int(current_voice_in_peek['center'][0]), int(current_voice_in_peek['center'][1]) + async def _async_ocr_task(img_path, target_y): + """内部异步任务:在线程池中运行 OCR""" + loop = asyncio.get_running_loop() + # 在默认执行器(线程池)中运行耗时的 _scan_chat_messages + msgs, _, _ = await loop.run_in_executor(None, _scan_chat_messages, img_path) - d.long_click(click_x, click_y, 1.0) # 缩短按压时间 + found = None + for pm in msgs: + if pm['type'] == 'voice' and pm.get('is_converted'): + if abs(pm['y'] - target_y) < 50: + found = pm.get('content') + break + return target_y, found + + # 创建并保存任务 + task = asyncio.create_task(_async_ocr_task(peek_shot, vy)) + # 我们需要一个列表来保存任务,这里临时利用 list + if not hasattr(analyze_chat_image, "_ocr_tasks"): + analyze_chat_image._ocr_tasks = [] + analyze_chat_image._ocr_tasks.append(task) + + # 2. 立即还原状态 (取消转文字) + # 注意:由于 OCR 还没出结果,我们无法精确定位展开后的文字位置 + # 但通常点击原语音气泡位置 (vx, vy) 也能触发菜单 + logger.info("准备还原状态 (取消转文字)...") + + d.long_click(vx, vy, 1.0) # 盲点原坐标 logger.info("正在快速寻找'隐藏文字'按钮...") cancel_template = os.path.join(TEMPLATE_DIR, "cancel_zhuan_wen_zi.jpg") @@ -547,7 +551,7 @@ async def analyze_chat_image(image_path, output_path, device=None, target_name=" else: logger.warning("❌ 未找到'隐藏文字'按钮,无法还原状态!(后续可能导致重复处理)") - # 4. 准备下一次循环 + # 3. 准备下一次循环 # 重新截图,因为界面可能微调,或者只是恢复了 next_screenshot = get_next_debug_path("step_restored") d.screenshot(next_screenshot) @@ -564,6 +568,17 @@ async def analyze_chat_image(image_path, output_path, device=None, target_name=" logger.info("跳过当前语音,继续扫描...") continue + # 循环结束后,等待所有异步 OCR 任务完成 + if hasattr(analyze_chat_image, "_ocr_tasks") and analyze_chat_image._ocr_tasks: + logger.info(f"等待 {len(analyze_chat_image._ocr_tasks)} 个异步 OCR 任务完成...") + results = await asyncio.gather(*analyze_chat_image._ocr_tasks) + for y, content in results: + if content: + captured_voice_contents[y] = content + logger.info(f"✅ [Async OCR] 异步获取到语音内容 (y={y}): {content}") + # 清空任务列表 + analyze_chat_image._ocr_tasks = [] + # 循环结束,返回最后一次分析的结果 if not final_messages: # 如果循环因为 max_loops 退出,确保有结果 final_messages = messages @@ -612,6 +627,16 @@ async def analyze_chat_image(image_path, output_path, device=None, target_name=" # 统一生成 dialogue_log for msg in final_messages: + # 尝试注入异步获取的语音内容 + if msg['type'] == 'voice': + # 模糊匹配 Y 坐标 + for y_key, content in captured_voice_contents.items(): + if abs(msg['y'] - y_key) < 20: + msg['is_converted'] = True + msg['content'] = content + logger.info(f"注入语音内容到最终消息列表: {content}") + break + # 只添加有内容的文本消息,或已转换且有内容的语音消息 if msg['type'] == 'text' and msg.get('content'): dialogue_log.append(msg) diff --git a/WeiXin/__pycache__/T2_ChatMonitor.cpython-310.pyc b/WeiXin/__pycache__/T2_ChatMonitor.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..234ca5ea712c0314b18acfd152f4d4daeb0d39ec GIT binary patch literal 8846 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 literal 0 HcmV?d00001 diff --git a/WeiXin/__pycache__/WxUtil.cpython-310.pyc b/WeiXin/__pycache__/WxUtil.cpython-310.pyc index 2508ae47d5564e4ea861114ba22e82631d526f9c..a2634e73fc07633d32f5114b3ec7b1ff87de974d 100644 GIT binary patch delta 7029 zcma)B32>Xob^iCq%?muh`vNbK5D)RvO-j6Vm=Pi5eWN}0tEup;)f;DKLR%etvOR>{5ahm3^Q>RS_lcugKQqoM@WXA1GlgT7~Z^1)W zl-``_-qeednJ@9m|Tx5%&GCTYRc)FcW1k`A2k-TLZNX+`A4r)sxEEmWe!Z&hsK zo1`SE=CS0tlsjmunl?}HpJG?-s%b3wA;Q}6JD0B7=LwNS&kQ94zOp}aJX6UObq0Ud zxI=LeNp(z1O15eo%Yk1mHB!?#8ETDldDI*QwfT1#OBh}?Ss>a9)dFgnmzAQGk`+qD zOQltZm-sWc6^GHR#krzNGFX3)&jQAw$wS=0d)m9&}W z&|DCyqIonQYSz#eT11PXrka+}QmCn+PFefU!dE?d*pI;4l#?>nl%iy~W zTQjOaDr5#?HDr=B!m?sd8(t+u;X6&+H|0V-O9!IZu%mmzqaBSCm1SVPAeq3$?Owm? zVQ%VUs#goL$vXaysfCpDk4^7yb%MYimzkBIpwSl?39@8hSvi(1{R2DPJ-Y^(9jjKg zCRFp^n~Sr$p(SJmk|Y}q>2eBTRj~%ko9N*!Yg=Ijb`-izDq}$ww;c6^JetPX8ou3@ z(^U-ZE@PZfC6%fV`Mjz-q%xn!&uXz_8Io1AVlZjmU?2eFa3|#N=g-)xNki-%TNfd} z<#O`D!>Ci(WTnVeBdI~+0s=#=hfkcGjFU-WKrsc>6K)!p)#kfxWrRE<>XIS1*YEQl zqx`L87g-;>k^Db0Y3K2@4s$!IZs$(>!=#R1u=k|naG4mm@T8Rg+TP#31uM6rWs^7P z2Ovy-&k@ys&mr0=^MFU{k?^P+XXGJ1nZBMJioKHVwK+RL$CS+zroLdq*u>~mDpRR@ zBIpZfSY*AtxuBQq;3o^7tV7W&gbgG<&G2!NI5}}gWLVq$p7A5pv+cXQrLdc9h@B|3 z5^{{6Dr(5|qr8EM;Su0rjNdNGZR|nzGe~v;QBu1GhKC34clYh;AMPDw*m^qvWVvTM zw-h(+ScmdnY(rdJhTEsG;`lOAtcmWuJ**q*ZsQEygdhBUxIJuu&*IEeeQYO>7QaU_ z_&`Zvb|M6!C2ngQRRikD3AW81iR|ZnDDQAyXm7)g+mLKW(ut%C$tEB!3)={vxH+T-D1`fFY-mA} znEhCOSuLqQi4~Jbgrif)MOKO!<17l~Fd99@kC)fwpi*`Q$>)(QspUnR3i(gUGdhHY zKRM`e(DktleXWwKjO|7x_aPCH>f)sp-K2thD>~~GLnwSCZuABxCgVoGFQBsfQEm^A zxFMu!Y%do3kqq$nD=N%`$nN96sW?F%<;N?tAnngot|3GGrOL|U1K9W=65%|8pvdeb z{3h8}{-2fS%?dUi^*V||cO+78qv~y@MNza-lk>p3o z2oW2S^)YEwia2y5%>acj@TQuq&FE$pNIIqoxavr_8rTFf*&y?+Mp7##J&O?v-4VJe z^puE_Jz||DfuQc7GW5*VlJ#6#3d4rdG+8Mbu_Vipyh#2GnM(ngqG-M(ki|hb^*k6r znVt`{{Gv(JrvgHDJx|Xck!i&_LjovT`NxvFX)X=rXx03()Ec3|K9)Y0p=HwQc{x%* zNu*HAn#-D(B1PgGmGq)KiPpf>6LwT{P!}Ln49c=~F-|QV>jZ9icvuk=cDP=E9ZH7t-aCmqbbhYV~@F7;0%0J`GD_&C^YKiC#Jl{_80s z22+Iax+O!ov_^0a+6YcMg_BNEgXlFO2ZxiE(^8~N%bzRI%XFunKP}IbNV!(1$+UG^ zo+Y7ciFnuKF~eMu9xY!{upYU4z_>w3EW@}FV5|TbD?b6_DvB~o0u(oagH^)8D$zSf zuhJ`)P^{D|0ma`XP>g;GjJH5YW*Y#;$`u$ZS72NdsTRm=*4GFatATF01I0CZwLmdd zpqMJAYb!buFhRd!+9sTG(DvyliPQ)`1+;oi#0=hV1Me-zT|#ub5JiAHkb~M~C}s8q z1QFc)f^eo&OTx0q!mgzSBY&&}>&{ENV3F7%39j1H-$r-9KM!e}ofewr}ZmoTYY!9bf> zcXmJm?mQ-g>_>X$Tq)f(Pe8**dfJ?GUIM9Va3fk5snyD0S%N&*>+&E!>29$Meh6-5 z(p_y5y$`W|04o?d{ucbc2CGv&9ni|BrH70S6czks}R(lBrx+6DxHyMC>|!-6zDc1mdgG`=x-HNcfKq zMj9Rjcq5Iv6$~}&)-f9$T3tkSt60t&ZIZy|aHL6ZqRZGs4UDb;QVxJL3p|B^C>V}+E-Bv!z#G^}y=e(}%jkFBAK-U5;`y^kKmi`CMul`okk0ba{zTZRxs_?{pqB-f$X!G=6SdbqZ((Ttla zJHuaG_h7v^XW&6boJ_7PAC1Nn2NiL`5XY0#Ao2olZ0xU}LE*=id$3PCbX~%;%;Wq@ z<67e586^&Wt8u+mgKoopjPj}`SKf;9c#1FNo?t<*8VafO>wKiCojfP{ywS9w4iC`c zL@R0{qvIU(vzP!Pj^}^Tqna!3$f(Dd{qD*oR#~^2r`=@Egr%T7`*KIU({% zx$7{R5T2$ABo;(9E&O83IN8jTTZ_~1q&sDFH8eE12pi@bT4%{&{{7a6qiBEOo8MTR zd2!*{w|{m0?859T3y)u3xc<4ttCt442X9=vw($7;jcb<{pPXHI^67=opILbMP0->+ zL(6|~xRLPa^8x%qc#eF8Xwn>k9Z%NfIhhj8piA?H#DWB88AHSvF~Y=~bVC#tg|Y}7 zazv^b;PkLjH_sUs*O6N|6T-FV~bt#@W`&7X}rVUT;o`BdDLX-t zIN7?)af>H38SwgoO8Tg(xmh?6@C8QQ{$OxIu|o;2LvHn?>J7s+mWeZ=hyuLfxGaj* zH$E!vz=~D#FjO$97)4*>_`c9+h}l3+F^vSn0jkIocgJ? zKJqE6Tp$}|E6E`al0;-;B~T|#)$*4ciwf`R7EfBy5Q=@g{xuTCdAoV_v4ywKIKz16 zcHVg7#l*}bD$WFQ3ooBtoSB6w4`U2{bm5HiU?+lsB8Qy`@muG=iNP`RSGQiddiyJ} zM3gvdCfivf^a-Z|S-ko@Ou%C7^1_)*&TuJO<8N;)ZC_1paf#Rt4Z~s*!dWQetl#EL zbV?}Q;+!&oFYGJO$U3-rQ?vU2O!g6oSoSz}!we6*fFu;(=`##r@xrN_*Jc)9pILn5 zm4%sSZp~lb;$)BVu}x=(su>0s`x20NrYGR>Pfn?B?@^ED_KkZ+RrWHripxs{av~7K zt>ytNeh~>r@+6WANDlGF&7~=4fsGqA-?+-M_`%I}(e>E&5)dWX%T$l1ig{;ivHCBN zAbyH@6y8~H{(tS)#ymE-O4(?ZCrTi)?7fbo8z)fJ7 zAK>1$GTCh4r`tZ4VwKbFDYBWQkR%gd+rBB&VaOwyP&7l}=Mdv5v&@gQZ(5rsr^`7| zWhLpvNQ}QLu%wYSkRQfVMRF2JmmQET)>HED*{l4u_7AHZU~Ed>;&i356!ZYYm1QHb zBgsLM$KTvmUMXM{6O0q!lH+9Lk6udnAGQ^c^Soet1^JHHAO^SJ1Q@q;9E7Xr(;eGt zh3308iPxWd=<3KP&+`0E7kM|<-PuVpUje(mRx3P??O`40ScpH})o8}lWM2NGu62H) zUPPUkc@YC5;8p{3H5x1;8rVMUeh~>?apP968qiecKDol(F6a zaRpoU4_N#F$C5A{c5fi(Vx{-pFqnH$@nLQoI7D9LBLiP1A#U2c-~6{&{at=!@4u0+^PUIJlkf2l z9vIlS8XKz_kcOgmBrzmsks!jX3CTB*{52AR-nW5Mtnd`$*3^?46A@O*PY!k#id82= z#J7^N6j)-Eu&DlQ@Zljbk=Ibsb<~lhjZe5kuU~J-#s^U)t$UziW z{Ph03u6qQ-YP`UaAvodot0T+tfco!_eg4b+1>`LM&HnA=quAyHB{KO1KcG}b72}BC zGs>QWKCr~TfnDE3-g3t#C3ZBCm08#>rW#jYmq~Ph|8@~e0Y2fwYd)wGTL$|Uh$yxM z>i3uzaZE}b@iAET6O;Kmw;rsI{ySc{utz+jC440eqt``-U5!dg z*yn*`)wl0tpSQb5xjmbwmA)mK97#Q)5D#l%V`UVsm9NqfC zAZ)pqFYw0PIn?X!=^XA9{~00jr4qG^q!Ia&j70%#IdWA<1mf$FJA`B&2_nwEgXCSf zw@d5?$ckjc3}+uA`3MODct;XsU=erBxV3FONW*@08-Y@27>Jed*AI2uoz_%qPEtWq MNz#U-@wB!72T!p6@c;k- delta 6474 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