'commit'
This commit is contained in:
163
WeiXin/WxUtil.py
163
WeiXin/WxUtil.py
@@ -26,6 +26,50 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(level
|
||||
logger = logging.getLogger("WxUtil")
|
||||
|
||||
|
||||
async def get_vlm_json(image_path, prompt):
|
||||
"""
|
||||
通用 VLM 分析函数,返回 JSON 数据 (自动处理归一化坐标的反归一化)
|
||||
"""
|
||||
try:
|
||||
# 调用 VLM
|
||||
response = await vlm_kit.analyze_image(image_path, prompt)
|
||||
json_str = vlm_kit.extract_json(response)
|
||||
result_data = json.loads(json_str)
|
||||
|
||||
# 获取图片尺寸进行坐标反归一化
|
||||
try:
|
||||
from PIL import Image
|
||||
with Image.open(image_path) as img:
|
||||
width, height = img.size
|
||||
|
||||
# 定义反归一化函数
|
||||
def denormalize(point):
|
||||
if not point or len(point) != 2:
|
||||
return point
|
||||
return [int(point[0] / 1000 * width), int(point[1] / 1000 * height)]
|
||||
|
||||
# 递归遍历字典进行反归一化 (仅针对常见坐标字段 center, input_box)
|
||||
def recursive_denormalize(data):
|
||||
if isinstance(data, dict):
|
||||
for key, value in data.items():
|
||||
if key in ["center", "input_box", "coordinates"] and isinstance(value, list) and len(value) == 2:
|
||||
data[key] = denormalize(value)
|
||||
elif isinstance(value, (dict, list)):
|
||||
recursive_denormalize(value)
|
||||
elif isinstance(data, list):
|
||||
for item in data:
|
||||
recursive_denormalize(item)
|
||||
|
||||
recursive_denormalize(result_data)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"坐标反归一化失败: {e},将使用原始坐标")
|
||||
|
||||
return result_data
|
||||
except Exception as e:
|
||||
logger.error(f"VLM Analysis Failed: {e}", exc_info=True)
|
||||
return None
|
||||
|
||||
async def get_vlm_analysis(image_path):
|
||||
"""
|
||||
仅调用 VLM 分析图片,返回原始 JSON 数据 (dict)
|
||||
@@ -37,10 +81,14 @@ async def get_vlm_analysis(image_path):
|
||||
请分析这张微信聊天截图。
|
||||
|
||||
【核心任务】
|
||||
识别图中的【语音消息气泡】和【文本消息气泡】。
|
||||
识别图中的【语音消息气泡】和【文本消息气泡】,并区分【发送者】。
|
||||
|
||||
【重要判别规则】
|
||||
1. 🔊 **语音消息 (Voice)**:
|
||||
1. 👤 **发送者 (Sender)**:
|
||||
- **对方 (Other)**:气泡在屏幕**左侧**,通常为白色或灰色,头像在左边。
|
||||
- **我 (Me)**:气泡在屏幕**右侧**,通常为绿色,头像在右边。
|
||||
|
||||
2. 🔊 **语音消息 (Voice)**:
|
||||
- **视觉特征**:
|
||||
- **高度**:固定(单行)。
|
||||
- **宽度**:随时长(1"~60")变化。
|
||||
@@ -49,9 +97,12 @@ async def get_vlm_analysis(image_path):
|
||||
- **内容**:气泡内**只有一个**表示时长的数字(如 `8"`)和一个声波图标。
|
||||
- **绝对排除**:凡是包含汉字、长句子的气泡,**统统不是**语音消息。
|
||||
|
||||
2. 📝 **文本消息 (Text)**:
|
||||
3. 📝 **文本消息 (Text)**:
|
||||
- **视觉特征**:气泡内包含汉字、标点符号、表情等文本内容。
|
||||
|
||||
4. 🔴 **未读状态 (Unread)**:
|
||||
- **特征**:语音气泡右上角或附近有一个明显的**小红点**。
|
||||
|
||||
【坐标系统】
|
||||
**必须使用 [0-1000] 的归一化坐标系。**
|
||||
- 左上角为 [0, 0],右下角为 [1000, 1000]。
|
||||
@@ -65,12 +116,15 @@ async def get_vlm_analysis(image_path):
|
||||
"messages": [
|
||||
{
|
||||
"type": "voice",
|
||||
"sender": "对方" | "我",
|
||||
"status": "converted" | "unconverted",
|
||||
"is_unread": true | false,
|
||||
"center": [x, y],
|
||||
"content": "8\""
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"sender": "对方" | "我",
|
||||
"center": [x, y],
|
||||
"content": "这里是文本内容"
|
||||
}
|
||||
@@ -79,7 +133,8 @@ async def get_vlm_analysis(image_path):
|
||||
注意:
|
||||
1. 坐标 `center` 和 `input_box` 必须是 [0-1000] 的归一化坐标。
|
||||
2. `status` 判断:如果语音气泡的正下方紧挨着一条文本消息(通常是转换出的文字),则为 `converted`,否则为 `unconverted`。
|
||||
3. 请按从上到下的顺序输出所有消息。
|
||||
3. `is_unread` 判断:如果有红点则为 true,否则为 false (仅针对语音消息)。
|
||||
4. 请按从上到下的顺序输出所有消息。
|
||||
"""
|
||||
|
||||
try:
|
||||
@@ -307,6 +362,36 @@ def find_input_box_center(image_path):
|
||||
logger.error(f"find_input_box_center error: {e}")
|
||||
return (540, 2100), None
|
||||
|
||||
def find_template_match(screen_path, template_path, threshold=0.8):
|
||||
"""
|
||||
使用 OpenCV 模板匹配寻找按钮中心坐标
|
||||
"""
|
||||
try:
|
||||
if not os.path.exists(template_path):
|
||||
logger.error(f"Template file not found: {template_path}")
|
||||
return None
|
||||
|
||||
img = cv2.imread(screen_path)
|
||||
template = cv2.imread(template_path)
|
||||
if img is None or template is None:
|
||||
return None
|
||||
|
||||
h, w = template.shape[:2]
|
||||
res = cv2.matchTemplate(img, template, cv2.TM_CCOEFF_NORMED)
|
||||
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
|
||||
|
||||
if max_val >= threshold:
|
||||
center_x = max_loc[0] + w // 2
|
||||
center_y = max_loc[1] + h // 2
|
||||
logger.info(f"Template matched! Score: {max_val:.2f}, Center: ({center_x}, {center_y})")
|
||||
return (center_x, center_y)
|
||||
|
||||
logger.info(f"Template not matched. Max score: {max_val:.2f}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Template matching failed: {e}")
|
||||
return None
|
||||
|
||||
def perform_input_action(d, center_point, text, auto_send=True):
|
||||
"""
|
||||
执行输入操作
|
||||
@@ -347,14 +432,74 @@ def perform_input_action(d, center_point, text, auto_send=True):
|
||||
|
||||
# 3. 发送
|
||||
if auto_send:
|
||||
if d(text="发送").exists:
|
||||
d(text="发送").click()
|
||||
logger.info("Clicked '发送'")
|
||||
# 优先使用模板匹配寻找“发送”按钮
|
||||
logger.info("尝试使用模板匹配寻找'发送'按钮...")
|
||||
tmp_screen = os.path.join(os.path.dirname(os.path.abspath(__file__)), "temp_send_check.jpg")
|
||||
d.screenshot(tmp_screen)
|
||||
|
||||
# 使用绝对路径
|
||||
template_path = r"d:\dsWork\aiData\WeiXin\Templates\send.jpg"
|
||||
send_btn_pos = find_template_match(tmp_screen, template_path, threshold=0.7) # 稍微降低阈值以提高召回
|
||||
|
||||
if send_btn_pos:
|
||||
logger.info(f"通过模板匹配找到发送按钮: {send_btn_pos}, 点击...")
|
||||
d.click(send_btn_pos[0], send_btn_pos[1])
|
||||
else:
|
||||
d.press("enter")
|
||||
logger.info("Pressed Enter")
|
||||
logger.warning("模板匹配未找到发送按钮,尝试原生控件查找...")
|
||||
if d(text="发送").exists:
|
||||
d(text="发送").click()
|
||||
logger.info("Clicked '发送'")
|
||||
else:
|
||||
d.press("enter")
|
||||
logger.info("Pressed Enter")
|
||||
|
||||
# 清理临时文件
|
||||
if os.path.exists(tmp_screen):
|
||||
try:
|
||||
os.remove(tmp_screen)
|
||||
except:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"perform_input_action error: {e}")
|
||||
|
||||
def match_template_center(image_path, template_path, threshold=0.8):
|
||||
"""
|
||||
使用 OpenCV 模板匹配寻找目标图片中心坐标
|
||||
"""
|
||||
try:
|
||||
if not os.path.exists(image_path) or not os.path.exists(template_path):
|
||||
logger.error(f"Image or template not found: {image_path}, {template_path}")
|
||||
return None
|
||||
|
||||
img = cv2.imread(image_path)
|
||||
template = cv2.imread(template_path)
|
||||
|
||||
if img is None or template is None:
|
||||
logger.error("Failed to read image or template")
|
||||
return None
|
||||
|
||||
# 转换为灰度图进行匹配
|
||||
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
||||
template_gray = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY)
|
||||
|
||||
# 模板匹配
|
||||
result = cv2.matchTemplate(img_gray, template_gray, cv2.TM_CCOEFF_NORMED)
|
||||
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
|
||||
|
||||
if max_val >= threshold:
|
||||
h, w = template_gray.shape
|
||||
top_left = max_loc
|
||||
center_x = int(top_left[0] + w / 2)
|
||||
center_y = int(top_left[1] + h / 2)
|
||||
logger.info(f"Template matched with confidence {max_val:.2f} at ({center_x}, {center_y})")
|
||||
return (center_x, center_y)
|
||||
else:
|
||||
logger.warning(f"Template match failed. Max confidence: {max_val:.2f} < Threshold: {threshold}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"match_template_center error: {e}")
|
||||
return None
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user