'commit'
This commit is contained in:
@@ -98,6 +98,191 @@ class ReadImageKit:
|
||||
|
||||
return "[]"
|
||||
|
||||
_prompt_detail = (
|
||||
"仅输出JSON对象(不含任何说明文字),识别充电站详情图片中的以下信息:\n"
|
||||
"1. station_name: 场站名称;\n"
|
||||
"2. address: 场站完整地址(通常在定位图标旁)。\n"
|
||||
"\n"
|
||||
"特别说明:\n"
|
||||
"- 严禁输出 Markdown 代码块标签,严格返回纯JSON对象。"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _to_minutes(t_str: str) -> int:
|
||||
"""HH:MM -> 分钟数"""
|
||||
if not t_str or ":" not in t_str:
|
||||
return 0
|
||||
try:
|
||||
h, m = map(int, t_str.split(":"))
|
||||
return h * 60 + m
|
||||
except:
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def _fmt(t: int) -> str:
|
||||
"""分钟数 -> HH:MM"""
|
||||
h = t // 60
|
||||
m = t % 60
|
||||
return f"{h:02d}:{m:02d}"
|
||||
|
||||
@staticmethod
|
||||
def expand_schedule_to_24h(rows: list) -> list:
|
||||
"""
|
||||
将时段列表规整为全天24个整点小时段
|
||||
"""
|
||||
# 预处理:转换为分钟区间
|
||||
intervals = []
|
||||
for r in rows:
|
||||
s = ReadImageKit._to_minutes(r.get("start"))
|
||||
e = ReadImageKit._to_minutes(r.get("end"))
|
||||
if e <= s and e != 0: # 处理 00:00-00:00 这种可能
|
||||
if e == 0: e = 1440
|
||||
else: continue
|
||||
if e == 0 and s > 0: e = 1440
|
||||
|
||||
s = max(0, s)
|
||||
e = min(1440, e)
|
||||
intervals.append({
|
||||
"s": s, "e": e,
|
||||
"price": r.get("price")
|
||||
})
|
||||
|
||||
# 排序
|
||||
intervals.sort(key=lambda x: (x["s"], x["e"]))
|
||||
|
||||
result = []
|
||||
for h in range(24):
|
||||
hs = h * 60
|
||||
he = (h + 1) * 60
|
||||
best_price = None
|
||||
|
||||
# 找到覆盖当前小时段的价格
|
||||
for it in intervals:
|
||||
# 计算重叠
|
||||
overlap_s = max(hs, it["s"])
|
||||
overlap_e = min(he, it["e"])
|
||||
if overlap_e > overlap_s:
|
||||
best_price = it["price"]
|
||||
break # 简单处理,取第一个覆盖的
|
||||
|
||||
result.append({
|
||||
"start": ReadImageKit._fmt(hs),
|
||||
"end": ReadImageKit._fmt(he),
|
||||
"price": best_price
|
||||
})
|
||||
return result
|
||||
|
||||
_prompt_price_detail = (
|
||||
"仅输出JSON对象(不含任何说明文字),识别充电价格详情图片中的分时段电价信息。图片中通常包含“充电时段”、“单价”、“电费”、“服务费”等列。\n"
|
||||
"请识别出所有的时段行,返回一个列表,每个元素包含:\n"
|
||||
"1. start: 开始时间 (HH:MM);\n"
|
||||
"2. end: 结束时间 (HH:MM);\n"
|
||||
"3. price: 总单价 (数字,如 0.6100);\n"
|
||||
"4. ele_fee: 电费 (数字);\n"
|
||||
"5. ser_fee: 服务费 (数字)。\n"
|
||||
"\n"
|
||||
"注意:\n"
|
||||
"- 只需识别“直流桩”或当前选中的桩型下的数据。\n"
|
||||
"- 严禁输出 Markdown 代码块标签,严格返回纯JSON对象。"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def get_price_detail_from_image(cls, image_path: str):
|
||||
"""
|
||||
使用 VL 模型从三级价格详情页截图中识别分时段电价
|
||||
"""
|
||||
if not os.path.exists(image_path):
|
||||
logger.error(f"Image not found: {image_path}")
|
||||
return None
|
||||
|
||||
with open(image_path, "rb") as image_file:
|
||||
encoded_image = base64.b64encode(image_file.read()).decode("utf-8")
|
||||
|
||||
try:
|
||||
response = await asyncio.to_thread(
|
||||
cls._client.chat.completions.create,
|
||||
model=VL_MODEL_NAME,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": cls._prompt_price_detail},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": f"data:image/jpeg;base64,{encoded_image}"},
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
max_tokens=2000,
|
||||
temperature=0.01
|
||||
)
|
||||
|
||||
content = response.choices[0].message.content
|
||||
logger.info(f"VL Price Detail Response: {content}")
|
||||
|
||||
json_str = cls._extract_json(content)
|
||||
data = json.loads(json_str)
|
||||
|
||||
# 兼容性处理:如果返回的是对象且包含列表字段,提取列表
|
||||
if isinstance(data, dict):
|
||||
for key in ["price_list", "prices", "schedule", "data"]:
|
||||
if key in data and isinstance(data[key], list):
|
||||
return data[key]
|
||||
# 如果本身就是个包含列表的字典,也可能需要根据实际情况调整
|
||||
# 这里假设 prompt 引导它返回一个包含列表的对象或直接是列表
|
||||
if "items" in data: return data["items"]
|
||||
|
||||
return data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error calling VL model for price detail: {e}")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
async def get_station_detail_from_image(cls, image_path: str):
|
||||
"""
|
||||
使用 VL 模型从详情页截图中识别地址、名称和电价
|
||||
"""
|
||||
if not os.path.exists(image_path):
|
||||
logger.error(f"Image not found: {image_path}")
|
||||
return None
|
||||
|
||||
with open(image_path, "rb") as image_file:
|
||||
encoded_image = base64.b64encode(image_file.read()).decode("utf-8")
|
||||
|
||||
try:
|
||||
response = await asyncio.to_thread(
|
||||
cls._client.chat.completions.create,
|
||||
model=VL_MODEL_NAME,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": cls._prompt_detail},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": f"data:image/jpeg;base64,{encoded_image}"},
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
max_tokens=1000,
|
||||
temperature=0.01
|
||||
)
|
||||
|
||||
content = response.choices[0].message.content
|
||||
logger.info(f"VL Detail Response: {content}")
|
||||
|
||||
json_str = cls._extract_json(content)
|
||||
detail = json.loads(json_str)
|
||||
|
||||
return detail
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error calling VL model for detail: {e}")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
async def get_stations_from_image(cls, image_path: str, device_info=None):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user