This commit is contained in:
HuangHai
2026-01-25 13:42:17 +08:00
parent 07c65bed47
commit f9c9181b73
16 changed files with 386 additions and 47 deletions

View File

@@ -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