From f9f1183e4b29244290a240b0492dc6fa08623c59 Mon Sep 17 00:00:00 2001 From: HycJack <772403255@qq.com> Date: Tue, 26 May 2026 01:50:23 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=91=E7=9C=A0=E6=97=B6=E6=AE=B5=E6=93=8D?= =?UTF-8?q?=E4=BD=9C=E5=B0=8F=E7=A8=8B=E5=BA=8F=E5=8A=A0=E6=8F=90=E7=A4=BA?= =?UTF-8?q?=EF=BC=8C=E5=8D=9A=E5=A3=AB=E8=A7=92=E8=89=B2=E8=AF=AD=E9=80=9F?= =?UTF-8?q?=E8=B0=83=E5=BF=AB=E4=B8=80=E7=82=B9=EF=BC=8C=20=E5=88=B7?= =?UTF-8?q?=E8=87=AA=E5=B7=B1=E5=8D=A1=E6=B2=A1=E6=9C=89=E7=95=99=E8=A8=80?= =?UTF-8?q?=E6=83=85=E5=86=B5=E4=B8=8B=E6=92=AD=E6=94=BE=E6=9C=80=E5=90=8E?= =?UTF-8?q?=E4=B8=80=E6=9D=A1=E7=95=99=E8=A8=80=EF=BC=8C=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E8=A7=92=E8=89=B2=E5=88=87=E6=8D=A2=E6=8F=90=E7=A4=BA=E9=9F=B3?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- talkingq-url/assets/audio/switch_role.mp3 | Bin 0 -> 6417 bytes talkingq-url/banban/middleware/auth.py | 77 ++++++++++++++++++- talkingq-url/banban/routers/roles.py | 12 ++- talkingq-url/handlers/mqtt_handler.py | 1 + talkingq-url/implementations/minimax_tts.py | 6 +- talkingq-url/services/offline_audio_cache.py | 7 +- 6 files changed, 93 insertions(+), 10 deletions(-) create mode 100644 talkingq-url/assets/audio/switch_role.mp3 diff --git a/talkingq-url/assets/audio/switch_role.mp3 b/talkingq-url/assets/audio/switch_role.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..46b8a68a77b20cb5bf4cee291b57501dbb40d375 GIT binary patch literal 6417 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 be-y_5`+@iTfRZy0x_%*q{_m&%ZyEm!S-=7a literal 0 HcmV?d00001 diff --git a/talkingq-url/banban/middleware/auth.py b/talkingq-url/banban/middleware/auth.py index 137c7cc..152115c 100644 --- a/talkingq-url/banban/middleware/auth.py +++ b/talkingq-url/banban/middleware/auth.py @@ -1,12 +1,12 @@ from collections.abc import Awaitable, Callable import logging - +from datetime import datetime, time, timedelta from fastapi import FastAPI, HTTPException, Request from sqlalchemy import text - +from fastapi.responses import JSONResponse from database.connection import get_db_manager from banban.security import auth_error_response, decode_access_token - +from banban.service.device import device_service NO_AUTH_PATH_PREFIXES = ( "/banban/auth/login", @@ -34,6 +34,28 @@ def _is_no_auth_path(path: str) -> bool: return True return False +def convert_to_time(value): + """将字符串或timedelta转换为time对象""" + if value is None: + return None + if isinstance(value, time): + return value + if isinstance(value, timedelta): + # timedelta 可能表示时间间隔,例如 22:00:00 表示为 22小时 + total_seconds = value.total_seconds() + hours = int(total_seconds // 3600) + minutes = int((total_seconds % 3600) // 60) + seconds = int(total_seconds % 60) + return time(hours, minutes, seconds) + if isinstance(value, str): + # 支持 "HH:MM" 或 "HH:MM:SS" + parts = value.split(':') + if len(parts) == 2: + return datetime.strptime(value, "%H:%M").time() + elif len(parts) == 3: + return datetime.strptime(value, "%H:%M:%S").time() + raise ValueError(f"Unsupported type for time conversion: {type(value)}") + def install_auth_middleware(app: FastAPI) -> None: @app.middleware("http") @@ -122,4 +144,53 @@ def install_auth_middleware(app: FastAPI) -> None: await session.close() request.state.user_id = user_id + async def is_device_sleep(device_id: str) -> bool: + db_manager = await get_db_manager() + session = await db_manager.get_session() + try: + row = await device_service.get_device_status( + device_id=device_id, + user_id=user_id, + ) + sleep_start_str=row.get("disable_time_start") + sleep_end_str=row.get("disable_time_end") + print(sleep_start_str, sleep_end_str) + now = datetime.now() + current_time = now.time() + print(current_time) + start_time = convert_to_time(sleep_start_str) + end_time = convert_to_time(sleep_end_str) + print(start_time, end_time) + # 如果任意一个转换后为 None,视为未配置睡眠时间 + if start_time is None or end_time is None: + return True + now = datetime.now() + current_time = now.time() + + if start_time <= end_time: + return not (start_time <= current_time <= end_time) + else: + return not (current_time >= start_time or current_time <= end_time) + + finally: + await session.close() + + path = request.url.path + # 仅拦截以 /banban/devices/ 开头的路径 + if path.startswith("/banban/devices/") and ('volume' in path or 'location' in path or 'firmware/update' in path) : + # 解析 device_id,假设路径格式为 /banban/devices/{device_id}/... 或 /banban/devices/{device_id} + parts = path.split("/") + # parts 示例: ['', 'banban', 'devices', 'device_id', 'action', ...] + if len(parts) >= 4: + device_id = parts[3] # 第四个片段即为 device_id + if device_id: # 确保 device_id 非空 + if not await is_device_sleep(device_id): + return JSONResponse( + status_code=400, # 或 403 / 409 + content={"detail": f"设备 {device_id} 不在线或已配置睡眠时间,请稍后再试"} + ) + else: + # 路径格式不符合预期,可以放行或返回错误(根据业务决定) + # 这里选择放行,让后续路由自行处理 + pass return await call_next(request) diff --git a/talkingq-url/banban/routers/roles.py b/talkingq-url/banban/routers/roles.py index af11547..14820b4 100644 --- a/talkingq-url/banban/routers/roles.py +++ b/talkingq-url/banban/routers/roles.py @@ -10,7 +10,8 @@ from handlers.prompt_sound_handler import send_welcome_sound from services.conversation_history import DeviceConversationHistory, conversation_history_manager from services.device_config import DeviceConfig, device_config_manager from services.role_manager import role_manager - +from handlers.mqtt_handler import TalkingQMQTTService +from config import settings router = APIRouter(prefix="/roles", tags=["banban-roles"]) @@ -121,8 +122,13 @@ async def update_device_role( role_key, ) - if payload.play_welcome: - await send_welcome_sound(device_id, role_key, language) + # if payload.play_welcome: + # await send_welcome_sound(device_id, role_key, language) + service = await TalkingQMQTTService.get_instance() + if service is None: + raise HTTPException(status_code=503, detail="MQTT 服务未初始化") + audio_url = f"http://{settings.server_host}:{settings.server_port}/assets/audio/switch_role.mp3" + msg_id = await service.send_nfc_notice(device_id, audio_url) return DeviceRoleResponse( device_id=device_id, diff --git a/talkingq-url/handlers/mqtt_handler.py b/talkingq-url/handlers/mqtt_handler.py index 3507718..1cf365c 100644 --- a/talkingq-url/handlers/mqtt_handler.py +++ b/talkingq-url/handlers/mqtt_handler.py @@ -454,6 +454,7 @@ class TalkingQMQTTService: params = {f"url_{k}": audio_url for k, audio_url in enumerate(audio_urls, start=1)} payload = {"msg_id": "005", "type": 0, "params": params} await self._publish(topic, payload) + await offline_audio_cache.add_audio_url(device_id, audio_urls[-1]) return payload = { diff --git a/talkingq-url/implementations/minimax_tts.py b/talkingq-url/implementations/minimax_tts.py index ffd325f..a7efdd2 100644 --- a/talkingq-url/implementations/minimax_tts.py +++ b/talkingq-url/implementations/minimax_tts.py @@ -112,14 +112,16 @@ class MiniMaxTTS(TTS, BaseService): "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json" } - + speed = 1.0 + if self.selected_role and "boshi" in self.selected_role: + speed = 2.0 payload = { "model": self.model, "text": text, "stream": True, "voice_setting": { "voice_id": self.voice_id, - "speed": 1.0, + "speed": speed, "vol": 2.0, "pitch": 0 }, diff --git a/talkingq-url/services/offline_audio_cache.py b/talkingq-url/services/offline_audio_cache.py index 30814d4..afc08d2 100644 --- a/talkingq-url/services/offline_audio_cache.py +++ b/talkingq-url/services/offline_audio_cache.py @@ -18,8 +18,11 @@ class OfflineAudioCache: return list(self.offline_audio.get(device_id, [])) async def pop_audio_urls(self, device_id: str) -> List[str]: - async with self.lock: - return self.offline_audio.pop(device_id, []) + urls = await self.get_audio_urls(device_id) + if urls and len(urls) > 1: + async with self.lock: + return self.offline_audio.pop(device_id, []) + return urls async def clear_audio_urls(self, device_id: str): async with self.lock: