diff --git a/.idea/Crawler.iml b/.idea/Crawler.iml
deleted file mode 100644
index 4ba3f29..0000000
--- a/.idea/Crawler.iml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
index f1a2754..ad51e97 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -2,7 +2,7 @@
-
+
\ No newline at end of file
diff --git a/Apps/AiTeJiYiChong/Crawler.py b/Apps/AiTeJiYiChong/Crawler.py
index 55bfeb6..13bce89 100644
--- a/Apps/AiTeJiYiChong/Crawler.py
+++ b/Apps/AiTeJiYiChong/Crawler.py
@@ -74,11 +74,11 @@ async def get_station_list(d, service, max_scrolls=MAX_SCROLLS):
logger.info("正在执行图形学切片分析...")
json_data = Kit.crop_cards_from_image(screenshot_path)
- # 3. 调用 VL 模型识别并保存数据
- stations = await service.process_station_list_vl(screenshot_path, device_info=device_info)
+ # 3. 调用混合模式识别 (图形学切片 + 本地 OCR)
+ stations = await service.process_station_list_hybrid(screenshot_path, device_info=device_info)
logger.info(f"本页识别到 {len(stations)} 个场站")
- # 4. 匹配几何卡片与 VL 识别结果
+ # 4. 匹配几何卡片与识别结果 (混合模式下已经包含在 stations 中,但为了兼容旧逻辑进行填充)
if json_data and json_data.get("cards") and stations:
for card in json_data["cards"]:
card_rect = card["rect"] # [x1, y1, x2, y2]
diff --git a/Apps/AiTeJiYiChong/Opener.py b/Apps/AiTeJiYiChong/Opener.py
index fa03766..2b778a7 100644
--- a/Apps/AiTeJiYiChong/Opener.py
+++ b/Apps/AiTeJiYiChong/Opener.py
@@ -27,18 +27,9 @@ async def open_mini_program():
await asyncio.sleep(5)
# 2. 确保在消息列表页并点击搜索
- logger.info("尝试查找并点击 '搜索按钮'...")
- # 优先尝试从 XinDianTu 的模板中复用 SearchButton.jpg (如果存在)
- search_template = os.path.join(os.path.dirname(BASE_DIR), "XinDianTu", "Templates", "SearchButton.jpg")
- if not os.path.exists(search_template):
- search_template = os.path.join(BASE_DIR, "Templates", "SearchButton.jpg")
-
- if click_image_template(d, search_template):
- logger.info("点击了搜索按钮")
- else:
- logger.warning("未找到搜索按钮,使用坐标点击 (84%, 8%)")
- w, h = d.window_size()
- d.click(int(w * 0.84), int(h * 0.08))
+ logger.info("直接使用坐标点击 '搜索按钮' (84%, 8%)...")
+ w, h = d.window_size()
+ d.click(int(w * 0.84), int(h * 0.08))
await asyncio.sleep(2)
diff --git a/Apps/AiTeJiYiChong/ReadImageKit.py b/Apps/AiTeJiYiChong/ReadImageKit.py
index dad2c08..4a30318 100644
--- a/Apps/AiTeJiYiChong/ReadImageKit.py
+++ b/Apps/AiTeJiYiChong/ReadImageKit.py
@@ -7,6 +7,7 @@ import json
import aiohttp
import logging
import base64
+import cv2
from openai import AsyncOpenAI, BadRequestError
from Config.Config import (
ALY_LLM_API_KEY, VL_MODEL_NAME, VL_MODEL_NAME_AD
@@ -15,6 +16,8 @@ from Apps.AiTeJiYiChong.Config.Setting import (
SAFE_EXCLUDE_RATIO, FALLBACK_WIDTH, FALLBACK_HEIGHT,
BOTTOM_SAFE_EXCLUDE_RATIO
)
+from Util.PaddleOCRKit import get_ocr_kit
+from Apps.AiTeJiYiChong import Kit
logger = logging.getLogger(__name__)
@@ -287,6 +290,65 @@ class ReadImageKit:
logger.error(f"Error calling VL model for detail: {e}")
return None
+ @classmethod
+ async def get_stations_hybrid(cls, image_path: str, device_info=None):
+ """
+ 混合识别模式:图形学切片 + 本地 PaddleOCR 识别
+ """
+ if not os.path.exists(image_path):
+ logger.error(f"Image not found: {image_path}")
+ return []
+
+ # 1. 使用 Kit 中的图形学算法识别卡片区域
+ # Kit.crop_cards_from_image 会生成 .json, _flag.jpg, _vl.jpg
+ # 我们主要需要它返回的 json_data
+ json_data = Kit.crop_cards_from_image(image_path, save_debug=True)
+ if not json_data or not json_data.get("cards"):
+ logger.warning("No cards detected by graphical slicing.")
+ return []
+
+ img = Image.open(image_path).convert("RGB")
+ ocr_kit = get_ocr_kit()
+
+ final_stations = []
+
+ # 2. 对每个卡片区域进行 OCR 识别
+ # 注意:PaddleOCR 识别过程较快,且通常不涉及网络请求,可以根据需要选择并行或串行
+ # 这里使用串行以保证日志输出整齐,如果追求极致性能可改用 asyncio.to_thread 并行
+ for card in json_data["cards"]:
+ rect = card["rect"] # [x1, y1, x2, y2]
+ # 裁剪卡片
+ patch = img.crop((rect[0], rect[1], rect[2], rect[3]))
+
+ # 转换为 ndarray 供 PaddleOCR 使用
+ patch_cv = cv2.cvtColor(np.array(patch), cv2.COLOR_RGB2BGR)
+
+ # OCR 识别
+ logger.info(f"正在识别卡片 {card['id']}: {rect}")
+ res = ocr_kit.recognize(patch_cv)
+
+ if res and res.get("station_name"):
+ # 注入点击坐标和原始区域信息
+ res["uia_center_x"] = card["click_point"][0]
+ res["uia_center_y"] = card["click_point"][1]
+ res["rect"] = rect
+
+ # 转换 bounds 到 0-1000 空间(保持与 VL 模式兼容)
+ w, h = img.size
+ res["bounds"] = [
+ int(rect[0] * 1000 / w),
+ int(rect[1] * 1000 / h),
+ int(rect[2] * 1000 / w),
+ int(rect[3] * 1000 / h)
+ ]
+
+ final_stations.append(res)
+ logger.info(f"卡片 {card['id']} 识别成功: {res['station_name']}")
+ else:
+ logger.warning(f"卡片 {card['id']} 识别失败或无名称")
+
+ return final_stations
+
@classmethod
async def get_stations_from_image(cls, image_path: str, device_info=None):
"""
diff --git a/Apps/AiTeJiYiChong/Service.py b/Apps/AiTeJiYiChong/Service.py
index b5ada8d..7bf3baf 100644
--- a/Apps/AiTeJiYiChong/Service.py
+++ b/Apps/AiTeJiYiChong/Service.py
@@ -125,6 +125,69 @@ class AiTeJiYiChongService:
logger.info(f"场站详情处理完成: {name}")
return detail
+ async def process_station_list_hybrid(self, image_path, device_info=None) -> list:
+ """
+ 基于混合模式处理场站列表 (图形学切片 + 本地 OCR)
+ """
+ station_list = await ReadImageKit.get_stations_hybrid(image_path, device_info=device_info)
+ if not station_list:
+ return []
+
+ processed_stations = []
+ async with await self.db.get_session() as session:
+ for station in station_list:
+ name = station.get("station_name")
+ if not name:
+ continue
+
+ station_hash = self.get_hash(name)
+ now = datetime.now()
+ station["station_hash"] = station_hash
+
+ # 1. 保存 Profile
+ profile_id = self.generate_id()
+ await self.station_profile_model.save(
+ session=session,
+ id=profile_id,
+ station_hash=station_hash,
+ operator=self.operator,
+ station_name=name,
+ valid_start_time=now
+ )
+ station["profile_id"] = profile_id
+ station["valid_start_time"] = now.isoformat()
+
+ # 2. 保存 Status (解析价格和电桩)
+ status_id = self.generate_id()
+
+ # 处理 piles 字段
+ piles_data = station.get("piles")
+ total, free = 0, 0
+ if isinstance(piles_data, list):
+ for p in piles_data:
+ total += int(p.get("total", 0))
+ free += int(p.get("free", 0))
+
+ await self.station_status_model.save(
+ session=session,
+ id=status_id,
+ station_hash=station_hash,
+ total_piles=total,
+ free_piles=free,
+ piles_detail_json=piles_data,
+ current_price=float(station.get("price", 0)) if station.get("price") else 0.0,
+ parking_info=station.get("parking", ""),
+ distance=station.get("distance", ""),
+ valid_start_time=now
+ )
+ station["status_id"] = status_id
+
+ processed_stations.append(station)
+
+ await session.commit()
+
+ return processed_stations
+
async def process_station_list_vl(self, image_path, device_info=None) -> list:
"""
基于 VL 模式处理场站列表
diff --git a/Apps/AiTeJiYiChong/TestClick.py b/Apps/AiTeJiYiChong/TestClick.py
deleted file mode 100644
index 15924f0..0000000
--- a/Apps/AiTeJiYiChong/TestClick.py
+++ /dev/null
@@ -1,41 +0,0 @@
-# coding=utf-8
-import asyncio
-import uiautomator2 as u2
-import os
-import sys
-import uuid
-
-project_root = r"D:\dsWork\aiData"
-if project_root not in sys.path:
- sys.path.append(project_root)
-
-from Apps.AiTeJiYiChong import Kit
-from Apps.AiTeJiYiChong.Kit import take_screenshot, read_image
-from Config.Config import TEMP_IMAGE_DIR
-
-async def test_click_and_detail():
- d = u2.connect()
- image_uuid = str(uuid.uuid4())
- print(f"Taking initial screenshot...")
- screenshot_path = take_screenshot(d, image_uuid, save_dir=TEMP_IMAGE_DIR)
-
- print(f"Analyzing cards in {screenshot_path}...")
- json_data = Kit.crop_cards_from_image(screenshot_path)
-
- if json_data and json_data.get("cards"):
- first_card = json_data["cards"][0]
- click_x, click_y = first_card["click_point"]
- print(f"Clicking card at ({click_x}, {click_y})...")
- d.click(click_x, click_y)
-
- print("Waiting for detail page...")
- await asyncio.sleep(5)
-
- detail_uuid = f"detail_{image_uuid}"
- detail_path = take_screenshot(d, detail_uuid, save_dir=TEMP_IMAGE_DIR)
- print(f"Detail page screenshot: {detail_path}")
- else:
- print("No cards found on current screen.")
-
-if __name__ == "__main__":
- asyncio.run(test_click_and_detail())
diff --git a/Apps/AiTeJiYiChong/__pycache__/Crawler.cpython-310.pyc b/Apps/AiTeJiYiChong/__pycache__/Crawler.cpython-310.pyc
index d57bc83..6d62598 100644
Binary files a/Apps/AiTeJiYiChong/__pycache__/Crawler.cpython-310.pyc and b/Apps/AiTeJiYiChong/__pycache__/Crawler.cpython-310.pyc differ
diff --git a/Apps/AiTeJiYiChong/__pycache__/Opener.cpython-310.pyc b/Apps/AiTeJiYiChong/__pycache__/Opener.cpython-310.pyc
index 92f6bbc..b075cfa 100644
Binary files a/Apps/AiTeJiYiChong/__pycache__/Opener.cpython-310.pyc and b/Apps/AiTeJiYiChong/__pycache__/Opener.cpython-310.pyc differ
diff --git a/Apps/AiTeJiYiChong/__pycache__/ReadImageKit.cpython-310.pyc b/Apps/AiTeJiYiChong/__pycache__/ReadImageKit.cpython-310.pyc
index 791acdb..46ca2e7 100644
Binary files a/Apps/AiTeJiYiChong/__pycache__/ReadImageKit.cpython-310.pyc and b/Apps/AiTeJiYiChong/__pycache__/ReadImageKit.cpython-310.pyc differ
diff --git a/Apps/AiTeJiYiChong/__pycache__/Service.cpython-310.pyc b/Apps/AiTeJiYiChong/__pycache__/Service.cpython-310.pyc
index 446095f..5a6a5d5 100644
Binary files a/Apps/AiTeJiYiChong/__pycache__/Service.cpython-310.pyc and b/Apps/AiTeJiYiChong/__pycache__/Service.cpython-310.pyc differ
diff --git a/Apps/AiTeJiYiChong/说明.txt b/Apps/AiTeJiYiChong/说明.txt
deleted file mode 100644
index 849e1c8..0000000
--- a/Apps/AiTeJiYiChong/说明.txt
+++ /dev/null
@@ -1 +0,0 @@
-微信小程序名称: 艾特吉易充
\ No newline at end of file
diff --git a/Apps/XinDianTu/SearchButton.jpg b/Apps/XinDianTu/SearchButton.jpg
deleted file mode 100644
index 391d4f9..0000000
Binary files a/Apps/XinDianTu/SearchButton.jpg and /dev/null differ
diff --git a/Config/Config.py b/Config/Config.py
index db0ed57..773e1d8 100644
--- a/Config/Config.py
+++ b/Config/Config.py
@@ -1,16 +1,16 @@
# 黄海在公司内网开发时的配置信息
-DORIS_HOST = "10.10.14.204"
-DORIS_PORT = 9030
-DORIS_FENODES = "10.10.14.204:8030"
-REDIS_HOST = '10.10.14.14'
-REDIS_PASSWORD = None # 如果没有密码则设为 None
+# DORIS_HOST = "10.10.14.204"
+# DORIS_PORT = 9030
+# DORIS_FENODES = "10.10.14.204:8030"
+# REDIS_HOST = '10.10.14.14'
+# REDIS_PASSWORD = None # 如果没有密码则设为 None
# 黄海在家开发时的配置信息
-#DORIS_HOST = "www.hzkjai.com"
-#DORIS_PORT = 27025
-#DORIS_FENODES = "www.hzkjai.com:27024"
-#REDIS_HOST = '127.0.0.1'
-#REDIS_PASSWORD = "DsideaL147258369"
+DORIS_HOST = "www.hzkjai.com"
+DORIS_PORT = 27025
+DORIS_FENODES = "www.hzkjai.com:27024"
+REDIS_HOST = '127.0.0.1'
+REDIS_PASSWORD = "DsideaL147258369"
# 视觉模型配置
VL_MODEL_NAME = "qwen3-vl-flash"
diff --git a/Config/__pycache__/Config.cpython-310.pyc b/Config/__pycache__/Config.cpython-310.pyc
index afc66d1..0a90099 100644
Binary files a/Config/__pycache__/Config.cpython-310.pyc and b/Config/__pycache__/Config.cpython-310.pyc differ
diff --git a/Util/OcrParser.py b/Util/OcrParser.py
index 622d172..f26ab09 100644
--- a/Util/OcrParser.py
+++ b/Util/OcrParser.py
@@ -50,8 +50,8 @@ class OcrParser:
# 2. 距离 (Distance)
for line in clean_lines:
- # 匹配 "7.4km", "17.4km"
- m = re.search(r"(\d+(\.\d+)?)\s*km", line, re.IGNORECASE)
+ # 匹配 "7.4km", "17.4km", "90m"
+ m = re.search(r"(\d+(\.\d+)?)\s*(km|m)", line, re.IGNORECASE)
if m:
result["distance"] = m.group(0)
break
diff --git a/Util/PaddleOCRKit.py b/Util/PaddleOCRKit.py
index 9d4b24e..e67e880 100644
--- a/Util/PaddleOCRKit.py
+++ b/Util/PaddleOCRKit.py
@@ -26,13 +26,23 @@ class PaddleOCRKit:
Using PP-OCRv4 mobile model for speed.
"""
try:
+ # 1. 抑制 PaddlePaddle 的日志输出
+ os.environ['GLOG_minloglevel'] = '3' # 抑制 GLOG (C++) 日志
+ os.environ['FLAGS_allocator_strategy'] = 'auto_growth' # 减少显存占用警告
+
+ # 2. 抑制 Python 层的 paddle 日志
+ import logging as py_logging
+ py_logging.getLogger('paddle').setLevel(py_logging.ERROR)
+ py_logging.getLogger('ppocr').setLevel(py_logging.ERROR)
+
logger.info("Initializing PaddleOCR (PP-OCRv4 Mobile)...")
# use_angle_cls is deprecated, using use_textline_orientation instead
# ocr_version='PP-OCRv4' defaults to mobile model
self._ocr = PaddleOCR(
use_textline_orientation=True,
lang="ch",
- ocr_version='PP-OCRv4'
+ ocr_version='PP-OCRv4',
+ show_log=False # 关键:关闭 PaddleOCR 内部日志打印
)
logger.info("PaddleOCR initialized successfully.")
except Exception as e:
diff --git a/Util/__pycache__/OcrParser.cpython-310.pyc b/Util/__pycache__/OcrParser.cpython-310.pyc
index b36f8f5..49ac918 100644
Binary files a/Util/__pycache__/OcrParser.cpython-310.pyc and b/Util/__pycache__/OcrParser.cpython-310.pyc differ