diff --git a/src/chat/chat_loop/wakeup_manager.py b/src/chat/chat_loop/wakeup_manager.py index fa755e882..46af60249 100644 --- a/src/chat/chat_loop/wakeup_manager.py +++ b/src/chat/chat_loop/wakeup_manager.py @@ -26,6 +26,8 @@ class WakeUpManager: self.angry_start_time = 0.0 # 愤怒状态开始时间 self.last_decay_time = time.time() # 上次衰减时间 self._decay_task: Optional[asyncio.Task] = None + self.last_log_time = 0 + self.log_interval = 30 # 从配置文件获取参数 wakeup_config = global_config.wakeup_system @@ -123,7 +125,12 @@ class WakeUpManager: # 群聊未被艾特,不增加唤醒度 return False - logger.info(f"{self.context.log_prefix} 唤醒度变化: {old_value:.1f} -> {self.wakeup_value:.1f} (阈值: {self.wakeup_threshold})") + current_time = time.time() + if current_time - self.last_log_time > self.log_interval: + logger.info(f"{self.context.log_prefix} 唤醒度变化: {old_value:.1f} -> {self.wakeup_value:.1f} (阈值: {self.wakeup_threshold})") + self.last_log_time = current_time + else: + logger.debug(f"{self.context.log_prefix} 唤醒度变化: {old_value:.1f} -> {self.wakeup_value:.1f} (阈值: {self.wakeup_threshold})") # 检查是否达到唤醒阈值 if self.wakeup_value >= self.wakeup_threshold: diff --git a/src/chat/utils/utils_video.py b/src/chat/utils/utils_video.py index f68118580..d5333ee86 100644 --- a/src/chat/utils/utils_video.py +++ b/src/chat/utils/utils_video.py @@ -61,6 +61,8 @@ class VideoAnalyzer: self.max_image_size = config.max_image_size self.enable_frame_timing = config.enable_frame_timing self.batch_analysis_prompt = config.batch_analysis_prompt + self.frame_extraction_mode = config.frame_extraction_mode + self.frame_interval_seconds = config.frame_interval_seconds # 将配置文件中的模式映射到内部使用的模式名称 config_mode = config.analysis_mode @@ -92,6 +94,8 @@ class VideoAnalyzer: self.batch_size = 3 # 批处理时每批处理的帧数 self.timeout = 60.0 # 分析超时时间(秒) self.enable_frame_timing = True + self.frame_extraction_mode = "fixed_number" + self.frame_interval_seconds = 2.0 self.batch_analysis_prompt = """请分析这个视频的内容。这些图片是从视频中按时间顺序提取的关键帧。 请提供详细的分析,包括: @@ -191,24 +195,59 @@ class VideoAnalyzer: logger.info(f"视频信息: {total_frames}帧, {fps:.2f}FPS, {duration:.2f}秒") - # 动态计算帧间隔 - if duration > 0: - frame_interval = max(1, int(duration / self.max_frames * fps)) - else: - frame_interval = 30 # 默认间隔 - frame_count = 0 extracted_count = 0 - while cap.isOpened() and extracted_count < self.max_frames: - ret, frame = cap.read() - if not ret: - break + if self.frame_extraction_mode == "time_interval": + # 新模式:按时间间隔抽帧 + time_interval = self.frame_interval_seconds + next_frame_time = 0.0 + + while cap.isOpened(): + ret, frame = cap.read() + if not ret: + break - if frame_count % frame_interval == 0: - # 转换为PIL图像并压缩 - frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - pil_image = Image.fromarray(frame_rgb) + current_time = cap.get(cv2.CAP_PROP_POS_MSEC) / 1000.0 + + if current_time >= next_frame_time: + # 转换为PIL图像并压缩 + frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + pil_image = Image.fromarray(frame_rgb) + + # 调整图像大小 + if max(pil_image.size) > self.max_image_size: + ratio = self.max_image_size / max(pil_image.size) + new_size = tuple(int(dim * ratio) for dim in pil_image.size) + pil_image = pil_image.resize(new_size, Image.Resampling.LANCZOS) + + # 转换为base64 + buffer = io.BytesIO() + pil_image.save(buffer, format='JPEG', quality=self.frame_quality) + frame_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8') + + frames.append((frame_base64, current_time)) + extracted_count += 1 + + logger.debug(f"提取第{extracted_count}帧 (时间: {current_time:.2f}s)") + + next_frame_time += time_interval + else: + # 旧模式:固定总帧数 + if duration > 0: + frame_interval = max(1, int(total_frames / self.max_frames)) + else: + frame_interval = 1 # 如果无法获取时长,则逐帧提取直到达到max_frames + + while cap.isOpened() and extracted_count < self.max_frames: + ret, frame = cap.read() + if not ret: + break + + if frame_count % frame_interval == 0: + # 转换为PIL图像并压缩 + frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + pil_image = Image.fromarray(frame_rgb) # 调整图像大小 if max(pil_image.size) > self.max_image_size: @@ -227,8 +266,8 @@ class VideoAnalyzer: extracted_count += 1 logger.debug(f"提取第{extracted_count}帧 (时间: {timestamp:.2f}s)") - - frame_count += 1 + + frame_count += 1 cap.release() logger.info(f"✅ 成功提取{len(frames)}帧") diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 1ee7cd305..5f422dfc6 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -619,6 +619,8 @@ class VideoAnalysisConfig(ValidatedConfigBase): enable: bool = Field(default=True, description="启用") analysis_mode: str = Field(default="batch_frames", description="分析模式") + frame_extraction_mode: str = Field(default="fixed_number", description="抽帧模式") + frame_interval_seconds: float = Field(default=2.0, description="抽帧时间间隔") max_frames: int = Field(default=8, description="最大帧数") frame_quality: int = Field(default=85, description="帧质量") max_image_size: int = Field(default=800, description="最大图像大小") diff --git a/src/llm_models/utils_model.py b/src/llm_models/utils_model.py index 8432302c5..d4ed32fb8 100644 --- a/src/llm_models/utils_model.py +++ b/src/llm_models/utils_model.py @@ -340,16 +340,22 @@ class LLMRequest: is_truncated = True logger.warning("未检测到 [done] 标记,判定为截断") - if (is_empty_reply or is_truncated) and empty_retry_count < max_empty_retry: - empty_retry_count += 1 - reason = "空回复" if is_empty_reply else "截断" - logger.warning(f"检测到{reason},正在进行第 {empty_retry_count}/{max_empty_retry} 次重新生成") + if is_empty_reply or is_truncated: + if empty_retry_count < max_empty_retry: + empty_retry_count += 1 + reason = "空回复" if is_empty_reply else "截断" + logger.warning(f"检测到{reason},正在进行第 {empty_retry_count}/{max_empty_retry} 次重新生成") - if empty_retry_interval > 0: - await asyncio.sleep(empty_retry_interval) + if empty_retry_interval > 0: + await asyncio.sleep(empty_retry_interval) - model_info, api_provider, client = self._select_model() - continue + model_info, api_provider, client = self._select_model() + continue + else: + # 已达到最大重试次数,但仍然是空回复或截断 + reason = "空回复" if is_empty_reply else "截断" + # 抛出异常,由外层重试逻辑或最终的异常处理器捕获 + raise RuntimeError(f"经过 {max_empty_retry + 1} 次尝试后仍然是{reason}的回复") # 记录使用情况 if usage := response.usage: diff --git a/src/manager/schedule_manager.py b/src/manager/schedule_manager.py index 27d0d0e04..4a708aeab 100644 --- a/src/manager/schedule_manager.py +++ b/src/manager/schedule_manager.py @@ -418,7 +418,12 @@ class ScheduleManager: if is_in_time_range: # 检查是否被唤醒 if wakeup_manager and wakeup_manager.is_in_angry_state(): - logger.info(f"在休眠活动 '{activity}' 期间,但已被唤醒。") + current_timestamp = datetime.now().timestamp() + if current_timestamp - self.last_sleep_log_time > self.sleep_log_interval: + logger.info(f"在休眠活动 '{activity}' 期间,但已被唤醒。") + self.last_sleep_log_time = current_timestamp + else: + logger.debug(f"在休眠活动 '{activity}' 期间,但已被唤醒。") return False current_timestamp = datetime.now().timestamp() diff --git a/src/plugins/built_in/tts_plugin/plugin.py b/src/plugins/built_in/tts_plugin/plugin.py index 4e4d3648b..30748a9ff 100644 --- a/src/plugins/built_in/tts_plugin/plugin.py +++ b/src/plugins/built_in/tts_plugin/plugin.py @@ -34,6 +34,8 @@ class TTSAction(BaseAction): # 动作使用场景 action_require = [ "当需要发送语音信息时使用", + "当用户要求你说话时使用", + "当用户要求听你声音时使用", "当用户明确要求使用语音功能时使用", "当表达内容更适合用语音而不是文字传达时使用", "当用户想听到语音回答而非阅读文本时使用", diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index a55f6ace6..af0b15fe8 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -381,7 +381,9 @@ enable_friend_chat = false # 是否启用好友聊天 [video_analysis] # 视频分析配置 enable = true # 是否启用视频分析功能 analysis_mode = "batch_frames" # 分析模式:"frame_by_frame"(逐帧分析,非常慢 "建议frames大于8时不要使用这个" ...但是详细)、"batch_frames"(批量分析,快但可能略简单 -其实效果也差不多)或 "auto"(自动选择) -max_frames = 16 # 最大分析帧数 +frame_extraction_mode = "fixed_number" # 抽帧模式: "fixed_number" (固定总帧数) 或 "time_interval" (按时间间隔) +frame_interval_seconds = 2.0 # 按时间间隔抽帧的秒数(仅在 mode = "time_interval" 时生效) +max_frames = 16 # 最大分析帧数(仅在 mode = "fixed_number" 时生效) frame_quality = 80 # 帧图像JPEG质量 (1-100) max_image_size = 800 # 单帧最大图像尺寸(像素) enable_frame_timing = true # 是否在分析中包含帧的时间信息