实现精细化人脸识别功能,人脸交互详细, UI部分还需要优化

This commit is contained in:
冯佳
2025-09-15 16:03:17 +08:00
parent babab70845
commit 7205a21a8d
6 changed files with 220 additions and 124 deletions

View File

@ -453,6 +453,7 @@ class QFaceCameraViewPage(PageTemplate):
self.t_video.start() self.t_video.start()
self.face_verify_result = None # 用于存储人脸验证结果 self.face_verify_result = None # 用于存储人脸验证结果
self.verify_in_progress = False # 标志当前是否有验证在进行中
self.auto_connect_serial() self.auto_connect_serial()
@ -541,36 +542,104 @@ class QFaceCameraViewPage(PageTemplate):
self.log("[WARN] 摄像头线程已停止") self.log("[WARN] 摄像头线程已停止")
self.video_worker=None # 自动重连可在这里实现 self.video_worker=None # 自动重连可在这里实现
# 帧处理
def on_frame(self, fr: dict): def on_frame(self, fr: dict):
self.log("< " + fr["raw"].hex(" ")) """
接收到一帧数据后的处理
"""
raw_bytes = fr.get("raw", b"")
self.log("< " + raw_bytes.hex(" "))
msg_id = fr.get("msg_id") msg_id = fr.get("msg_id")
data = fr.get("data", b"") data = fr.get("data", b"")
if msg_id == MID_REPLY: if msg_id == MID_REPLY:
info = parse_reply(data) info = parse_reply(data)
self.log(f"[REPLY] {info}") self.log(f"[REPLY] {info}")
if info.get("mid") == CMD_ENROLL_ITG:
if info.get("result") == 0x00:
user_id = info.get("user_id")
if user_id:
if save_user(user_id):
self.log(f"[INFO] 用户ID={user_id} 已保存")
else:
inform_box : DialogInform = DialogInform()
inform_box.information("提示", f"用户ID {user_id} 已存在!")
elif info.get("mid") == CMD_VERIFY :
if info.get("result") == 0x00: mid = info.get("mid")
user_id = info.get("user_id") result = info.get("result")
print(f"[INFO] 用户(ID={user_id}) 验证通过")
self.face_verify_result = True # 命令分发字典
else: dispatch_reply = {
self.face_verify_result = False CMD_ENROLL_SINGLE: self._handle_enroll_reply,
CMD_VERIFY: self._handle_verify_reply,
CMD_GET_ALL_USERID :self._handle_get_all_userid_reply,
}
handler = dispatch_reply.get(mid)
if handler:
handler(info)
elif msg_id == MID_NOTE: elif msg_id == MID_NOTE:
info = parse_note(data) info = parse_note(data)
self.log(f"[NOTE] {info}") self.log(f"[NOTE] {info}")
self._handle_note(info)
# ---------------- NOTE 处理 ----------------
def _handle_note(self, info: dict):
"""
处理 NOTE 消息,给用户交互提示
"""
user_message = info.get("user_message")
nid = info.get("nid")
# 仅在验证进行中时提示 NOTE
if getattr(self, "verify_in_progress", False):
if user_message:
DialogInform(self).information("提示", user_message)
# 日志记录(工程用)
if nid == NID_FACE_STATE:
state = info.get("state")
self.log(f"[INFO] 人脸状态: {state}, yaw={info.get('yaw')}, pitch={info.get('pitch')}, roll={info.get('roll')}")
elif nid == NID_READY:
self.log("[INFO] 模组已就绪")
elif nid == NID_OTA_DONE:
self.log("[INFO] 固件升级完成")
elif nid == NID_UNKNOWNERROR:
self.log("[ERROR] 模组发生未知错误")
# ---------------- REPLY 处理 ----------------
def _handle_enroll_reply(self, info: dict):
user_id = info.get("user_id")
if info.get("result") == 0x00:
if user_id:
if save_user(user_id):
self.refresh()
self.log(f"[INFO] 用户ID={user_id} 已保存")
else:
DialogInform(self).information("提示", f"用户ID {user_id} 已存在!")
else:
self.log(f"[ERROR] 注册失败, 用户ID={user_id}")
def _handle_verify_reply(self, info: dict):
"""
验证结果处理
"""
user_id = info.get("user_id")
result = info.get("result")
# 验证结束,状态机复位
self.verify_in_progress = False
if result == 0x00:
print(f"[INFO] 用户(ID={user_id}) 验证通过")
self.face_verify_result = True
else:
print(f"[INFO] 用户(ID={user_id}) 验证失败")
self.face_verify_result = False
def _handle_get_all_userid_reply(self, info: dict):
count = info.get("count")
result = info.get("result")
user_ids = info.get("user_ids", [])
if result == 0x00:
DialogInform(self).information("提示", f"{count} 个用户\n用户ID 列表:" + ", ".join(map(str, user_ids)))
# 日志 # 日志
def log(self, s: str): def log(self, s: str):
@ -595,8 +664,6 @@ class QFaceCameraViewPage(PageTemplate):
self.close_serial() self.close_serial()
super().closeEvent(e) super().closeEvent(e)
#P06故障查询页面 QFaultQueryPage #P06故障查询页面 QFaultQueryPage
class QFaultQueryPage(PageTemplate): class QFaultQueryPage(PageTemplate):
def __init__(self, parent_window): def __init__(self, parent_window):
@ -774,7 +841,7 @@ class APPWindow(QMainWindow):
QFaceCameraViewPage.video_worker = None QFaceCameraViewPage.video_worker = None
QFaceCameraViewPage.ser = None QFaceCameraViewPage.ser = None
self.showFullScreen() self.showFullScreen()
self.menu_sequence_list : PageTemplate = [] self.menu_sequence_list : PageTemplate = []

View File

@ -3037,7 +3037,7 @@ color: rgb(255, 170, 0);</string>
</rect> </rect>
</property> </property>
<property name="statusTip"> <property name="statusTip">
<string>Index=7, Action=SetPage5_1,SelectImag=IMxx_00D.png</string> <string>Index=7, Action=SetPage5_1,SelectImag=IMxx_00D.png,password</string>
</property> </property>
<property name="text"> <property name="text">
<string/> <string/>

View File

@ -91,39 +91,13 @@
<property name="flat"> <property name="flat">
<bool>false</bool> <bool>false</bool>
</property> </property>
<widget class="QPushButton" name="btn_save">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>211</width>
<height>41</height>
</rect>
</property>
<property name="statusTip">
<string/>
</property>
<property name="styleSheet">
<string notr="true">background-color: rgb(0, 0, 0);
color: rgb(170, 0, 0);</string>
</property>
<property name="text">
<string>日志</string>
</property>
<property name="iconSize">
<size>
<width>72</width>
<height>144</height>
</size>
</property>
</widget>
<widget class="QTextEdit" name="txt_log"> <widget class="QTextEdit" name="txt_log">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>-20</x> <x>-20</x>
<y>60</y> <y>20</y>
<width>701</width> <width>701</width>
<height>301</height> <height>341</height>
</rect> </rect>
</property> </property>
</widget> </widget>
@ -182,7 +156,7 @@ color: rgb(170, 0, 0);</string>
</rect> </rect>
</property> </property>
<property name="statusTip"> <property name="statusTip">
<string/> <string>mian=51</string>
</property> </property>
<property name="title"> <property name="title">
<string>命令</string> <string>命令</string>
@ -197,7 +171,7 @@ color: rgb(170, 0, 0);</string>
</rect> </rect>
</property> </property>
<property name="statusTip"> <property name="statusTip">
<string>Index=0, Action=Reset,SelectImag=IMxx_00F.png</string> <string>Index=0, Action=Reset,SelectImag=IMxx_00F.png,groupstart=8</string>
</property> </property>
<property name="text"> <property name="text">
<string>复位</string> <string>复位</string>
@ -213,65 +187,33 @@ color: rgb(170, 0, 0);</string>
</rect> </rect>
</property> </property>
<property name="statusTip"> <property name="statusTip">
<string>Index=0, Action=VideoMode,SelectImag=IMxx_00F.png</string> <string>Index=1, Action=VideoMode,SelectImag=IMxx_00F.png</string>
</property> </property>
<property name="text"> <property name="text">
<string>视频模式</string> <string>视频模式</string>
</property> </property>
</widget> </widget>
<widget class="QPushButton" name="btn_verify">
<property name="geometry">
<rect>
<x>12</x>
<y>115</y>
<width>80</width>
<height>23</height>
</rect>
</property>
<property name="statusTip">
<string>Index=0, Action=Verify,password,SelectImag=IMxx_00F.png</string>
</property>
<property name="text">
<string>识别</string>
</property>
</widget>
<widget class="QPushButton" name="btn_enroll"> <widget class="QPushButton" name="btn_enroll">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>12</x> <x>10</x>
<y>144</y> <y>210</y>
<width>80</width> <width>80</width>
<height>23</height> <height>23</height>
</rect> </rect>
</property> </property>
<property name="statusTip"> <property name="statusTip">
<string>Index=0, Action=EnrollItgSingle,SelectImag=IMxx_00F.png</string> <string>Index=5, Action=EnrollItgSingle,SelectImag=IMxx_00F.png</string>
</property> </property>
<property name="text"> <property name="text">
<string>ITG注册</string> <string>ITG注册</string>
</property> </property>
</widget> </widget>
<widget class="QPushButton" name="btn_users">
<property name="geometry">
<rect>
<x>12</x>
<y>173</y>
<width>80</width>
<height>23</height>
</rect>
</property>
<property name="statusTip">
<string>Index=0, Action=Users,SelectImag=IMxx_00F.png</string>
</property>
<property name="text">
<string>用户管理</string>
</property>
</widget>
<widget class="QLineEdit" name="FaceRecogTimeoutEdit"> <widget class="QLineEdit" name="FaceRecogTimeoutEdit">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>0</x> <x>-10</x>
<y>310</y> <y>180</y>
<width>120</width> <width>120</width>
<height>25</height> <height>25</height>
</rect> </rect>
@ -286,7 +228,7 @@ color: rgb(170, 0, 0);</string>
<enum>Qt::NoFocus</enum> <enum>Qt::NoFocus</enum>
</property> </property>
<property name="statusTip"> <property name="statusTip">
<string>System=FaceRecogTimeout, Action=ModifySystem, Index=13,SelectImag=P4_ParaSelect.png, password</string> <string>System=FaceRecogTimeout, Action=ModifySystem, Index=4,SelectImag=P4_ParaSelect.png, password</string>
</property> </property>
<property name="styleSheet"> <property name="styleSheet">
<string notr="true">background-color: rgba(85, 170, 127,0); <string notr="true">background-color: rgba(85, 170, 127,0);
@ -306,13 +248,13 @@ color: rgb(177, 229, 252);</string>
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>10</x> <x>10</x>
<y>210</y> <y>90</y>
<width>80</width> <width>80</width>
<height>23</height> <height>23</height>
</rect> </rect>
</property> </property>
<property name="statusTip"> <property name="statusTip">
<string>Index=0, Action=ConnectCamera,password,SelectImag=IMxx_00F.png</string> <string>Index=2, Action=ConnectCamera,SelectImag=IMxx_00F.png</string>
</property> </property>
<property name="text"> <property name="text">
<string>打开视频</string> <string>打开视频</string>
@ -322,13 +264,13 @@ color: rgb(177, 229, 252);</string>
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>10</x> <x>10</x>
<y>90</y> <y>120</y>
<width>80</width> <width>80</width>
<height>23</height> <height>23</height>
</rect> </rect>
</property> </property>
<property name="statusTip"> <property name="statusTip">
<string>Index=0, Action=FaceBox,SelectImag=IMxx_00F.png</string> <string>Index=3, Action=FaceBox,SelectImag=IMxx_00F.png</string>
</property> </property>
<property name="text"> <property name="text">
<string>人脸框</string> <string>人脸框</string>
@ -344,12 +286,28 @@ color: rgb(177, 229, 252);</string>
</rect> </rect>
</property> </property>
<property name="statusTip"> <property name="statusTip">
<string>Index=0, Action=DeleteUser,SelectImag=IMxx_00F.png</string> <string>Index=6, Action=DeleteUser,SelectImag=IMxx_00F.png</string>
</property> </property>
<property name="text"> <property name="text">
<string>删除用户</string> <string>删除用户</string>
</property> </property>
</widget> </widget>
<widget class="QPushButton" name="btn_delete_user_id_2">
<property name="geometry">
<rect>
<x>10</x>
<y>270</y>
<width>80</width>
<height>23</height>
</rect>
</property>
<property name="statusTip">
<string>Index=7, Action=UserCount,SelectImag=IMxx_00F.png,groupend=8</string>
</property>
<property name="text">
<string>查看用户</string>
</property>
</widget>
</widget> </widget>
<widget class="QLabel" name="P05_01BG"> <widget class="QLabel" name="P05_01BG">
<property name="geometry"> <property name="geometry">

View File

@ -42,7 +42,7 @@ import serial.tools.list_ports
from Shared_CODE.FaceRecognitionProtocol import ( from Shared_CODE.FaceRecognitionProtocol import (
build_reset, build_uvc_view, build_face_view, build_verify, build_reset, build_uvc_view, build_face_view, build_verify,
build_enroll_itg_single, build_delete_all, build_get_all_userid, build_enroll_itg_single, build_delete_all, build_get_all_userid,
build_delete_user, unpack_frame, parse_reply, parse_note, build_delete_user, unpack_frame, parse_reply, parse_note,build_enroll_single,
MID_REPLY, MID_NOTE, CMD_ENROLL, CMD_ENROLL_ITG MID_REPLY, MID_NOTE, CMD_ENROLL, CMD_ENROLL_ITG
) )
@ -829,8 +829,6 @@ class UIFrameWork(QMainWindow, class_comm_mqtt_interface):
self._face_verify_locked = False # 解锁 self._face_verify_locked = False # 解锁
if face_frame.face_verify_result: if face_frame.face_verify_result:
input = True input = True
inform_box = DialogInform()
inform_box.information("提示", "人脸认证成功")
self._after_face_verify(select_object, action_str) self._after_face_verify(select_object, action_str)
else: else:
input = False input = False
@ -840,8 +838,6 @@ class UIFrameWork(QMainWindow, class_comm_mqtt_interface):
self._face_verify_timer.stop() self._face_verify_timer.stop()
self._face_verify_locked = False # 解锁 self._face_verify_locked = False # 解锁
input = False input = False
inform_box = DialogInform()
inform_box.information("提示", "人脸认证超时")
# 解绑旧槽,绑定新槽 # 解绑旧槽,绑定新槽
try: try:
self._face_verify_timer.timeout.disconnect() self._face_verify_timer.timeout.disconnect()
@ -963,16 +959,17 @@ class UIFrameWork(QMainWindow, class_comm_mqtt_interface):
pd_val = 0 pd_val = 0
timeout_val = system_parameter().get_verify_timeout() timeout_val = system_parameter().get_verify_timeout()
face_send = self.parent_window.P05_01_FaceCameraView face_send = self.parent_window.P05_01_FaceCameraView
face_send.verify_in_progress = True
face_send.send(build_verify(pd_val, timeout_val)) face_send.send(build_verify(pd_val, timeout_val))
def do_enroll_itg_single(self): def do_enroll_itg_single(self):
admin_val = 0 admin_val = 0
uname = " " uname = " "
face_dir = 31
timeout_val = system_parameter().get_verify_timeout() timeout_val = system_parameter().get_verify_timeout()
itg_val = 0 face_send = self.parent_window.P05_01_FaceCameraView
self.send(build_enroll_itg_single(admin_val, uname, face_dir, timeout_val, itg_val)) face_send.verify_in_progress = True
self.send(build_enroll_single(admin_val, uname, timeout_val))
def do_manage_users(self): def do_manage_users(self):
UserManageDialog(self, self.send).exec_() UserManageDialog(self, self.send).exec_()
@ -1046,7 +1043,7 @@ class UIFrameWork(QMainWindow, class_comm_mqtt_interface):
else: else:
self.log("[WARN] 串口未连接,无法控制人脸框") self.log("[WARN] 串口未连接,无法控制人脸框")
def delete_user_by_id(self, CSV_FILE = CSV_FILE): def delete_user_by_id(self, CSV_FILE=CSV_FILE):
users = load_users() users = load_users()
# 弹出对话框选择用户ID # 弹出对话框选择用户ID
@ -1064,22 +1061,27 @@ class UIFrameWork(QMainWindow, class_comm_mqtt_interface):
DialogInform(self).information("提示", "请输入有效数字ID") DialogInform(self).information("提示", "请输入有效数字ID")
return return
# 查找用户 user_id_str = str(user_id)
user = next((u for u in users if u["user_id"] == user_id), None)
# 查找用户(用字符串匹配,避免类型问题)
user = next((u for u in users if str(u["user_id"]).strip() == user_id_str), None)
if not user: if not user:
DialogInform(self).information("提示", "用户不存在") DialogInform(self).information("提示", "用户不存在")
return return
try: try:
# 1⃣ 下发删除命令 # 1⃣ 下发删除命令(串口模块)
self.send(build_delete_user(user_id)) try:
self.send(build_delete_user(user_id))
except Exception as e:
DialogInform(self).information("提示", f"警告:串口删除失败,但本地仍会删除\n{e}")
# 2⃣ 删除 CSV 文件对应行 # 2⃣ 删除 CSV 文件对应行
if os.path.exists(CSV_FILE): if os.path.exists(CSV_FILE):
with open(CSV_FILE, "r", encoding="utf-8", newline="") as f: with open(CSV_FILE, "r", encoding="utf-8", newline="") as f:
reader = csv.DictReader(f) reader = csv.DictReader(f)
fieldnames = reader.fieldnames fieldnames = reader.fieldnames
new_rows = [row for row in reader if str(row.get("user_id")) != str(user_id)] new_rows = [row for row in reader if str(row.get("user_id")).strip() != user_id_str]
with open(CSV_FILE, "w", encoding="utf-8", newline="") as f: with open(CSV_FILE, "w", encoding="utf-8", newline="") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames) writer = csv.DictWriter(f, fieldnames=fieldnames)
@ -1091,12 +1093,12 @@ class UIFrameWork(QMainWindow, class_comm_mqtt_interface):
return return
# 3⃣ 更新内存用户列表 # 3⃣ 更新内存用户列表
users = [u for u in users if u["user_id"] != user_id] users = [u for u in users if str(u["user_id"]).strip() != user_id_str]
save_users_list(users) save_users_list(users)
# 4⃣ 提示删除成功 # 4⃣ 提示删除成功
DialogInform(self).information("提示", f"用户 {user.get('user_name', '')} (ID={user_id}) 已删除") DialogInform(self).information("提示", f"用户 {user.get('user_name', '')} (ID={user_id}) 已删除")
self.refresh()
def query_user_count(self): def query_user_count(self):
self.send(build_get_all_userid()) self.send(build_get_all_userid())

View File

@ -78,7 +78,7 @@ CMD_NAMES = {
CMD_VERIFY: "人脸验证", CMD_VERIFY: "人脸验证",
CMD_LED_CONTROL: "LED控制", CMD_LED_CONTROL: "LED控制",
CMD_ENROLL: "人脸录入(多帧)", CMD_ENROLL: "人脸录入(多帧)",
CMD_ENROLL_SINGLE: "人脸录入(单帧)", CMD_ENROLL_SINGLE: "人脸录入",
CMD_ENROLL_ITG: "人脸录入(集成式)", CMD_ENROLL_ITG: "人脸录入(集成式)",
CMD_DELETE_USER: "删除单个用户", CMD_DELETE_USER: "删除单个用户",
CMD_DELETE_ALL: "删除所有用户", CMD_DELETE_ALL: "删除所有用户",
@ -104,7 +104,7 @@ RESULT_NAMES = {
0x09: "失败:超过最大用户数", 0x09: "失败:超过最大用户数",
0x0A: "失败:已录入该用户", 0x0A: "失败:已录入该用户",
0x0C: "失败:活体检测未通过", 0x0C: "失败:活体检测未通过",
0x0D: "失败:超时", 0x0D: "失败:人脸超时",
0x0E: "失败:认证失败", 0x0E: "失败:认证失败",
0x13: "失败:文件读取错误", 0x13: "失败:文件读取错误",
0x14: "失败:文件写入错误", 0x14: "失败:文件写入错误",
@ -372,34 +372,105 @@ def parse_reply(data: bytes) -> dict:
# 自动生成提示消息 # 自动生成提示消息
if info["ok"]: if info["ok"]:
if mid == CMD_VERIFY and "user_id" in info: if mid == CMD_VERIFY:
info["message"] = ( info["message"] = (
f"{info['mid_name']} 成功 - 用户ID: {info['user_id']}" f"{info['mid_name']} 成功 - 用户ID: {info['user_id']}"
) )
pass
if mid == CMD_FACE_VIEW or mid == CMD_UVC_VIEW:
info["message"] = f"{info['mid_name']} 成功"
pass
else: else:
info["message"] = f"{info['mid_name']} 成功" info["message"] = f"{info['mid_name']} 成功"
inform_box : DialogInform = DialogInform()
inform_box.information(f"{info['mid_name']}", f"{info['mid_name']}成功")
else: else:
info["message"] = f"{info['mid_name']} 失败: {info['result_name']}" info["message"] = f"{info['mid_name']} 失败: {info['result_name']}"
inform_box : DialogInform = DialogInform()
inform_box.information(f"{info['mid_name']}", f"{info['result_name']} ")
return info return info
def parse_note(data: bytes) -> dict: def parse_note(data: bytes) -> dict:
"""
NOTE 消息解析:
- 保留工程信息(方便调试)
- 提供用户可读提示(仅在已知状态下提示)
"""
if len(data) < 1: if len(data) < 1:
return {"type":"NOTE","error":"short"} return {"type": "NOTE", "error": "short"}
nid = data[0] nid = data[0]
rest = data[1:] rest = data[1:]
info = {"type":"NOTE","nid": nid, "nid_name": NOTE_NAMES.get(nid, f"0x{nid:02X}")} info = {
"type": "NOTE",
"nid": nid,
"nid_name": NOTE_NAMES.get(nid, f"0x{nid:02X}")
}
user_message = None
# ---------- 人脸状态 ----------
if nid == NID_FACE_STATE and len(rest) >= 16: if nid == NID_FACE_STATE and len(rest) >= 16:
vals = struct.unpack(">hhhhhhhh", rest[:16]) # state 用 1 个字节就够,第二个字节是保留/扩展
state = rest[0]
sub_state = rest[1]
left = int.from_bytes(rest[2:4], "big", signed=True)
top = int.from_bytes(rest[4:6], "big", signed=True)
right = int.from_bytes(rest[6:8], "big", signed=True)
bottom = int.from_bytes(rest[8:10], "big", signed=True)
yaw = int.from_bytes(rest[10:12], "big", signed=True)
pitch = int.from_bytes(rest[12:14], "big", signed=True)
roll = int.from_bytes(rest[14:16], "big", signed=True)
info.update({ info.update({
"state": vals[0], "state": state,
"left": vals[1], "top": vals[2], "right": vals[3], "bottom": vals[4], "sub_state": sub_state,
"yaw": vals[5], "pitch": vals[6], "roll": vals[7] "left": left, "top": top, "right": right, "bottom": bottom,
"yaw": yaw, "pitch": pitch, "roll": roll
}) })
# 只处理已知状态
state_messages = {
0: "人脸检测正常,请保持姿势",
1: "未检测到人脸,请面对摄像头",
6: "请靠近摄像头",
7: "请稍微远离摄像头",
8: "检测到遮挡,请移开遮挡物",
9: "请正对摄像头",
}
if state in state_messages:
user_message = state_messages[state]
# 如果是正常状态,可以补充角度提示
if state == 0:
if yaw > 15:
user_message += "(请把头向左转一些)"
elif yaw < -15:
user_message += "(请把头向右转一些)"
elif pitch > 15:
user_message += "(请抬起头)"
elif pitch < -15:
user_message += "(请低下头)"
# ---------- 其他 NOTE ----------
elif nid == NID_READY:
user_message = "设备已就绪,可以开始人脸识别"
elif nid == NID_UNKNOWNERROR:
user_message = "发生未知错误,请重试"
elif nid == NID_OTA_DONE:
user_message = "升级完成,请重启设备"
elif nid == NID_EYE_STATE:
user_message = "检测到眼睛状态变化"
if user_message:
info["user_message"] = user_message
return info return info
def parse_image(data: bytes) -> dict: def parse_image(data: bytes) -> dict:
return {"type":"IMAGE","jpeg":data} return {"type":"IMAGE","jpeg":data}

View File

@ -1,3 +1 @@
1,1,2025-09-15 10:48:28 1,1,2025-09-15 15:31:32
2,2,2025-09-15 10:56:46
3,3,2025-09-15 11:00:04

1 1 1 2025-09-15 10:48:28 2025-09-15 15:31:32
2 2 2025-09-15 10:56:46
3 3 2025-09-15 11:00:04