diff --git a/talkingq-url/assets/audio/switch_role.mp3 b/talkingq-url/assets/audio/switch_role.mp3 new file mode 100644 index 0000000..46b8a68 Binary files /dev/null and b/talkingq-url/assets/audio/switch_role.mp3 differ 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: