diff --git a/Apps/XinDianTu/Config/__pycache__/Setting.cpython-310.pyc b/Apps/XinDianTu/Config/__pycache__/Setting.cpython-310.pyc index 23fe7e2..c433eb7 100644 Binary files a/Apps/XinDianTu/Config/__pycache__/Setting.cpython-310.pyc and b/Apps/XinDianTu/Config/__pycache__/Setting.cpython-310.pyc differ diff --git a/Apps/XinDianTu/ReadImageKit.py b/Apps/XinDianTu/ReadImageKit.py index 11873b0..f636c14 100644 --- a/Apps/XinDianTu/ReadImageKit.py +++ b/Apps/XinDianTu/ReadImageKit.py @@ -19,7 +19,7 @@ from Apps.XinDianTu.Config.Setting import ( logger = logging.getLogger(__name__) -class XinDianTuReadImageKit: +class ReadImageKit: _client = OpenAI( api_key=ALY_LLM_API_KEY, base_url="https://dashscope.aliyuncs.com/compatible-mode/v1" @@ -101,7 +101,7 @@ class XinDianTuReadImageKit: @staticmethod def _add_center(obj, device_info): - return XinDianTuReadImageKit._add_click_point(obj, device_info, anchor="center") + return ReadImageKit._add_click_point(obj, device_info, anchor="center") @staticmethod def _add_click_point(obj, device_info, anchor: str = "center"): @@ -210,7 +210,7 @@ class XinDianTuReadImageKit: async def find_all_time_button_coordinate(image_url: str, device_info: dict = None) -> dict: if device_info is None: logger.warning("未提供动态设备信息,使用通用回退配置。") - device_info = XinDianTuReadImageKit._FALLBACK_DEVICE_INFO + device_info = ReadImageKit._FALLBACK_DEVICE_INFO prompt = ( "仅输出JSON对象(不含任何说明文字),请找到图片中带有“全部时段”字样的按钮区域(通常在价格表下方,是一个带有右箭头的文字按钮)。\n" @@ -226,7 +226,7 @@ class XinDianTuReadImageKit: loop = asyncio.get_event_loop() resp = await loop.run_in_executor( None, - lambda: XinDianTuReadImageKit._client.chat.completions.create( + lambda: ReadImageKit._client.chat.completions.create( model=VL_MODEL_NAME, messages=[ { @@ -247,12 +247,12 @@ class XinDianTuReadImageKit: ) content = resp.choices[0].message.content or "" - raw = XinDianTuReadImageKit._extract_json(content) + raw = ReadImageKit._extract_json(content) try: data = json.loads(raw) if isinstance(data, dict) and (data.get("bounds") or data.get("bounds_norm")): - data = XinDianTuReadImageKit._add_center(data, device_info) + data = ReadImageKit._add_center(data, device_info) # 只返回中心坐标 return { "uia_center_x": data.get("uia_center_x"), @@ -268,7 +268,7 @@ class XinDianTuReadImageKit: async def find_station_coordinate_first_page(image_url: str, station_name: str, device_info: dict = None) -> dict: if device_info is None: logger.warning("未提供动态设备信息,使用通用回退配置。") - device_info = XinDianTuReadImageKit._FALLBACK_DEVICE_INFO + device_info = ReadImageKit._FALLBACK_DEVICE_INFO prompt = ( "仅输出JSON对象(不含任何说明文字)。" @@ -283,7 +283,7 @@ class XinDianTuReadImageKit: loop = asyncio.get_event_loop() resp = await loop.run_in_executor( None, - lambda: XinDianTuReadImageKit._client.chat.completions.create( + lambda: ReadImageKit._client.chat.completions.create( model=VL_MODEL_NAME, messages=[ { @@ -299,14 +299,14 @@ class XinDianTuReadImageKit: ) content = resp.choices[0].message.content or "" - raw = XinDianTuReadImageKit._extract_json(content) + raw = ReadImageKit._extract_json(content) try: data = json.loads(raw) if isinstance(data, dict) and ( data.get("station_name_bounds") or data.get("station_name_bounds_norm") or data.get( "bounds") or data.get("bounds_norm")): - data = XinDianTuReadImageKit._add_click_point(data, device_info, anchor="station_text") + data = ReadImageKit._add_click_point(data, device_info, anchor="station_text") return { "uia_center_x": data.get("uia_center_x"), "uia_center_y": data.get("uia_center_y") @@ -319,12 +319,12 @@ class XinDianTuReadImageKit: async def parse_first_level_image_url(image_url: str, device_info: dict = None) -> list: if device_info is None: logger.warning("未提供动态设备信息,使用通用回退配置。") - device_info = XinDianTuReadImageKit._FALLBACK_DEVICE_INFO + device_info = ReadImageKit._FALLBACK_DEVICE_INFO loop = asyncio.get_event_loop() resp = await loop.run_in_executor( None, - lambda: XinDianTuReadImageKit._client.chat.completions.create( + lambda: ReadImageKit._client.chat.completions.create( model=VL_MODEL_NAME, messages=[ { @@ -337,7 +337,7 @@ class XinDianTuReadImageKit: }, }, {"type": "text", "text": json.dumps(device_info, ensure_ascii=False)}, - {"type": "text", "text": XinDianTuReadImageKit._prompt}, + {"type": "text", "text": ReadImageKit._prompt}, ], }, ], @@ -345,18 +345,18 @@ class XinDianTuReadImageKit: ) content = resp.choices[0].message.content or "" - raw = XinDianTuReadImageKit._extract_json(content) + raw = ReadImageKit._extract_json(content) try: data = json.loads(raw) if isinstance(data, list): # 过滤掉 None (即被 _add_click_point 排除掉的顶部项) - data = [XinDianTuReadImageKit._add_click_point(x, device_info, anchor="station_text") for x in data if + data = [ReadImageKit._add_click_point(x, device_info, anchor="station_text") for x in data if x.get("b_use", 1) == 1] data = [x for x in data if x is not None] elif isinstance(data, dict): if data.get("b_use", 1) == 1: - data = XinDianTuReadImageKit._add_click_point(data, device_info, anchor="station_text") + data = ReadImageKit._add_click_point(data, device_info, anchor="station_text") data = [data] if data is not None else [] else: data = [] @@ -397,7 +397,7 @@ class XinDianTuReadImageKit: if name: idxs.append(i) tasks.append(asyncio.create_task( - XinDianTuReadImageKit.find_station_coordinate_first_page(image_url, name, device_info) + ReadImageKit.find_station_coordinate_first_page(image_url, name, device_info) )) if tasks: results = await asyncio.gather(*tasks, return_exceptions=True) @@ -418,7 +418,7 @@ class XinDianTuReadImageKit: """ 统一入口:混合识别模式 """ - return await XinDianTuReadImageKit.get_stations_hybrid(image_path, uploader, cdn_domain) + return await ReadImageKit.get_stations_hybrid(image_path, uploader, cdn_domain) @staticmethod async def parse_vl_image(vl_image_url, json_metadata, device_info=None): @@ -453,7 +453,7 @@ class XinDianTuReadImageKit: loop = asyncio.get_event_loop() resp = await loop.run_in_executor( None, - lambda: XinDianTuReadImageKit._client.chat.completions.create( + lambda: ReadImageKit._client.chat.completions.create( model=VL_MODEL_NAME, messages=[ { @@ -468,7 +468,7 @@ class XinDianTuReadImageKit: ) content = resp.choices[0].message.content or "" - raw = XinDianTuReadImageKit._extract_json(content) + raw = ReadImageKit._extract_json(content) final_stations = [] try: @@ -529,7 +529,7 @@ class XinDianTuReadImageKit: 混合识别模式:图形学切片 + 大模型小图 OCR """ # 1. 图形学切片 - segments = XinDianTuReadImageKit.get_card_segments(image_path) + segments = ReadImageKit.get_card_segments(image_path) if not segments: return [] @@ -544,7 +544,7 @@ class XinDianTuReadImageKit: # 记录上传后的 URL 供识别使用 seg["patch_url"] = patch_url - tasks.append(XinDianTuReadImageKit.recognize_card_text(patch_url)) + tasks.append(ReadImageKit.recognize_card_text(patch_url)) # 等待所有 OCR 完成 results = await asyncio.gather(*tasks) @@ -772,7 +772,7 @@ class XinDianTuReadImageKit: loop = asyncio.get_event_loop() resp = await loop.run_in_executor( None, - lambda: XinDianTuReadImageKit._client.chat.completions.create( + lambda: ReadImageKit._client.chat.completions.create( model=VL_MODEL_NAME, messages=[ { @@ -820,7 +820,7 @@ class XinDianTuReadImageKit: """ if device_info is None: logger.warning("未提供动态设备信息,使用通用回退配置。") - device_info = XinDianTuReadImageKit._FALLBACK_DEVICE_INFO + device_info = ReadImageKit._FALLBACK_DEVICE_INFO prompt = ( "分析图片中是否存在覆盖在主界面上的广告弹窗(Popup Ad)。" @@ -834,7 +834,7 @@ class XinDianTuReadImageKit: loop = asyncio.get_event_loop() resp = await loop.run_in_executor( None, - lambda: XinDianTuReadImageKit._client.chat.completions.create( + lambda: ReadImageKit._client.chat.completions.create( model=VL_MODEL_NAME_AD, # 使用更强的视觉模型 messages=[ { @@ -853,7 +853,7 @@ class XinDianTuReadImageKit: ) content = resp.choices[0].message.content or "" - raw = XinDianTuReadImageKit._extract_json(content) + raw = ReadImageKit._extract_json(content) try: data = json.loads(raw) @@ -861,7 +861,7 @@ class XinDianTuReadImageKit: if has_ad and data.get("close_button_bounds"): # 使用现有的辅助方法转换坐标 temp_obj = {"bounds": data.get("close_button_bounds")} - XinDianTuReadImageKit._add_click_point(temp_obj, device_info) + ReadImageKit._add_click_point(temp_obj, device_info) return { "has_ad": True, "uia_center_x": temp_obj.get("uia_center_x"), @@ -912,7 +912,7 @@ class XinDianTuReadImageKit: """ if device_info is None: logger.warning("未提供动态设备信息,使用通用回退配置。") - device_info = XinDianTuReadImageKit._FALLBACK_DEVICE_INFO + device_info = ReadImageKit._FALLBACK_DEVICE_INFO # 视觉解析提示词:约束输出字段与格式,避免模型输出说明文字 prompt = ( @@ -928,7 +928,7 @@ class XinDianTuReadImageKit: loop = asyncio.get_event_loop() def _do_request(url_val): - return XinDianTuReadImageKit._client.chat.completions.create( + return ReadImageKit._client.chat.completions.create( model=VL_MODEL_NAME, messages=[ { @@ -957,7 +957,7 @@ class XinDianTuReadImageKit: # 如果是媒体格式或数据检查错误 if 'InvalidParameter.DataInspection' in str(err_code) or 'media format' in str(e).lower(): logger.warning(f"Image URL rejected ({err_code}). Attempting Base64 fallback: {image_url}") - base64_str = await XinDianTuReadImageKit._download_as_base64(image_url) + base64_str = await ReadImageKit._download_as_base64(image_url) if base64_str: # 简单推断格式,默认 jpeg ext = "jpeg" @@ -982,7 +982,7 @@ class XinDianTuReadImageKit: return [] content = resp.choices[0].message.content or "" - raw = XinDianTuReadImageKit._extract_json(content) + raw = ReadImageKit._extract_json(content) try: # 反序列化模型返回的 JSON @@ -1069,8 +1069,8 @@ class XinDianTuReadImageKit: hourly = [] for r in rows: # 起止时间转分钟 - s = XinDianTuReadImageKit._to_minutes(r.get("start")) - e = XinDianTuReadImageKit._to_minutes(r.get("end")) + s = ReadImageKit._to_minutes(r.get("start")) + e = ReadImageKit._to_minutes(r.get("end")) if e <= s: continue cur = s @@ -1078,8 +1078,8 @@ class XinDianTuReadImageKit: # 下一小时边界;不超过区间终点 nxt = min(e, ((cur // 60) + 1) * 60) hourly.append({ - "start": XinDianTuReadImageKit._fmt(cur), - "end": XinDianTuReadImageKit._fmt(nxt), + "start": ReadImageKit._fmt(cur), + "end": ReadImageKit._fmt(nxt), "price_kwh": r.get("price_kwh"), "electric_fee_kwh": r.get("electric_fee_kwh"), "service_fee_kwh": r.get("service_fee_kwh"), @@ -1120,10 +1120,10 @@ class XinDianTuReadImageKit: """ if device_info is None: logger.warning("未提供动态设备信息,使用通用回退配置。") - device_info = XinDianTuReadImageKit._FALLBACK_DEVICE_INFO + device_info = ReadImageKit._FALLBACK_DEVICE_INFO # 启动寻找“全部时段”按钮的任务,为后续可能的点击做准备 - button_task = asyncio.create_task(XinDianTuReadImageKit.find_all_time_button_coordinate(image_url, device_info)) + button_task = asyncio.create_task(ReadImageKit.find_all_time_button_coordinate(image_url, device_info)) prompt = ( "仅输出JSON对象(不含任何说明文字)。" @@ -1139,7 +1139,7 @@ class XinDianTuReadImageKit: loop = asyncio.get_event_loop() resp = await loop.run_in_executor( None, - lambda: XinDianTuReadImageKit._client.chat.completions.create( + lambda: ReadImageKit._client.chat.completions.create( model=VL_MODEL_NAME, messages=[ { @@ -1158,7 +1158,7 @@ class XinDianTuReadImageKit: ) content = resp.choices[0].message.content or "" - raw = XinDianTuReadImageKit._extract_json(content) + raw = ReadImageKit._extract_json(content) result = {} try: @@ -1200,14 +1200,14 @@ class XinDianTuReadImageKit: # MD5 -> URL 的映射,用于去重 md5_to_url = {} for u in image_urls: - m = await XinDianTuReadImageKit._fetch_md5(u) + m = await ReadImageKit._fetch_md5(u) if m and m not in md5_to_url: md5_to_url[m] = u unique_urls = list(md5_to_url.values()) # 合并时段结果 combined = [] for u in unique_urls: - rows = await XinDianTuReadImageKit.parse_price_schedule(station_name, u, device_info=device_info) + rows = await ReadImageKit.parse_price_schedule(station_name, u, device_info=device_info) if rows: combined.extend(rows) return combined @@ -1230,8 +1230,8 @@ class XinDianTuReadImageKit: # 预处理:转换为分钟区间 intervals = [] for r in rows: - s = XinDianTuReadImageKit._to_minutes(r.get("start")) - e = XinDianTuReadImageKit._to_minutes(r.get("end")) + s = ReadImageKit._to_minutes(r.get("start")) + e = ReadImageKit._to_minutes(r.get("end")) if e <= s: continue s = max(0, s) @@ -1256,8 +1256,8 @@ class XinDianTuReadImageKit: best_overlap = overlap best = it result.append({ - "start": XinDianTuReadImageKit._fmt(hs), - "end": XinDianTuReadImageKit._fmt(he), + "start": ReadImageKit._fmt(hs), + "end": ReadImageKit._fmt(he), "price_kwh": best["price_kwh"] if best else None, "electric_fee_kwh": best["electric_fee_kwh"] if best else None, "service_fee_kwh": best["service_fee_kwh"] if best else None, @@ -1269,7 +1269,7 @@ async def test1(): # 测试新电途第一层结构的读取 url = "https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/Temp/8fd79c68-fec6-4ca7-8d8e-fbff3c6862c8.jpg" print(f"Testing First Level with URL: {url}") - result = await XinDianTuReadImageKit.parse_first_level_image_url(url) + result = await ReadImageKit.parse_first_level_image_url(url) print("First Level Result:") print(json.dumps(result, ensure_ascii=False, indent=2)) @@ -1278,7 +1278,7 @@ async def test2(station_name: str): # 测试新电途第二层结构的读取 url = "https://dsideal.obs.myhuaweicloud.com/HuangHai/Temp/SecondPage.jpg" print(f"Testing address extraction from: {url}") - result = await XinDianTuReadImageKit.parse_address(station_name, url) + result = await ReadImageKit.parse_address(station_name, url) result["station_name"] = station_name print("Address result:") print(json.dumps(result, ensure_ascii=False, indent=2)) @@ -1290,8 +1290,8 @@ async def test3(station_name: str): "https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/Temp/XinDianTu/1.jpg", "https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/Temp/XinDianTu/2.jpg", ] - rows = await XinDianTuReadImageKit.parse_price_schedule_multi(station_name, samples) - hourly = XinDianTuReadImageKit.hourly_full_day(rows) + rows = await ReadImageKit.parse_price_schedule_multi(station_name, samples) + hourly = ReadImageKit.hourly_full_day(rows) print(json.dumps(hourly, ensure_ascii=False, indent=2)) diff --git a/Apps/XinDianTu/Service.py b/Apps/XinDianTu/Service.py index 3cc1402..19f10a2 100644 --- a/Apps/XinDianTu/Service.py +++ b/Apps/XinDianTu/Service.py @@ -11,7 +11,7 @@ project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(_ if project_root not in sys.path: sys.path.append(project_root) -from Apps.XinDianTu.XinDianTuReadImageKit import XinDianTuReadImageKit +from Apps.XinDianTu.ReadImageKit import ReadImageKit from DbKit.Db import Db from Config.Config import DB_URL from Model.StationProfile import StationProfile @@ -46,7 +46,7 @@ class XinDianTuService: """ 基于 VL 模式处理场站列表 """ - station_list = await XinDianTuReadImageKit.parse_vl_image(vl_image_url, json_metadata, device_info=device_info) + station_list = await ReadImageKit.parse_vl_image(vl_image_url, json_metadata, device_info=device_info) if not station_list: return [] @@ -163,7 +163,7 @@ class XinDianTuService: """ 混合模式处理场站列表 (图形学切片 + OCR) """ - station_list = await XinDianTuReadImageKit.parse_hybrid_image(local_image_path, uploader, cdn_domain) + station_list = await ReadImageKit.parse_hybrid_image(local_image_path, uploader, cdn_domain) if not station_list: return [] @@ -241,7 +241,7 @@ class XinDianTuService: return processed_stations async def process_station_list(self, list_url: str, device_info: dict = None) -> list: - station_list = await XinDianTuReadImageKit.parse_first_level_image_url(list_url, device_info=device_info) + station_list = await ReadImageKit.parse_first_level_image_url(list_url, device_info=device_info) if not station_list: return [] @@ -338,7 +338,7 @@ class XinDianTuService: async def process_station_address(self, station_name: str, detail_url: str, device_info: dict = None) -> dict: logger.info(f"\n[Service] Processing Address for '{station_name}' from: {detail_url}") - address_data = await XinDianTuReadImageKit.parse_address(station_name, detail_url, device_info=device_info) + address_data = await ReadImageKit.parse_address(station_name, detail_url, device_info=device_info) address = address_data.get("address") if address: address = address.strip() @@ -493,8 +493,8 @@ class XinDianTuService: async def process_price_schedule(self, station_name: str, schedule_urls: list, device_info: dict = None): logger.info(f"\n[Service] Processing Price Schedule for '{station_name}'") - rows = await XinDianTuReadImageKit.parse_price_schedule_multi(station_name, schedule_urls, device_info=device_info) - hourly_schedule = XinDianTuReadImageKit.hourly_full_day(rows) + rows = await ReadImageKit.parse_price_schedule_multi(station_name, schedule_urls, device_info=device_info) + hourly_schedule = ReadImageKit.hourly_full_day(rows) if hourly_schedule: logger.info(f" - Got {len(hourly_schedule)} hourly records.") diff --git a/Apps/XinDianTu/__pycache__/ReadImageKit.cpython-310.pyc b/Apps/XinDianTu/__pycache__/ReadImageKit.cpython-310.pyc new file mode 100644 index 0000000..bb6bbb4 Binary files /dev/null and b/Apps/XinDianTu/__pycache__/ReadImageKit.cpython-310.pyc differ diff --git a/Apps/XinDianTu/__pycache__/Service.cpython-310.pyc b/Apps/XinDianTu/__pycache__/Service.cpython-310.pyc index 686bdb1..3d49a39 100644 Binary files a/Apps/XinDianTu/__pycache__/Service.cpython-310.pyc and b/Apps/XinDianTu/__pycache__/Service.cpython-310.pyc differ