This commit is contained in:
HuangHai
2026-01-12 22:04:41 +08:00
parent 231f52d3ee
commit de62305a28
11 changed files with 302 additions and 189 deletions

View File

@@ -78,6 +78,76 @@ class ReadImageKit:
"严格返回纯JSON格式。"
)
@classmethod
async def detect_ad_popup(cls, image_path: str, device_info=None):
"""
使用 VL 模型检测是否存在弹窗广告,并返回关闭按钮坐标
"""
if not os.path.exists(image_path):
return None
with open(image_path, "rb") as image_file:
encoded_image = base64.b64encode(image_file.read()).decode("utf-8")
img_input = {"url": f"data:image/jpeg;base64,{encoded_image}"}
# 构建提示词
prompt = (
"请仔细检查这张图片中是否存在**弹窗广告**或**悬浮广告**。\n"
"广告可能有以下几种形式:\n"
"1. **屏幕中央的大型弹窗广告**:通常遮挡了页面内容,内容多为优惠券、活动推广等。\n"
"2. **屏幕左侧或右侧的悬浮小广告**:例如带有'活动'、'红包'、'优惠'字样的小图标。\n"
"3. **底部的横幅广告**:带有明显的关闭按钮。\n\n"
"如果发现了上述任何一种广告,请务必找到该广告的**关闭按钮**(通常是一个'X'符号,或者'关闭'字样,或者位于广告下方/右上角的圆圈叉号)。\n"
"请返回该关闭按钮的**中心坐标**。\n\n"
"请以纯 JSON 格式输出:\n"
"{\n"
" \"has_ad\": true/false,\n"
" \"close_point\": [x, y] // 0-1000 归一化坐标\n"
"}\n"
"如果没有广告,请返回 `{\"has_ad\": false}`。\n"
"只输出 JSON,不要包含任何其他文字。"
)
try:
resp = await cls._client.chat.completions.create(
model=VL_MODEL_NAME, # 使用默认 VL 模型即可,或者使用 AD 专用
timeout=30,
messages=[
{
"role": "user",
"content": [
{"type": "image_url", "image_url": img_input},
{"type": "text", "text": prompt},
],
}
],
)
content = resp.choices[0].message.content.strip()
raw_json = cls._extract_json(content)
result = json.loads(raw_json)
if result.get("has_ad") and result.get("close_point"):
# 转换坐标
norm_point = result["close_point"]
if len(norm_point) == 2:
if not device_info:
device_info = cls._FALLBACK_DEVICE_INFO
w = device_info.get("displayWidth", FALLBACK_WIDTH)
h = device_info.get("displayHeight", FALLBACK_HEIGHT)
# 假设模型返回的是 0-1000 的坐标
x = int(norm_point[0] / 1000 * w)
y = int(norm_point[1] / 1000 * h)
return {"x": x, "y": y}
return None
except Exception as e:
logger.error(f"Failed to detect ad popup: {e}")
return None
@staticmethod
def _extract_json(text: str) -> str:
if not text:
@@ -509,6 +579,10 @@ class ReadImageKit:
"""
使用 VL 模型定位详情页中的 '分时价格' 按钮
"""
if device_info is None:
logger.warning("未提供动态设备信息,使用通用回退配置。")
device_info = cls._FALLBACK_DEVICE_INFO
if not os.path.exists(image_path):
return {}
@@ -517,12 +591,14 @@ class ReadImageKit:
img_input = {"url": f"data:image/jpeg;base64,{encoded}"}
prompt = (
"请识别图片中 '分时价格' 或 '查看分时电价' 按钮的中心坐标。\n"
"该按钮通常位于充电站详情页,可能是一个带有价格图标或箭头的小型矩形区域。\n"
"请直接以 JSON 格式输出该按钮的中心点坐标:\n"
"{\"uia_center_x\": 500, \"uia_center_y\": 500}\n"
"注意:坐标系为 0-1000 归一化坐标,其中 (0,0) 为左上角,(1000,1000) 为右下角。\n"
"只输出 JSON,不要有任何解释文字。"
"请找到图片中 '分时价格' 或 '查看分时电价' 按钮区域(通常在详情页,可能是一个带有价格图标或箭头的小型矩形区域)。\n"
"返回格式示例:\n"
"{\n"
' "bounds": {"x1": 100, "y1": 200, "x2": 300, "y2": 400}, \n'
' "bounds_norm": {"left": 0.1, "top": 0.2, "right": 0.3, "bottom": 0.4}\n'
"}\n"
"注意:bounds应使用0-1000的归一化坐标空间。\n"
"如果未找到,返回空JSON {}。"
)
try:
@@ -533,26 +609,32 @@ class ReadImageKit:
{
"role": "user",
"content": [
{"type": "text", "text": prompt},
{"type": "image_url", "image_url": img_input}
{"type": "image_url", "image_url": img_input},
{"type": "text", "text": json.dumps(device_info, ensure_ascii=False)},
{"type": "text", "text": prompt}
]
}
],
max_tokens=100,
max_tokens=200,
temperature=0.01
)
content = resp.choices[0].message.content.strip()
raw = cls._extract_json(content)
res = json.loads(raw)
# 转换 0-1000 坐标到实际像素坐标
if res.get("uia_center_x") is not None and device_info:
w = device_info.get("width", FALLBACK_WIDTH)
h = device_info.get("height", FALLBACK_HEIGHT)
res["uia_center_x"] = int(res["uia_center_x"] * w / 1000)
res["uia_center_y"] = int(res["uia_center_y"] * h / 1000)
try:
data = json.loads(raw)
if isinstance(data, dict) and (data.get("bounds") or data.get("bounds_norm")):
data = cls._add_center(data, device_info)
# 只返回中心坐标
return {
"uia_center_x": data.get("uia_center_x"),
"uia_center_y": data.get("uia_center_y")
}
return {}
except Exception as e:
logger.error(f"Error parsing JSON: {e}")
return {}
return res
except Exception as e:
logger.error(f"VL find_time_price_button_coordinate error: {e}")
return {}