import cv2
import asyncio
import websockets
import json
import yaml
import logging
from logging.handlers import RotatingFileHandler
from datetime import datetime, timedelta
from typing import Optional, Dict, Any, Tuple
import numpy as np
import time
from collections import defaultdict
from PIL import Image, ImageDraw, ImageFont, ImageFilter
import subprocess
import os
import threading
import queue
from compreface import CompreFace
from compreface.service import RecognitionService, DetectionService

# 把鼠标移到屏幕右下角
def move_cursor_to_corner():
    """把鼠标移到屏幕右下角"""
    try:
        import os
        if not os.environ.get('DISPLAY'):
            return
            
        import ctypes
        libX11 = cdll.LoadLibrary('libX11.so.6')
        
        display = libX11.XOpenDisplay(None)
        if not display:
            return
            
        screen = libX11.XDefaultScreen(display)
        width = libX11.XDisplayWidth(display, screen)
        height = libX11.XDisplayHeight(display, screen)
        
        # 移动到右下角（减去一点偏移）
        libX11.XWarpPointer(display, None, None, 0, 0, 0, 0, width - 10, height - 10)
        libX11.XFlush(display)
        libX11.XCloseDisplay(display)
        
        print(f"Cursor moved to corner: {width}x{height}")
    except Exception as e:
        print(f"Move cursor error: {e}")


class FaceRecognitionSystem:
    def __init__(self, config_path: str = "config.yaml"):
        """初始化人脸识别系统"""
        # 先移动鼠标光标到右下角（在任何窗口创建之前）
        move_cursor_to_corner()
        
        # 加载配置
        with open(config_path, 'r', encoding='utf-8') as f:
            self.config = yaml.safe_load(f)

        # 设置日志
        self._setup_logging()

        # 初始化CompreFace
        self._init_compreface()

        # 初始化摄像头
        self.camera = None

        # WebSocket连接
        self.ws = None
        self.ws_url = self.config['websocket']['url']
        self.ws_connected = False  # WebSocket连接状态标志
        self.ws_reconnect_count = 0  # 重连次数计数(用于日志控制)

        # 状态变量
        self.robot_status = {
            'is_speaking': False,
            'is_thinking': False,
            'listening': False,
            'is_asr_processing': False,
            'role_name': '',
            'mode': '',  # 模式: await(待机)/local(展厅讲解员)/smart(访客引导者)
            'face_detect_inited': False,  # 人脸识别系统是否准备就绪
            'battery': 0,  # 电池电量 %
            'bms_electric': 0,  # 电池电流 mA
            'bms_voltage': 0,  # 电池电压 mV
            'run_time': 0,  # 运行时间 s
            'style_name': '',  # 当前风格
            'is_actioning': False,  # 是否正在做动作
            'volume': 0,  # 当前音量
            'voice_id': '',  # 语音ID
        }

        # 人脸检测状态
        self.frame_count = 0
        self.face_present_start = None
        self.current_face_id = None

        # 识别记录(防止重复识别)
        self.recognition_history = {}  # {person_id: last_recognition_time}

        # 定期清理机制
        self.last_cleanup_time = time.time()
        self.cleanup_interval = 60  # 清理间隔(秒)

        # 显示相关变量
        self.current_display_frame = None
        self.last_detection_result = None  # 最后的检测结果
        self.last_recognition_result = None  # 最后的识别结果
        self.display_info = {
            'quality': 0,
            'face_detected': False,
            'face_box': None,
            'face_box_mirrored': False,  # 标记 face_box 是否已翻转
            'person_name': None,
            'person_role': None,
            'similarity': 0,
            'frame_count': 0,
            'fps': 0
        }
        self.last_fps_time = time.time()
        self.fps_counter = 0

        # 加载中文字体
        self.font_path = self._get_chinese_font()
        self.font_small = ImageFont.truetype(self.font_path, 20)
        self.font_medium = ImageFont.truetype(self.font_path, 24)
        self.font_large = ImageFont.truetype(self.font_path, 50)
        
        # SOTA QR Design Fonts
        self.font_qr_title = ImageFont.truetype(self.font_path, 56)
        self.font_qr_subtitle = ImageFont.truetype(self.font_path, 30)
        self.font_qr_body = ImageFont.truetype(self.font_path, 24)
        self.font_qr_step_title = ImageFont.truetype(self.font_path, 28)
        self.font_qr_badge = ImageFont.truetype(self.font_path, 30)

        self.qrcode_instruction_title = "访客预约流程"
        self.qrcode_instruction_subtitle = "Visitor Registration Process"
        self.qrcode_instruction_steps = [
            "第一步：扫码关注“康达新材”公众号。",
            "第二步：点击【关于我们】→【我是访客】进入“访客注册”界面，填写并上传相应信息并点击“提交”。",
            "第三步：将第二步信息提交完毕后在“访客预约”界面选择右下角的“+”号按钮，填写预约信息。",
            "第四步：请仔细阅读安全告知书，点击我知道了。",
            "第五步：填写被访人信息及来访事由等内容并提交。",
            "第六步：显示提交成功，并且手机、微信会收到预约相关短信通知。",
        ]

        # FFmpeg推流进程
        self.ffmpeg_process = None
        self.stream_enabled = self.config.get('stream', {}).get('enabled', False)
        self.stream_retry_count = 0  # 推流重试计数
        self.stream_max_retries = self.config.get('stream', {}).get('max_retries', 5)  # 最大重试次数
        self.stream_last_retry_time = None  # 上次重试时间
        self.stream_retry_cooldown = 10  # 重试冷却时间(秒)

        # 添加异步推流队列和线程
        self.stream_frame_queue = queue.Queue(maxsize=30)  # 增加到30帧缓冲
        self.stream_thread = None
        self.stream_thread_running = False

        self.logger.info("人脸识别系统初始化完成")

        # 添加摄像头状态跟踪
        self.camera_failure_count = 0  # 连续失败次数
        self.camera_last_retry_time = None  # 上次重试时间
        self.camera_retry_cooldown = self.config['camera'].get('retry_interval', 2)  # 重试冷却时间(秒)
        self.camera_max_failures = self.config['camera'].get('max_failures', 5)  # 触发重新初始化的失败次数阈值

        # 缓存屏幕尺寸，避免每帧重新获取
        self._cache_screen_size()

        # 二维码显示相关
        self.qrcode_image_path = self.config.get('qrcode', {}).get('image_path', 'qrcode.png')
        self.qrcode_display_duration = self.config.get('qrcode', {}).get('display_duration', 10)
        self.qrcode_window_name = 'Visitor guidance QR code'
        self.qrcode_display_start_time = None  # 二维码显示开始时间
        self.qrcode_showing = False  # 二维码是否正在显示

        # 视频播放相关
        self.video_path = self.config.get('video', {}).get('path', 'exhibition.mp4')
        self.video_window_name = 'Hello Video'
        self.video_showing = False
        self.video_capture = None
        self.video_start_time = None
        self.video_loop = self.config.get('video', {}).get('loop', True)  # 是否循环播放
        self.last_robot_role = ''  # 记录上一次的机器人角色

        # 添加stderr读取线程
        self.stderr_thread = None
        self.stderr_thread_running = False

        # 缓存屏幕尺寸，避免每帧重新获取
        self._cache_screen_size()

    def _cache_screen_size(self):
        """缓存屏幕尺寸，提高性能"""
        self.screen_width = 1920
        self.screen_height = 1080
        try:
            import screeninfo
            screen = screeninfo.get_monitors()[0]
            self.screen_width = screen.width
            self.screen_height = screen.height
            self.logger.info(f"屏幕尺寸已缓存: {self.screen_width}x{self.screen_height}")
        except Exception as e:
            self.logger.warning(f"无法获取屏幕尺寸，使用默认尺寸 1920x1080: {e}")

    def _force_cleanup_ffmpeg(self):
        """强制清理所有FFmpeg相关资源 - 防止多进程推流"""
        self.logger.info("=== 开始清理FFmpeg资源 ===")

        # 1. 停止stderr读取线程
        if self.stderr_thread and self.stderr_thread.is_alive():
            self.logger.debug("停止stderr线程...")
            self.stderr_thread_running = False
            self.stderr_thread.join(timeout=1)

        # 2. 停止推流线程
        if self.stream_thread and self.stream_thread.is_alive():
            self.logger.debug("停止推流线程...")
            self.stream_thread_running = False
            try:
                self.stream_frame_queue.put(None, timeout=0.5)
            except:
                pass
            self.stream_thread.join(timeout=2)

        # 3. 清空帧队列
        cleared = 0
        while not self.stream_frame_queue.empty():
            try:
                self.stream_frame_queue.get_nowait()
                cleared += 1
            except:
                break
        if cleared > 0:
            self.logger.debug(f"清空队列: {cleared}帧")

        # 4. 强制终止FFmpeg进程
        if self.ffmpeg_process:
            self.logger.warning("发现旧的FFmpeg进程，强制终止...")
            try:
                # 先尝试关闭stdin
                if self.ffmpeg_process.stdin and not self.ffmpeg_process.stdin.closed:
                    try:
                        self.ffmpeg_process.stdin.close()
                    except:
                        pass

                # 获取进程PID
                pid = self.ffmpeg_process.pid
                self.logger.info(f"终止FFmpeg进程 PID={pid}")

                # 立即kill（不等待graceful shutdown）
                self.ffmpeg_process.kill()
                self.ffmpeg_process.wait(timeout=2)
                self.logger.info(f"✓ FFmpeg进程已终止 (PID={pid})")

            except Exception as e:
                self.logger.error(f"终止FFmpeg进程失败: {e}")
            finally:
                self.ffmpeg_process = None

        # 5. 额外安全措施：杀死所有可能残留的ffmpeg进程
        try:
            import psutil
            current_pid = os.getpid()
            killed_count = 0

            for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
                try:
                    # 查找ffmpeg进程
                    if proc.info['name'] and 'ffmpeg' in proc.info['name'].lower():
                        # 检查是否是我们启动的（通过检查命令行参数）
                        cmdline = proc.info.get('cmdline', [])
                        if cmdline and any('rawvideo' in str(arg) for arg in cmdline):
                            if proc.pid != current_pid:
                                self.logger.warning(f"发现残留FFmpeg进程 PID={proc.pid}, 正在终止...")
                                proc.kill()
                                proc.wait(timeout=2)
                                killed_count += 1
                except (psutil.NoSuchProcess, psutil.AccessDenied):
                    pass

            if killed_count > 0:
                self.logger.warning(f"✓ 清理了 {killed_count} 个残留FFmpeg进程")

        except ImportError:
            self.logger.debug("未安装psutil，跳过残留进程清理（建议安装: pip install psutil）")
        except Exception as e:
            self.logger.debug(f"清理残留进程时出错: {e}")

        # 6. 等待一小段时间确保资源释放
        time.sleep(0.5)

        self.logger.info("=== FFmpeg资源清理完成 ===")

    def _show_video(self):
        """显示展厅讲解视频"""
        try:
            if not os.path.exists(self.video_path):
                self.logger.error(f"视频文件不存在: {self.video_path}")
                return False

            # 如果视频已经在播放,不需要重复初始化
            if self.video_showing and self.video_capture is not None:
                return True

            # 打开视频文件
            self.video_capture = cv2.VideoCapture(self.video_path)
            if not self.video_capture.isOpened():
                self.logger.error(f"无法打开视频文件: {self.video_path}")
                return False

            # 创建视频窗口并设置为全屏
            cv2.namedWindow(self.video_window_name, cv2.WINDOW_NORMAL)

            # 设置窗口为全屏模式
            cv2.setWindowProperty(self.video_window_name, cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)

            # 获取视频信息
            video_width = int(self.video_capture.get(cv2.CAP_PROP_FRAME_WIDTH))
            video_height = int(self.video_capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
            video_fps = self.video_capture.get(cv2.CAP_PROP_FPS)

            self.logger.info(f"视频全屏显示: 原始尺寸 {video_width}x{video_height}, FPS: {video_fps:.2f}")

            self.video_showing = True
            self.video_start_time = time.time()
            self.logger.info(f"开始播放展厅讲解视频: {self.video_path} (FPS: {video_fps:.2f})")
            return True

        except Exception as e:
            self.logger.error(f"显示视频失败: {e}")
            return False

    def _draw_robot_status_on_video(self, frame: np.ndarray) -> np.ndarray:
        """在视频帧上绘制机器人状态信息"""
        try:
            h, w = frame.shape[:2]

            # 准备状态文本
            status_texts = []

            # 模式中文映射
            mode_map = {
                'await': '待机模式',
                'local': '展厅讲解员',
                'smart': '访客引导者'
            }

            # 显示当前模式
            mode_prefix = ""
            if self.robot_status['mode']:
                mode_name = mode_map.get(self.robot_status['mode'], self.robot_status['mode'])
                mode_prefix = f"{mode_name}: "

            # 收集状态信息
            status_parts = []
            if self.robot_status['listening']:
                status_parts.append("已开启对话")
            else:
                status_parts.append("已关闭对话")
            if self.robot_status['is_thinking']:
                status_parts.append("正在思考...")
            if self.robot_status['is_asr_processing']:
                status_parts.append("正在倾听...")
            if self.robot_status['is_speaking']:
                status_parts.append("正在讲话...")

            # 模式 + 状态信息在同一行
            if mode_prefix or status_parts:
                status_texts.append(mode_prefix + " ".join(status_parts))

            # 如果没有状态需要显示，直接返回原帧
            if not status_texts:
                return frame

            # 转换为PIL图像以支持中文
            frame_pil = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
            draw = ImageDraw.Draw(frame_pil)

            # 计算文本位置（中间上方）
            y_start = 50  # 距离顶部的距离
            line_height = 45

            for i, text in enumerate(status_texts):
                # 获取文本尺寸（兼容旧版本Pillow）
                try:
                    # Pillow >= 8.0.0
                    bbox = draw.textbbox((0, 0), text, font=self.font_large)
                    text_width = bbox[2] - bbox[0]
                    text_height = bbox[3] - bbox[1]
                except AttributeError:
                    # Pillow < 8.0.0
                    text_width, text_height = draw.textsize(text, font=self.font_large)

                # 计算居中位置
                x = (w - text_width) // 2
                y = y_start + i * line_height

                # 绘制半透明背景
                padding = 15
                bg_x1 = x - padding
                bg_y1 = y - padding
                bg_x2 = x + text_width + padding
                bg_y2 = y + text_height + padding

                # 创建半透明背景
                overlay = frame_pil.copy()
                overlay_draw = ImageDraw.Draw(overlay)
                overlay_draw.rectangle(
                    [bg_x1, bg_y1, bg_x2, bg_y2],
                    fill=(0, 0, 0, 180)  # 黑色半透明
                )

                # 混合背景
                frame_pil = Image.blend(frame_pil, overlay, 0.7)
                draw = ImageDraw.Draw(frame_pil)

                # 绘制边框
                draw.rectangle(
                    [bg_x1, bg_y1, bg_x2, bg_y2],
                    outline=(0, 255, 255),  # 黄色边框
                    width=3
                )

                # 绘制文本
                draw.text((x, y), text, font=self.font_large, fill=(255, 255, 255))

            # 转换回OpenCV格式
            frame = cv2.cvtColor(np.array(frame_pil), cv2.COLOR_RGB2BGR)
            return frame

        except Exception as e:
            self.logger.error(f"在视频上绘制状态失败: {e}")
            return frame

    def _update_video(self):
        """更新视频帧(在主循环中调用)"""
        if not self.video_showing or self.video_capture is None:
            return

        try:
            ret, frame = self.video_capture.read()

            if ret:
                # 在视频帧上绘制机器人状态
                frame_with_status = self._draw_robot_status_on_video(frame)

                # 使用缓存的屏幕尺寸拉伸视频帧到全屏
                frame_with_status = cv2.resize(frame_with_status, (self.screen_width, self.screen_height), interpolation=cv2.INTER_LINEAR)

                # 显示视频帧
                cv2.imshow(self.video_window_name, frame_with_status)
            else:
                # 视频播放完毕
                if self.video_loop:
                    # 循环播放,重新定位到开头
                    self.video_capture.set(cv2.CAP_PROP_POS_FRAMES, 0)
                    self.logger.debug("视频循环播放")
                else:
                    # 不循环,关闭视频
                    self.logger.info("视频播放完毕")
                    self._close_video()

        except Exception as e:
            self.logger.error(f"更新视频帧失败: {e}")
            self._close_video()

    def _close_video(self):
        """关闭视频窗口"""
        try:
            if self.video_showing:
                if self.video_capture is not None:
                    self.video_capture.release()
                    self.video_capture = None

                cv2.destroyWindow(self.video_window_name)
                self.video_showing = False
                self.video_start_time = None
                self.logger.info("关闭视频窗口")
        except Exception as e:
            self.logger.debug(f"关闭视频窗口时出错: {e}")

    def _check_role_and_display_video(self):
        """检查机器人角色并显示/隐藏视频"""
        current_role = self.robot_status['role_name']

        # 角色发生变化时的处理
        if current_role != self.last_robot_role:
            self.logger.info(f"机器人角色变化: {self.last_robot_role} -> {current_role}")
            self.last_robot_role = current_role

            # 如果切换到展厅讲解员,显示视频
            if current_role == "展厅讲解员" or current_role == "初始角色":
                self._show_video()
            # 如果切换到其他角色,关闭视频
            elif self.video_showing:
                self._close_video()

        # 如果是展厅讲解员且视频应该显示但未显示,尝试显示
        if (current_role == "展厅讲解员" or current_role == "初始角色") and not self.video_showing:
            self._show_video()

        # 如果不是展厅讲解员但视频在显示,关闭视频
        if current_role != "展厅讲解员" and current_role != "初始角色" and self.video_showing:
            self._close_video()

    def _wrap_text_for_width(self, text: str, font: ImageFont.FreeTypeFont, max_width: int, draw: ImageDraw.ImageDraw):
        lines = []
        current = ''
        for char in text:
            tentative = current + char
            try:
                width = draw.textlength(tentative, font=font)
            except AttributeError:
                width, _ = draw.textsize(tentative, font=font)
            if width <= max_width:
                current = tentative
            else:
                if current:
                    lines.append(current)
                current = char
        if current:
            lines.append(current)
        return lines

    def _create_gradient_background(self, size: Tuple[int, int], start_color: Tuple[int, int, int], end_color: Tuple[int, int, int]) -> Image.Image:
        width, height = size
        # Vertical gradient
        base = Image.new('RGB', size, start_color)
        top = Image.new('RGB', size, end_color)
        mask = Image.new('L', size)
        mask_data = np.tile(np.linspace(0, 255, height, dtype=np.uint8), (width, 1)).T
        mask = Image.fromarray(mask_data, 'L')
        return Image.composite(top, base, mask)

    def _draw_rounded_rect(self, draw, box, radius, fill, outline=None, width=0):
        # 兼容旧版本PIL，手动绘制圆角矩形
        x1, y1, x2, y2 = box
        
        # 绘制主体部分
        draw.rectangle([x1+radius, y1, x2-radius, y2], fill=fill, outline=outline, width=width)
        draw.rectangle([x1, y1+radius, x2, y2-radius], fill=fill, outline=outline, width=width)
        
        # 绘制四个角的圆弧
        if radius > 0:
            # 左上角
            draw.ellipse([x1, y1, x1+2*radius, y1+2*radius], fill=fill, outline=outline, width=width)
            # 右上角
            draw.ellipse([x2-2*radius, y1, x2, y1+2*radius], fill=fill, outline=outline, width=width)
            # 左下角
            draw.ellipse([x1, y2-2*radius, x1+2*radius, y2], fill=fill, outline=outline, width=width)
            # 右下角
            draw.ellipse([x2-2*radius, y2-2*radius, x2, y2], fill=fill, outline=outline, width=width)

    def _draw_shadow(self, image, box, radius, offset=(0, 4), blur=10, shadow_color=(0,0,0,50)):
        # Create a separate shadow layer
        shadow_layer = Image.new('RGBA', image.size, (0,0,0,0))
        shadow_draw = ImageDraw.Draw(shadow_layer)
        
        sx0, sy0, sx1, sy1 = box
        dx, dy = offset
        
        shadow_box = (sx0 + dx, sy0 + dy, sx1 + dx, sy1 + dy)
        shadow_draw.rounded_rectangle(shadow_box, radius=radius, fill=shadow_color)
        
        # Blur the shadow
        shadow_layer = shadow_layer.filter(ImageFilter.GaussianBlur(blur))
        
        # Composite
        image.alpha_composite(shadow_layer)

    def _show_qrcode(self):
        """显示二维码图片 - 直接全屏铺满"""
        try:
            if not os.path.exists(self.qrcode_image_path):
                self.logger.error(f"二维码图片不存在: {self.qrcode_image_path}")
                return False

            # 读取二维码图片
            qr_image = cv2.imread(self.qrcode_image_path)
            if qr_image is None:
                self.logger.error(f"无法读取二维码图片: {self.qrcode_image_path}")
                return False

            # 直接铺满全屏
            canvas_width = self.screen_width
            canvas_height = self.screen_height
            qr_image_scaled = cv2.resize(qr_image, (canvas_width, canvas_height), interpolation=cv2.INTER_LINEAR)

            # 创建二维码窗口
            cv2.namedWindow(self.qrcode_window_name, cv2.WINDOW_NORMAL | cv2.WINDOW_GUI_NORMAL)
            cv2.resizeWindow(self.qrcode_window_name, canvas_width, canvas_height)
            cv2.moveWindow(self.qrcode_window_name, 0, 0)

            # 设置窗口全屏显示
            cv2.setWindowProperty(self.qrcode_window_name, cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)

            cv2.imshow(self.qrcode_window_name, qr_image_scaled)

            # 设置窗口置顶
            cv2.setWindowProperty(self.qrcode_window_name, cv2.WND_PROP_TOPMOST, 1)

            self.qrcode_showing = True
            self.qrcode_display_start_time = time.time()
            self.logger.info(f"显示二维码图片: {self.qrcode_image_path}")
            return True

        except Exception as e:
            self.logger.error(f"显示二维码失败: {e}")
            return False

    def _close_qrcode(self):
        """关闭二维码窗口"""
        try:
            if self.qrcode_showing:
                cv2.destroyWindow(self.qrcode_window_name)
                self.qrcode_showing = False
                self.qrcode_display_start_time = None
                self.logger.info("关闭二维码窗口")
        except Exception as e:
            self.logger.debug(f"关闭二维码窗口时出错: {e}")

    def _check_qrcode_timeout(self):
        """检查二维码是否应该关闭"""
        if self.qrcode_showing and self.qrcode_display_start_time:
            elapsed = time.time() - self.qrcode_display_start_time
            if elapsed >= self.qrcode_display_duration:
                self._close_qrcode()
                return True
        return False

    def _try_reconnect_camera(self) -> bool:
        """尝试重新连接摄像头"""
        current_time = time.time()

        # 检查是否在冷却期内
        if self.camera_last_retry_time:
            elapsed = current_time - self.camera_last_retry_time
            if elapsed < self.camera_retry_cooldown:
                return False

        self.camera_last_retry_time = current_time
        self.logger.info("尝试重新连接摄像头...")

        cam_config = self.config['camera']

        # 释放旧的摄像头资源
        if self.camera is not None:
            try:
                self.camera.release()
            except:
                pass
            self.camera = None

        # 尝试重新打开
        self.camera = cv2.VideoCapture(cam_config['device_id'])

        if self.camera.isOpened():
            # 设置摄像头参数
            self.camera.set(cv2.CAP_PROP_FRAME_WIDTH, cam_config['width'])
            self.camera.set(cv2.CAP_PROP_FRAME_HEIGHT, cam_config['height'])
            self.camera.set(cv2.CAP_PROP_FPS, cam_config['fps'])

            # 验证是否能读取帧
            ret, frame = self.camera.read()
            if ret:
                self.logger.info("✓ 摄像头重新连接成功")
                self.camera_failure_count = 0
                return True
            else:
                self.logger.warning("摄像头已打开但无法读取帧")
                self.camera.release()
                self.camera = None
        else:
            self.logger.warning("无法重新打开摄像头")

        return False

    def _setup_logging(self):
        """设置日志系统"""
        log_config = self.config['logging']
        log_dir = log_config.get('dir', 'logs')

        # 确保日志目录存在
        os.makedirs(log_dir, exist_ok=True)

        # 按日期生成日志文件名
        date_str = datetime.now().strftime('%Y%m%d')
        log_file = os.path.join(log_dir, f'face_rec_{date_str}.log')

        self.logger = logging.getLogger('FaceRecognition')
        self.logger.setLevel(getattr(logging, log_config.get('level', 'INFO')))

        # 清理旧日志（保留最近的5个）
        self._cleanup_old_logs(log_dir)

        # 只保留文件处理器
        file_handler = RotatingFileHandler(
            log_file,
            maxBytes=log_config.get('max_bytes', 10 * 1024 * 1024),
            backupCount=log_config.get('backup_count', 5)
        )

        # 格式化
        formatter = logging.Formatter(
            '%(asctime)s - %(levelname)s - %(message)s'
        )
        file_handler.setFormatter(formatter)

        self.logger.addHandler(file_handler)

        self.logger.info(f"日志文件: {log_file}")

    def _cleanup_old_logs(self, log_dir: str, keep_days: int = None):
        """清理旧的日志文件（保留最近N天）"""
        if keep_days is None:
            keep_days = self.config.get('logging', {}).get('keep_days', 7)
        try:
            from datetime import datetime, timedelta
            cutoff_date = (datetime.now() - timedelta(days=keep_days)).strftime('%Y%m%d')
            
            for f in os.listdir(log_dir):
                if f.startswith('face_rec_') and f.endswith('.log'):
                    date_part = f.replace('face_rec_', '').replace('.log', '')
                    if date_part < cutoff_date:
                        os.remove(os.path.join(log_dir, f))
        except Exception:
            pass

    def _get_chinese_font(self) -> str:
        """获取中文字体路径"""
        import platform
        import os

        system = platform.system()

        # Windows系统
        if system == "Windows":
            font_paths = [
                "C:/Windows/Fonts/msyh.ttc",  # 微软雅黑
                "C:/Windows/Fonts/simhei.ttf",  # 黑体
                "C:/Windows/Fonts/simsun.ttc",  # 宋体
            ]
        # macOS系统
        elif system == "Darwin":
            font_paths = [
                "/System/Library/Fonts/PingFang.ttc",  # 苹方
                "/System/Library/Fonts/STHeiti Medium.ttc",  # 黑体
                "/Library/Fonts/Arial Unicode.ttf",
            ]
        # Linux系统
        else:
            font_paths = [
                "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
                "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
            ]

        # 查找可用的字体
        for font_path in font_paths:
            if os.path.exists(font_path):
                self.logger.info(f"使用中文字体: {font_path}")
                return font_path

        # 如果没找到,尝试使用配置文件中的字体路径
        if 'font_path' in self.config.get('display', {}):
            custom_font = self.config['display']['font_path']
            if os.path.exists(custom_font):
                self.logger.info(f"使用自定义字体: {custom_font}")
                return custom_font

        # 默认使用一个基本字体(不支持中文,但至少不会报错)
        self.logger.warning("未找到中文字体,将使用默认字体(可能无法显示中文)")
        return "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"

    def _init_ffmpeg_stream(self):
        """初始化FFmpeg推流 - 关键修复"""
        if not self.stream_enabled:
            self.logger.info("流媒体推流功能未启用")
            return False

        # ⚠️ 关键修复：先强制清理所有旧的推流进程
        self._force_cleanup_ffmpeg()

        # 检查冷却期
        if self.stream_last_retry_time:
            elapsed = time.time() - self.stream_last_retry_time
            if elapsed < self.stream_retry_cooldown:
                self.logger.debug(f"推流重试冷却中,还需等待 {self.stream_retry_cooldown - elapsed:.1f} 秒")
                return False

        stream_config = self.config['stream']
        ffmpeg_config = stream_config['ffmpeg']
        stream_url = stream_config.get('rtmp_url') or stream_config.get('stream_url', '')

        cam_config = self.config['camera']
        panel_height = 200
        total_height = cam_config['height'] + panel_height

        # 判断输出格式
        if stream_url.startswith('rtmp://'):
            output_format = 'flv'
        elif stream_url.startswith('rtsp://'):
            output_format = 'rtsp'
        else:
            self.logger.error(f"不支持的推流协议: {stream_url}")
            return False

        # 构建FFmpeg命令 - 关键优化
        ffmpeg_cmd = [
            'ffmpeg',
            '-y',
            '-f', 'rawvideo',
            '-pix_fmt', 'bgr24',
            '-s', f"{cam_config['width']}x{total_height}",
            '-r', str(ffmpeg_config['fps']),
            '-i', '-',
            '-c:v', ffmpeg_config['video_codec'],
            '-pix_fmt', ffmpeg_config['pixel_format'],
            '-preset', 'ultrafast',
            '-tune', ffmpeg_config['tune'],
            '-b:v', ffmpeg_config['video_bitrate'],
            '-maxrate', ffmpeg_config['video_bitrate'],
            '-bufsize', '1M',  # 减小缓冲区避免延迟累积
            '-r', str(ffmpeg_config['fps']),
            '-g', str(ffmpeg_config['fps'] * 2),
            '-threads', '2',
            '-flush_packets', '1',  # 立即刷新包
        ]

        if not ffmpeg_config.get('audio', False):
            ffmpeg_cmd.append('-an')

        # RTMP特定参数 - 关键优化
        if output_format == 'flv':
            ffmpeg_cmd.extend([
                '-f', 'flv',
                '-flvflags', 'no_duration_filesize',
                # 关键修复：移除或增加timeout，避免7分钟后的连接超时
                # 原来的5秒太短，改为30秒或移除
                # '-timeout', '30000000',  # 30秒超时(微秒)
                '-reconnect', '1',
                '-reconnect_streamed', '1',
                '-reconnect_delay_max', '5',  # 增加重连延迟
                '-reconnect_at_eof', '1',  # EOF时重连
                stream_url
            ])
        elif output_format == 'rtsp':
            ffmpeg_cmd.extend([
                '-f', 'rtsp',
                '-rtsp_transport', 'tcp',
                stream_url
            ])

        try:
            self.stream_retry_count += 1
            self.stream_last_retry_time = time.time()

            self.logger.info(f"启动FFmpeg推流 (第{self.stream_retry_count}次尝试): {stream_url}")

            # 关键修复：将stderr重定向到DEVNULL而不是PIPE，避免缓冲区满导致阻塞
            # 或者启动线程持续读取stderr
            self.ffmpeg_process = subprocess.Popen(
                ffmpeg_cmd,
                stdin=subprocess.PIPE,
                stdout=subprocess.DEVNULL,
                stderr=subprocess.PIPE,  # 保留PIPE用于错误诊断
                bufsize=512 * 1024  # 512KB缓冲区
            )

            # 启动stderr读取线程 - 关键修复
            self._start_stderr_reader()

            # 等待检查
            time.sleep(0.5)
            if self.ffmpeg_process.poll() is not None:
                stderr_output = self.ffmpeg_process.stderr.read().decode('utf-8', errors='ignore')
                self.logger.error(f"FFmpeg进程启动失败:")
                for line in stderr_output.split('\n'):
                    if 'error' in line.lower() or 'failed' in line.lower():
                        self.logger.error(f"  {line.strip()}")
                self.ffmpeg_process = None
                return False

            self.logger.info("FFmpeg推流进程启动成功")

            # 启动推流线程
            self._start_stream_thread()

            if self.stream_retry_count > 0:
                self.logger.info(f"推流恢复成功 (之前重试了{self.stream_retry_count}次)")
            self.stream_retry_count = 0

            return True

        except FileNotFoundError:
            self.logger.error("FFmpeg未安装或不在系统PATH中")
            self.stream_enabled = False
            return False
        except Exception as e:
            self.logger.error(f"启动FFmpeg推流失败: {e}")
            return False

    def _stderr_reader(self):
        """读取FFmpeg的stderr输出 - 防止管道缓冲区满"""
        last_log_time = time.time()
        error_lines = []

        while self.stderr_thread_running and self.ffmpeg_process:
            try:
                if self.ffmpeg_process.stderr:
                    line = self.ffmpeg_process.stderr.readline()
                    if not line:
                        break

                    line_str = line.decode('utf-8', errors='ignore').strip()

                    # 只记录重要的错误和警告
                    if any(keyword in line_str.lower() for keyword in ['error', 'failed', 'warning']):
                        error_lines.append(line_str)

                        # 每10秒汇总输出一次
                        current_time = time.time()
                        if current_time - last_log_time >= 10:
                            if error_lines:
                                self.logger.warning(f"FFmpeg stderr最近10秒: {len(error_lines)}条消息")
                                # 只输出前3条
                                for err in error_lines[:3]:
                                    self.logger.debug(f"  {err}")
                                error_lines.clear()
                            last_log_time = current_time

            except Exception as e:
                self.logger.debug(f"读取stderr错误: {e}")
                break

        self.logger.debug("stderr读取线程已停止")

    def _start_stderr_reader(self):
        """启动stderr读取线程 - 关键修复：防止管道阻塞"""
        if self.stderr_thread and self.stderr_thread.is_alive():
            return

        self.stderr_thread_running = True
        self.stderr_thread = threading.Thread(target=self._stderr_reader, daemon=True)
        self.stderr_thread.start()
        self.logger.debug("✓ stderr读取线程已启动")

    def _start_stream_thread(self):
        """启动推流线程"""
        if self.stream_thread is not None and self.stream_thread.is_alive():
            self.logger.debug("推流线程已在运行")
            return

        self.stream_thread_running = True
        self.stream_thread = threading.Thread(target=self._stream_worker, daemon=True)
        self.stream_thread.start()
        self.logger.info("✓ 推流线程已启动")

    def _stream_worker(self):
        """推流工作线程 - 添加写入超时保护"""
        dropped_frames = 0
        total_frames = 0
        last_log_time = time.time()
        consecutive_failures = 0
        write_timeout = 1.0  # 写入超时1秒

        while self.stream_thread_running:
            try:
                # 获取帧
                try:
                    frame = self.stream_frame_queue.get(timeout=0.1)
                except queue.Empty:
                    continue

                if frame is None:  # 停止信号
                    break

                total_frames += 1

                # 检查FFmpeg进程
                if self.ffmpeg_process is None or self.ffmpeg_process.poll() is not None:
                    dropped_frames += 1
                    consecutive_failures += 1

                    if consecutive_failures >= 30:
                        if consecutive_failures == 30:
                            self.logger.error("FFmpeg进程异常，需要重启")
                            self.ffmpeg_process = None
                        if consecutive_failures > 100:
                            consecutive_failures = 30
                    continue

                try:
                    # 关键修复：添加写入超时机制
                    frame_bytes = frame.tobytes()

                    # 使用非阻塞写入（通过检查stdin是否可写）
                    if self.ffmpeg_process.stdin and not self.ffmpeg_process.stdin.closed:
                        # 简单的写入，依赖较小的缓冲区和flush_packets
                        self.ffmpeg_process.stdin.write(frame_bytes)
                        # 每5帧flush一次，避免缓冲区堆积
                        if total_frames % 5 == 0:
                            self.ffmpeg_process.stdin.flush()

                        consecutive_failures = 0
                    else:
                        dropped_frames += 1
                        consecutive_failures += 1

                except BrokenPipeError:
                    self.logger.warning("推流管道断开")
                    self.ffmpeg_process = None
                    dropped_frames += 1
                    consecutive_failures += 1
                except IOError as e:
                    if e.errno == 32:  # Broken pipe
                        self.logger.warning("推流管道异常")
                        self.ffmpeg_process = None
                    dropped_frames += 1
                    consecutive_failures += 1
                except Exception as e:
                    self.logger.debug(f"写入帧失败: {e}")
                    dropped_frames += 1
                    consecutive_failures += 1

                # 统计信息
                current_time = time.time()
                if current_time - last_log_time >= 10:
                    if total_frames > 0:
                        drop_rate = (dropped_frames / total_frames) * 100
                        process_status = "运行中" if (
                                    self.ffmpeg_process and self.ffmpeg_process.poll() is None) else "已停止"
                        self.logger.info(
                            f"推流统计: 总帧={total_frames}, 丢帧={dropped_frames}({drop_rate:.1f}%), "
                            f"队列={self.stream_frame_queue.qsize()}, 状态={process_status}"
                        )
                    dropped_frames = 0
                    total_frames = 0
                    last_log_time = current_time

            except Exception as e:
                self.logger.error(f"推流线程错误: {e}")
                time.sleep(0.1)

        self.logger.info("推流线程已停止")

    def _push_frame_to_stream(self, frame: np.ndarray):
        """推送帧到流媒体服务器 - 非阻塞方式"""
        if not self.stream_enabled or self.ffmpeg_process is None:
            return

        try:
            # 非阻塞方式放入队列
            try:
                self.stream_frame_queue.put_nowait(frame)
            except queue.Full:
                # 队列满时丢弃最旧的帧
                try:
                    self.stream_frame_queue.get_nowait()
                    self.stream_frame_queue.put_nowait(frame)
                except:
                    pass  # 丢帧，继续处理

        except Exception as e:
            self.logger.debug(f"推送帧到队列失败: {e}")

    def _close_ffmpeg_stream(self):
        """关闭FFmpeg推流 - 使用强制清理"""
        self.logger.info("关闭推流系统...")
        self._force_cleanup_ffmpeg()
        self.logger.info("推流系统已关闭")

    def _init_compreface(self):
        """初始化CompreFace SDK"""
        cf_config = self.config['compreface']

        # 创建CompreFace实例
        compre_face = CompreFace(
            cf_config['host'],
            cf_config['port'],
            {
                "limit": 0,
                "det_prob_threshold": 0.8,
                "prediction_count": 1
            }
        )

        # 初始化识别和检测服务
        self.recognition_service: RecognitionService = compre_face.init_face_recognition(
            cf_config['recognition_api_key']
        )

        self.detection_service: DetectionService = compre_face.init_face_detection(
            cf_config['detection_api_key']
        )

        self.logger.info("CompreFace服务初始化完成")

    def _init_camera(self):
        """初始化摄像头"""
        cam_config = self.config['camera']
        retry_interval = cam_config.get('retry_interval', 3)

        attempt = 0
        while True:
            attempt += 1
            self.logger.info(f"正在尝试打开摄像头 (第{attempt}次尝试)...")

            # 确保先释放之前的摄像头资源
            if self.camera is not None:
                try:
                    self.camera.release()
                except:
                    pass
                self.camera = None

            self.camera = cv2.VideoCapture(cam_config['device_id'])

            if self.camera.isOpened():
                # 设置摄像头参数
                self.camera.set(cv2.CAP_PROP_FRAME_WIDTH, cam_config['width'])
                self.camera.set(cv2.CAP_PROP_FRAME_HEIGHT, cam_config['height'])
                self.camera.set(cv2.CAP_PROP_FPS, cam_config['fps'])

                # 验证是否能读取帧
                ret, frame = self.camera.read()
                if ret:
                    self.logger.info(f"摄像头初始化成功 (设备ID: {cam_config['device_id']})")
                    # 重置失败计数
                    self.camera_failure_count = 0
                    self.camera_last_retry_time = None
                    return True
                else:
                    self.logger.warning("摄像头已打开但无法读取帧")
                    self.camera.release()
                    self.camera = None
            else:
                self.logger.warning(f"无法打开摄像头设备 {cam_config['device_id']}")

            # 等待后重试
            self.logger.info(f"{retry_interval}秒后重试...")
            time.sleep(retry_interval)

    def assess_frame_quality(self, frame: np.ndarray) -> float:
        """评估帧质量(使用Laplacian方差检测模糊度)"""
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        laplacian_var = cv2.Laplacian(gray, cv2.CV_64F).var()
        return laplacian_var

    def detect_faces(self, frame: np.ndarray) -> Optional[Dict[str, Any]]:
        """检测人脸"""
        try:
            import requests
            
            # 将帧编码为JPEG
            _, img_encoded = cv2.imencode('.jpg', frame)
            
            # 构建CompreFace REST API URL
            api_url = f"{self.config['compreface']['host']}:{self.config['compreface']['port']}/api/v1/detection/detect"
            
            # 准备请求参数
            params = {
                'face_plugins': 'pose'  # 启用pose插件
            }
            
            # 准备请求头
            headers = {
                'x-api-key': self.config['compreface']['detection_api_key']
            }
            
            # 发送请求
            files = {
                'file': ('image.jpg', img_encoded.tobytes(), 'image/jpeg')
            }
            
            response = requests.post(api_url, headers=headers, files=files, params=params)
            
            if response.status_code == 200:
                result = response.json()
                
                if result and 'result' in result and len(result['result']) > 0:
                    faces = result['result']

                    # 过滤掉太小的人脸
                    min_size = self.config['face_detection']['min_face_size']
                    valid_faces = []
                    
                    # 获取角度阈值
                    max_yaw = self.config['face_detection'].get('max_yaw', 30.0)
                    max_pitch = self.config['face_detection'].get('max_pitch', 30.0)

                    for face in faces:
                        # 检查人脸尺寸
                        face_width = face['box']['x_max'] - face['box']['x_min']
                        face_height = face['box']['y_max'] - face['box']['y_min']
                        
                        if face_width >= min_size and face_height >= min_size:
                            # 检查人脸角度
                            pose = face.get('pose', {})
                            yaw = pose.get('yaw', 0.0)
                            pitch = pose.get('pitch', 0.0)
                            
                            # 记录pose数据
                            self.logger.debug(f"检测到人脸数据: {list(face.keys())}")
                            if 'pose' in face:
                                self.logger.debug(f"Pose数据: yaw={yaw:.1f}°, pitch={pitch:.1f}°, roll={pose.get('roll', 0.0):.1f}°")
                            
                            # 判断是否为正脸
                            if abs(yaw) <= max_yaw and abs(pitch) <= max_pitch:
                                valid_faces.append(face)
                                self.logger.debug(f"人脸角度检查通过: yaw={yaw:.1f}°, pitch={pitch:.1f}°")
                            else:
                                self.logger.debug(f"人脸角度超出范围，跳过: yaw={yaw:.1f}°(阈值±{max_yaw}°), pitch={pitch:.1f}°(阈值±{max_pitch}°)")

                    if valid_faces:
                        # 返回第一个(最大的)人脸
                        return valid_faces[0]

            return None

        except Exception as e:
            self.logger.error(f"人脸检测错误: {e}")
            return None

    def recognize_face(self, frame: np.ndarray) -> Optional[Dict[str, Any]]:
        """识别人脸"""
        try:
            # 将帧编码为JPEG
            _, img_encoded = cv2.imencode('.jpg', frame)

            # 调用CompreFace识别API
            result = self.recognition_service.recognize(img_encoded.tobytes())

            if result and 'result' in result and len(result['result']) > 0:
                faces = result['result']
                if len(faces[0]['subjects']) > 0:
                    # 返回第一个识别结果
                    subject = faces[0]['subjects'][0]
                    return {
                        'subject': subject['subject'],
                        'similarity': subject['similarity'],
                        'box': faces[0]['box']
                    }

            return None

        except Exception as e:
            self.logger.error(f"人脸识别错误: {e}")
            return None

    def determine_role(self, person_id: str, similarity: float) -> Tuple[str, str]:
        """根据相似度确定角色"""
        role_config = self.config['role_mapping']

        if similarity < role_config['stranger_threshold']:
            return "未知", "陌生人"
        else:
            t = person_id.split("_")
            name = t[0]
            role = "员工" if len(t) == 1 else "访客"
            return name, role

    def should_recognize(self, person_id: str) -> bool:
        """检查是否应该识别(防止重复识别)"""
        cooldown = self.config['face_recognition']['recognition_cooldown']

        if person_id not in self.recognition_history:
            return True

        last_time = self.recognition_history[person_id]
        elapsed = (datetime.now() - last_time).total_seconds()

        return elapsed >= cooldown

    def cleanup_expired_records(self):
        """清理过期的识别记录，防止内存无限增长"""
        current_time = time.time()
        if current_time - self.last_cleanup_time < self.cleanup_interval:
            return

        self.last_cleanup_time = current_time

        # 清理超过1小时的记录
        expiry_seconds = 3600
        expired_keys = []
        for person_id, last_time in self.recognition_history.items():
            if isinstance(last_time, datetime):
                elapsed = (datetime.now() - last_time).total_seconds()
                if elapsed > expiry_seconds:
                    expired_keys.append(person_id)

        if expired_keys:
            for key in expired_keys:
                del self.recognition_history[key]
            self.logger.debug(f"清理了 {len(expired_keys)} 条过期识别记录")

    def cv2_add_chinese_text(self, img: np.ndarray, text: str, position: Tuple[int, int],
                             font: ImageFont.FreeTypeFont, text_color: Tuple[int, int, int]) -> np.ndarray:
        """在OpenCV图像上添加中文文本"""
        # 转换为PIL图像
        img_pil = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
        draw = ImageDraw.Draw(img_pil)

        # 绘制文本
        draw.text(position, text, font=font, fill=text_color)

        # 转换回OpenCV格式
        img = cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
        return img

    def draw_info_on_frame(self, frame: np.ndarray) -> np.ndarray:
        """在帧上绘制检测和识别信息"""
        display_frame = frame.copy()
        h, w = display_frame.shape[:2]

        # 绘制人脸框和识别结果
        if self.display_info['face_detected'] and self.display_info['face_box']:
            box = self.display_info['face_box']
            x_min = int(box['x_min'])
            y_min = int(box['y_min'])
            x_max = int(box['x_max'])
            y_max = int(box['y_max'])

            # 根据识别状态选择颜色
            if self.display_info['person_name']:
                # 已识别 - 绿色
                color = (0, 255, 0)
                thickness = 3
            else:
                # 仅检测到 - 黄色
                color = (0, 255, 255)
                thickness = 2

            # 绘制人脸框
            cv2.rectangle(display_frame, (x_min, y_min), (x_max, y_max), color, thickness)

            # 绘制识别信息
            if self.display_info['person_name']:
                name = self.display_info['person_name']
                role = self.display_info['person_role']
                similarity = self.display_info['similarity']

                # 准备文本
                text_lines = [
                    f"姓名: {name}",
                    f"角色: {role}",
                    f"相似度: {similarity:.2%}"
                ]

                # 计算文本背景框
                line_height = 35
                padding = 10

                # 计算所需的背景高度
                bg_height = len(text_lines) * line_height + padding * 2
                bg_y_start = max(0, y_min - bg_height - 10)

                # 绘制文本背景
                cv2.rectangle(
                    display_frame,
                    (x_min, bg_y_start),
                    (x_max, bg_y_start + bg_height),
                    (0, 0, 0),
                    -1
                )
                cv2.rectangle(
                    display_frame,
                    (x_min, bg_y_start),
                    (x_max, bg_y_start + bg_height),
                    color,
                    2
                )

                # 使用PIL绘制中文文本
                for i, text in enumerate(text_lines):
                    y_pos = bg_y_start + padding + i * line_height
                    display_frame = self.cv2_add_chinese_text(
                        display_frame,
                        text,
                        (x_min + padding, y_pos),
                        self.font_medium,
                        (255, 255, 255)
                    )

        # 绘制状态信息面板（左下角小窗口）
        panel_width = 200
        panel_height = 150
        panel_bg = np.zeros((panel_height, panel_width, 3), dtype=np.uint8)
        panel_bg[:] = (40, 40, 40)

        # 状态信息
        y_offset = 15
        x_offset = 10
        line_spacing = 22

        # WebSocket连接状态
        ws_status = "已连接" if self.ws_connected else "未连接"

        status_texts = [
            f"质量: {self.display_info['quality']:.1f}",
            f"WebSocket: {ws_status}",
            f"机器人说话: {'是' if self.robot_status['is_speaking'] else '否'}",
            f"机器人思考: {'是' if self.robot_status['is_thinking'] else '否'}",
            f"角色: {self.robot_status['role_name']}"
        ]

        # 如果人脸持续出现,显示倒计时
        if self.face_present_start:
            elapsed = (datetime.now() - self.face_present_start).total_seconds()
            face_duration = self.config['face_detection']['face_present_duration']
            remaining = max(0, face_duration - elapsed)
            status_texts.append(f"识别倒计时: {remaining:.1f}秒")

        # 使用PIL绘制中文状态文本（全部使用浅灰色）
        light_gray = (200, 200, 200)
        for i, text in enumerate(status_texts):
            panel_bg = self.cv2_add_chinese_text(
                panel_bg,
                text,
                (x_offset, y_offset + i * line_spacing),
                self.font_small,
                light_gray
            )

        # 将小面板叠加到画面左下角
        h, w = display_frame.shape[:2]
        y_start = h - panel_height
        display_frame[y_start:h, 0:panel_width] = panel_bg

        return display_frame

    def update_fps(self):
        """更新FPS计算"""
        self.fps_counter += 1
        current_time = time.time()
        elapsed = current_time - self.last_fps_time

        if elapsed >= 1.0:
            self.display_info['fps'] = self.fps_counter / elapsed
            self.fps_counter = 0
            self.last_fps_time = current_time

    async def send_websocket_message(self, message: Dict[str, Any]):
        """发送WebSocket消息"""
        if self.ws and self.ws_connected:
            try:
                await self.ws.send(json.dumps(message))
                self.logger.debug(f"发送消息: {message}")
            except websockets.exceptions.ConnectionClosed:
                self.logger.warning("WebSocket连接已关闭,无法发送消息")
                self.ws_connected = False
            except Exception as e:
                self.logger.error(f"发送WebSocket消息失败: {e}")
                self.ws_connected = False
        else:
            self.logger.debug("WebSocket未连接,消息发送失败")

    async def query_robot_status(self):
        """定期查询机器人状态"""
        interval = self.config['websocket']['status_interval']

        try:
            while self.ws_connected:
                try:
                    status_msg = {
                        "type": "get_status",
                        "message": ""
                    }
                    await self.send_websocket_message(status_msg)
                    await asyncio.sleep(interval)

                except Exception as e:
                    self.logger.error(f"查询状态错误: {e}")
                    self.ws_connected = False
                    break
        except Exception as e:
            self.logger.error(f"状态查询任务异常: {e}")
            self.ws_connected = False

    async def handle_websocket_messages(self):
        """处理WebSocket接收的消息"""
        # 从配置读取WebSocket循环延迟
        ws_loop_delay = self.config.get('performance', {}).get('websocket_loop_delay', 0.01)

        try:
            while self.ws_connected:
                try:
                    message = await self.ws.recv()
                    data = json.loads(message)

                    if data.get('type') == 'status':
                        status = data.get('message', {})
                        self.robot_status['is_speaking'] = status.get('is_speaking', False)
                        self.robot_status['is_thinking'] = status.get('is_thinking', False)
                        self.robot_status['listening'] = status.get('listening', False)
                        self.robot_status['is_asr_processing'] = status.get('is_asr_processing', False)
                        self.robot_status['role_name'] = status.get('role_name', '访客引导者')
                        self.robot_status['mode'] = status.get('mode', '')
                        self.robot_status['face_detect_inited'] = status.get('face_detect_inited', False)
                        self.robot_status['battery'] = status.get('battery', 0)
                        self.robot_status['bms_electric'] = status.get('bms_electric', 0)
                        self.robot_status['bms_voltage'] = status.get('bms_voltage', 0)
                        self.robot_status['run_time'] = status.get('run_time', 0)
                        self.robot_status['style_name'] = status.get('style_name', '')
                        self.robot_status['is_actioning'] = status.get('is_actioning', False)
                        self.robot_status['volume'] = status.get('volume', 0)
                        self.robot_status['voice_id'] = status.get('voice_id', '')

                        self.logger.debug(f"机器人状态: {self.robot_status}")

                except websockets.exceptions.ConnectionClosed:
                    self.logger.warning("WebSocket消息接收中断: 连接已关闭")
                    self.ws_connected = False
                    break
                except Exception as e:
                    self.logger.error(f"处理WebSocket消息错误: {e}")
                    self.ws_connected = False
                    break

                await asyncio.sleep(ws_loop_delay)  # 短暂延迟，避免 CPU 空转
        except Exception as e:
            self.logger.error(f"WebSocket消息处理任务异常: {e}")
            self.ws_connected = False

    async def connect_websocket(self):
        """连接WebSocket - 无限重连"""
        reconnect_delay = self.config['websocket']['reconnect_delay']

        while True:
            try:
                self.ws_reconnect_count += 1

                # 控制日志输出频率
                if self.ws_reconnect_count <= 3:
                    self.logger.info(f"连接WebSocket: {self.ws_url} (第{self.ws_reconnect_count}次尝试)")
                elif self.ws_reconnect_count % 10 == 0:
                    self.logger.info(f"持续尝试连接WebSocket (第{self.ws_reconnect_count}次)")
                else:
                    self.logger.debug(f"尝试连接WebSocket (第{self.ws_reconnect_count}次)")

                # 设置连接超时
                async with websockets.connect(
                        self.ws_url,
                        ping_interval=20,  # 每20秒发送ping
                        ping_timeout=10,  # ping超时10秒
                        close_timeout=5  # 关闭超时5秒
                ) as ws:
                    self.ws = ws
                    self.ws_connected = True
                    self.ws_reconnect_count = 0  # 连接成功,重置计数
                    self.logger.info("✓ WebSocket连接成功")

                    # 同时运行状态查询和消息接收
                    try:
                        await asyncio.gather(
                            self.query_robot_status(),
                            self.handle_websocket_messages()
                        )
                    except Exception as e:
                        self.logger.warning(f"WebSocket任务组异常: {e}")
                    finally:
                        # 确保连接状态被重置
                        self.ws_connected = False
                        self.ws = None
                        self.logger.info("WebSocket连接已断开,准备重连...")

            except websockets.exceptions.ConnectionClosed:
                self.logger.warning("WebSocket连接已正常关闭")
                self.ws_connected = False
                self.ws = None
            except ConnectionRefusedError:
                if self.ws_reconnect_count <= 3:
                    self.logger.error(f"WebSocket连接被拒绝: {self.ws_url}")
                elif self.ws_reconnect_count == 4:
                    self.logger.warning("WebSocket持续连接失败,将减少日志输出频率")
                self.ws_connected = False
                self.ws = None
            except OSError as e:
                if self.ws_reconnect_count <= 3:
                    self.logger.error(f"WebSocket网络错误: {e}")
                self.ws_connected = False
                self.ws = None
            except Exception as e:
                error_msg = str(e)
                # 过滤常见的连接错误
                if "no close frame" not in error_msg and "Connection closed" not in error_msg:
                    if self.ws_reconnect_count <= 3:
                        self.logger.error(f"WebSocket连接错误: {e}")
                    elif self.ws_reconnect_count == 4:
                        self.logger.warning("WebSocket持续连接失败,将减少日志输出频率")
                self.ws_connected = False
                self.ws = None

            # 等待后重连
            if self.ws_reconnect_count <= 3:
                self.logger.info(f"{reconnect_delay}秒后重连...")
            elif self.ws_reconnect_count % 10 == 0:
                self.logger.info(f"{reconnect_delay}秒后继续尝试重连...")
            await asyncio.sleep(reconnect_delay)

    def can_perform_detection(self) -> bool:
        """检查是否可以进行人脸检测"""
        return not self.robot_status['is_speaking'] and not self.robot_status['is_thinking'] and self.robot_status[
            'role_name'] == "访客引导者"

    async def process_video_stream(self):
        """处理视频流"""
        self._init_camera()

        # 初始化FFmpeg推流
        if self.stream_enabled:
            self._init_ffmpeg_stream()

        frame_interval = self.config['face_detection']['frame_interval']
        quality_threshold = self.config['face_detection']['quality_threshold']
        face_duration = self.config['face_detection']['face_present_duration']

        # 从配置读取性能控制参数
        perf_config = self.config.get('performance', {})
        target_fps = perf_config.get('target_fps', 30)
        cleanup_interval = perf_config.get('cleanup_interval', 60)
        target_frame_interval_ms = 1000 // target_fps  # 每帧间隔毫秒

        self.logger.info(f"目标帧率: {target_fps} FPS")

        self.logger.info("开始处理视频流")

        # 创建显示窗口并设置为全屏
        window_name = 'Hello'
        cv2.namedWindow(window_name, cv2.WINDOW_NORMAL)

        # 设置窗口为全屏模式
        cv2.setWindowProperty(window_name, cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)
        self.logger.info("人脸识别窗口设置为全屏模式")

        # 添加推流健康检查计数器
        stream_check_counter = 0
        stream_check_interval = 150  # 每150帧（约5秒）检查一次

        # 帧率控制初始化
        last_frame_time = time.time() * 1000

        # 清理定时控制
        last_cleanup_time = time.time()

        # 添加人脸消失计数器
        no_face_counter = 0
        no_face_threshold = self.config['face_detection'].get('no_face_threshold', 30)  # 连续N帧没有检测到人脸才清空

        try:
            while True:
                current_time = time.time()

                # 每cleanup_interval秒清理一次过期记录，防止内存无限增长
                if current_time - last_cleanup_time >= cleanup_interval:
                    self.cleanup_expired_records()
                    last_cleanup_time = current_time

                # 帧率控制：控制到约target_fps
                elapsed = time.time() * 1000 - last_frame_time
                if elapsed < target_frame_interval_ms:
                    await asyncio.sleep((target_frame_interval_ms - elapsed) / 1000)
                last_frame_time = time.time() * 1000

                # 检查机器人角色并显示/隐藏视频
                self._check_role_and_display_video()

                # 如果视频正在播放,更新视频帧
                if self.video_showing:
                    self._update_video()

                # 检查摄像头是否可用
                if self.camera is None or not self.camera.isOpened():
                    self.logger.warning("摄像头未打开,尝试重新连接...")
                    if not self._try_reconnect_camera():
                        await asyncio.sleep(0.1)
                        continue

                ret, frame = self.camera.read()

                if not ret:
                    self.camera_failure_count += 1

                    if self.camera_failure_count <= 3:
                        self.logger.warning(f"无法读取摄像头帧 (连续失败{self.camera_failure_count}次)")
                    elif self.camera_failure_count == 4:
                        self.logger.warning("摄像头持续读取失败,将减少日志输出")

                    if self.camera_failure_count >= self.camera_max_failures:
                        self.logger.error(f"摄像头连续失败{self.camera_failure_count}次,尝试重新初始化...")
                        if self._try_reconnect_camera():
                            self.logger.info("摄像头重新初始化成功,继续处理")
                            continue
                        else:
                            self.camera_failure_count = 0

                    await asyncio.sleep(0.1)
                    continue

                # 成功读取帧,重置失败计数
                if self.camera_failure_count > 0:
                    self.logger.info(f"摄像头恢复正常 (之前连续失败{self.camera_failure_count}次)")
                    self.camera_failure_count = 0

                self.frame_count += 1

                # 默认清空检测和识别结果
                should_detect = False

                # 检查是否到达检测间隔
                if self.frame_count % frame_interval == 0:
                    if self.can_perform_detection():
                        quality = self.assess_frame_quality(frame)
                        self.display_info['quality'] = quality

                        if quality >= quality_threshold:
                            should_detect = True
                        else:
                            self.logger.debug(f"帧质量不足: {quality:.2f}")
                    else:
                        self.logger.debug("机器人正在说话或思考,跳过检测")
                        self.face_present_start = None
                        self.display_info['quality'] = self.assess_frame_quality(frame)
                else:
                    self.display_info['quality'] = 0

                # 显示二维码时不进行人脸识别。
                if self.qrcode_showing:
                    should_detect = False

                # 执行人脸检测
                if should_detect:
                    face = self.detect_faces(frame)

                    if face:
                        # 检测到人脸，重置消失计数器
                        no_face_counter = 0

                        self.display_info['face_detected'] = True
                        self.display_info['face_box'] = face['box']
                        self.display_info['face_box_mirrored'] = False  # 重置翻转标志

                        # 记录人脸出现时间
                        if self.face_present_start is None:
                            self.face_present_start = datetime.now()
                            self.logger.info("检测到人脸,开始计时")
                            # 清空之前的识别结果
                            self.display_info['person_name'] = None
                            self.display_info['person_role'] = None
                            self.display_info['similarity'] = 0

                        # 检查是否满足持续出现时长
                        elapsed = (datetime.now() - self.face_present_start).total_seconds()

                        if elapsed >= face_duration:
                            self.logger.info(f"人脸持续出现{elapsed:.2f}秒,开始识别")

                            # 进行人脸识别
                            recognition_result = self.recognize_face(frame)

                            if recognition_result:
                                person_id = recognition_result['subject']
                                similarity = recognition_result['similarity']

                                # 更新显示信息
                                self.display_info['face_box'] = recognition_result['box']

                                # 检查是否应该识别(防止重复)
                                if self.should_recognize(person_id):
                                    name, role = self.determine_role(person_id, similarity)

                                    # 更新显示信息
                                    self.display_info['person_name'] = name
                                    self.display_info['person_role'] = role
                                    self.display_info['similarity'] = similarity

                                    self.logger.info(
                                        f"识别到: {name}, 相似度: {similarity:.6f}, 角色: {role}"
                                    )

                                    # 如果是陌生人,显示二维码
                                    if role == "陌生人":
                                        self._show_qrcode()

                                    # 发送识别结果
                                    reception_msg = {
                                        "type": "start_reception",
                                        "message": {
                                            "name": name,
                                            "role": role
                                        }
                                    }
                                    await self.send_websocket_message(reception_msg)

                                    # 记录识别时间
                                    self.recognition_history[person_id] = datetime.now()

                                # 重置计时器
                                self.face_present_start = None
                            else:
                                # 未识别到已知人脸
                                if self.should_recognize("unknown"):
                                    self.logger.info("检测到陌生人")

                                    # 更新显示信息
                                    self.display_info['person_name'] = "未知访客"
                                    self.display_info['person_role'] = "陌生人"
                                    self.display_info['similarity'] = 0

                                    # 显示二维码
                                    self._show_qrcode()

                                    reception_msg = {
                                        "type": "start_reception",
                                        "message": {
                                            "name": "未知访客",
                                            "role": "陌生人"
                                        }
                                    }
                                    await self.send_websocket_message(reception_msg)

                                    self.recognition_history["unknown"] = datetime.now()

                                # 重置计时器
                                self.face_present_start = None
                else:
                    # 没有检测到人脸
                    no_face_counter += 1

                    # 连续一段时间没检测到人脸，清空显示信息
                    if no_face_counter >= no_face_threshold:
                        if self.display_info['face_detected'] or self.display_info['person_name']:
                            self.logger.info("人脸消失，清空识别信息")
                        self.display_info['face_detected'] = False
                        self.display_info['face_box'] = None
                        self.display_info['person_name'] = None
                        self.display_info['person_role'] = None
                        self.display_info['similarity'] = 0

                        # 重置计时器
                        if self.face_present_start is not None:
                            self.logger.debug("人脸消失,重置计时器")
                        self.face_present_start = None

                # 镜像翻转：使画面与实际场景一致（在绘制信息之前）
                frame = cv2.flip(frame, 1)

                # 调整 face_box 坐标以适应镜像翻转（只翻转一次）
                if self.display_info['face_detected'] and self.display_info['face_box'] and not self.display_info['face_box_mirrored']:
                    h_flip, w_flip = frame.shape[:2]
                    box = self.display_info['face_box']
                    x_min = int(box['x_min'])
                    x_max = int(box['x_max'])
                    # 水平翻转坐标
                    self.display_info['face_box']['x_min'] = w_flip - 1 - x_max
                    self.display_info['face_box']['x_max'] = w_flip - 1 - x_min
                    self.display_info['face_box_mirrored'] = True

                # 绘制信息
                display_frame = self.draw_info_on_frame(frame)

                # 绘制人脸轮廓引导框（屏幕中央）
                h, w = display_frame.shape[:2]
                center_x = w // 2
                center_y = h // 2
                # 椭圆参数：中心点、宽半轴、高半轴、旋转角度、起始角度、结束角度
                cv2.ellipse(display_frame, (center_x, center_y), (150, 200), 0, 0, 360, (0, 200, 0), 3)

                # 添加提示文字（使用中文支持函数）
                guide_text = "请将脸部对准圆圈内"
                # 计算文字位置：距离底部40像素，居中显示
                # 使用PIL计算中文文字的实际宽度
                img_temp = Image.new('RGB', (1, 1))
                draw_temp = ImageDraw.Draw(img_temp)
                bbox = draw_temp.textbbox((0, 0), guide_text, font=self.font_medium)
                text_width = bbox[2] - bbox[0]
                text_x = center_x - text_width // 2
                text_y = h - 40
                display_frame = self.cv2_add_chinese_text(display_frame, guide_text, (text_x, text_y), self.font_medium, (0, 200, 0))

                # 使用缓存的屏幕尺寸拉伸视频帧到全屏
                display_frame = cv2.resize(display_frame, (self.screen_width, self.screen_height), interpolation=cv2.INTER_LINEAR)

                cv2.imshow(window_name, display_frame)

                # 检查二维码显示是否超时
                self._check_qrcode_timeout()

                # 推送到流媒体服务器
                if self.stream_enabled:
                    stream_check_counter += 1

                    # 定期健康检查
                    if stream_check_counter >= stream_check_interval:
                        stream_check_counter = 0

                        # 检查FFmpeg进程状态
                        ffmpeg_alive = self.ffmpeg_process is not None and self.ffmpeg_process.poll() is None
                        thread_alive = self.stream_thread is not None and self.stream_thread.is_alive()

                        # 如果FFmpeg进程死了或线程停了，尝试重启
                        if not ffmpeg_alive or not thread_alive:
                            current_time = time.time()

                            # 检查是否可以重试
                            can_retry = (
                                    self.stream_retry_count < self.stream_max_retries and
                                    (self.stream_last_retry_time is None or
                                     current_time - self.stream_last_retry_time >= self.stream_retry_cooldown)
                            )

                            if can_retry:
                                self.logger.warning(
                                    f"检测到推流异常 (FFmpeg存活={ffmpeg_alive}, 线程存活={thread_alive})，尝试重启...")

                                # 先清理旧的
                                if self.ffmpeg_process:
                                    try:
                                        if self.ffmpeg_process.stdin and not self.ffmpeg_process.stdin.closed:
                                            self.ffmpeg_process.stdin.close()
                                        self.ffmpeg_process.kill()
                                        self.ffmpeg_process.wait(timeout=2)
                                    except:
                                        pass
                                    self.ffmpeg_process = None

                                # 停止旧线程
                                if self.stream_thread and self.stream_thread.is_alive():
                                    self.stream_thread_running = False
                                    try:
                                        self.stream_frame_queue.put(None, timeout=1)
                                    except:
                                        pass
                                    self.stream_thread.join(timeout=2)

                                # 清空队列
                                while not self.stream_frame_queue.empty():
                                    try:
                                        self.stream_frame_queue.get_nowait()
                                    except:
                                        break

                                # 重新初始化
                                success = self._init_ffmpeg_stream()
                                if success:
                                    self.logger.info("✓ 推流重启成功")
                                else:
                                    self.logger.error("✗ 推流重启失败")
                            elif self.stream_retry_count >= self.stream_max_retries:
                                if stream_check_counter % 600 == 0:  # 每600帧（约20秒）提示一次
                                    self.logger.warning(f"推流已达到最大重试次数({self.stream_max_retries})，已停止尝试")

                    # 推送帧（非阻塞）
                    if self.ffmpeg_process and self.ffmpeg_process.poll() is None:
                        self._push_frame_to_stream(display_frame)

                # 检查键盘输入
                key = cv2.waitKey(1) & 0xFF
                if key == ord('q') or key == 27:  # 'q' 或 ESC 退出
                    self.logger.info("用户请求退出")
                    break

                await asyncio.sleep(0.001)

        except Exception as e:
            self.logger.error(f"视频流处理错误: {e}")
        finally:
            # 清理资源
            if self.camera:
                self.camera.release()
                self.logger.info("摄像头已释放")

            # 关闭二维码窗口
            self._close_qrcode()

            # 关闭视频窗口
            self._close_video()

            # 关闭FFmpeg推流
            if self.stream_enabled:
                self._close_ffmpeg_stream()

            cv2.destroyAllWindows()

    async def run(self):
        """运行系统"""
        self.logger.info("启动人脸识别系统")

        # 同时运行WebSocket连接和视频处理
        await asyncio.gather(
            self.connect_websocket(),
            self.process_video_stream()
        )


def main():
    """主函数"""
    system = FaceRecognitionSystem("config.yaml")

    try:
        asyncio.run(system.run())
    except KeyboardInterrupt:
        print("\n系统已停止")
    except Exception as e:
        print(f"系统错误: {e}")


if __name__ == "__main__":
    main()