This commit is contained in:
HuangHai
2026-01-12 08:49:17 +08:00
parent 9d57def591
commit 2787bd9df9
5 changed files with 56 additions and 56 deletions

View File

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

View File

@@ -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.")