上传后台修改
This commit is contained in:
BIN
talkingq-url/assets/audio/bind_nfc_finish_zh.mp3
Normal file
BIN
talkingq-url/assets/audio/bind_nfc_finish_zh.mp3
Normal file
Binary file not shown.
BIN
talkingq-url/assets/audio/bind_nfc_ready_zh.mp3
Normal file
BIN
talkingq-url/assets/audio/bind_nfc_ready_zh.mp3
Normal file
Binary file not shown.
BIN
talkingq-url/assets/roles/banban/bind_nfc_finish_zh.mp3
Normal file
BIN
talkingq-url/assets/roles/banban/bind_nfc_finish_zh.mp3
Normal file
Binary file not shown.
BIN
talkingq-url/assets/roles/banban/bind_nfc_ready_zh.mp3
Normal file
BIN
talkingq-url/assets/roles/banban/bind_nfc_ready_zh.mp3
Normal file
Binary file not shown.
BIN
talkingq-url/assets/roles/banban/device_sleep_mode_zh.mp3
Normal file
BIN
talkingq-url/assets/roles/banban/device_sleep_mode_zh.mp3
Normal file
Binary file not shown.
BIN
talkingq-url/assets/roles/banban/device_wake_up_zh.mp3
Normal file
BIN
talkingq-url/assets/roles/banban/device_wake_up_zh.mp3
Normal file
Binary file not shown.
BIN
talkingq-url/assets/roles/banban/have_rest_zh copy.mp3
Normal file
BIN
talkingq-url/assets/roles/banban/have_rest_zh copy.mp3
Normal file
Binary file not shown.
BIN
talkingq-url/assets/roles/banban/have_rest_zh.mp3
Normal file
BIN
talkingq-url/assets/roles/banban/have_rest_zh.mp3
Normal file
Binary file not shown.
BIN
talkingq-url/assets/roles/banban/low_power_off_zh.mp3
Normal file
BIN
talkingq-url/assets/roles/banban/low_power_off_zh.mp3
Normal file
Binary file not shown.
BIN
talkingq-url/assets/roles/banban/low_power_zh.mp3
Normal file
BIN
talkingq-url/assets/roles/banban/low_power_zh.mp3
Normal file
Binary file not shown.
BIN
talkingq-url/assets/roles/banban/network_connect_zh.mp3
Normal file
BIN
talkingq-url/assets/roles/banban/network_connect_zh.mp3
Normal file
Binary file not shown.
BIN
talkingq-url/assets/roles/banban/network_ok_zh.mp3
Normal file
BIN
talkingq-url/assets/roles/banban/network_ok_zh.mp3
Normal file
Binary file not shown.
BIN
talkingq-url/assets/roles/banban/tts_error.mp3
Normal file
BIN
talkingq-url/assets/roles/banban/tts_error.mp3
Normal file
Binary file not shown.
BIN
talkingq-url/assets/roles/banban/volume_down_zh.mp3
Normal file
BIN
talkingq-url/assets/roles/banban/volume_down_zh.mp3
Normal file
Binary file not shown.
BIN
talkingq-url/assets/roles/banban/volume_up_zh.mp3
Normal file
BIN
talkingq-url/assets/roles/banban/volume_up_zh.mp3
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -103,6 +103,5 @@ class ChildDAO(BaseDAO):
|
||||
AND pcr.status = 1
|
||||
""",
|
||||
{"child_id": child_id, "user_id": user_id},
|
||||
).scalar_one_or_none()
|
||||
is not None
|
||||
)
|
||||
)
|
||||
).scalar_one_or_none() is not None
|
||||
|
||||
@@ -321,7 +321,7 @@ class LocationDAO(BaseDAO):
|
||||
await self.execute(text(update_current_sql), params)
|
||||
await self.commit()
|
||||
|
||||
async def get_device_current_location(self, *, device_id: str) -> Mapping[str, Any]:
|
||||
async def get_device_current_location_by_device_id(self, *, device_id: str) -> Mapping[str, Any]:
|
||||
# sql 查询
|
||||
result = await self.execute(
|
||||
text(
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import logging
|
||||
from collections.abc import Mapping
|
||||
from datetime import datetime
|
||||
|
||||
from services.connection_manager import connection_manager
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import text
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
from handlers.mqtt_handler import TalkingQMQTTService
|
||||
try:
|
||||
from banban.security import get_current_user_id
|
||||
from banban.schemas.location import (
|
||||
@@ -148,22 +150,65 @@ async def get_current_device_location(
|
||||
request: Request,
|
||||
current_user_id: int = Depends(get_current_user_id),
|
||||
) -> DeviceLocationCurrentResponse:
|
||||
row = await location_service.get_device_current_location(
|
||||
device_id=device_id,
|
||||
user_id=current_user_id,
|
||||
)
|
||||
# 每次进入小程序地图页面获取最新位置坐标,等待设备上报GPS数据,再查询数据库坐标
|
||||
service = await TalkingQMQTTService.get_instance()
|
||||
if service is None:
|
||||
raise HTTPException(status_code=503, detail="MQTT 服务未初始化")
|
||||
await service.send_gps_query(device_id)
|
||||
try:
|
||||
# 每隔1s 获取一次GPS数据,最多3次
|
||||
# 先判断设备是否在线,不在线直接提示设备没有在线,通过websocket判断
|
||||
target_websocket = await connection_manager.get_connection(device_id)
|
||||
if not target_websocket or target_websocket.client_state.name != "CONNECTED":
|
||||
raise HTTPException(status_code=408, detail="设备未在线")
|
||||
i=0
|
||||
while i<1:
|
||||
try:
|
||||
row = await location_service.get_device_current_location(
|
||||
device_id=device_id,
|
||||
user_id=current_user_id,
|
||||
)
|
||||
except Exception as e:
|
||||
await asyncio.sleep(1)
|
||||
continue
|
||||
|
||||
if row is None:
|
||||
await asyncio.sleep(1)
|
||||
else:
|
||||
if row["updated_at"] is not None \
|
||||
and (datetime.now() - row['updated_at']) <= timedelta(seconds=10):
|
||||
logger.info(
|
||||
"device current location fetched",
|
||||
extra={
|
||||
"event": "device_current_location",
|
||||
"request_id": getattr(request.state, "request_id", None),
|
||||
"user_id": current_user_id,
|
||||
"device_id": device_id,
|
||||
"child_id": int(row["child_id"]),
|
||||
},
|
||||
)
|
||||
return _row_to_current_location_response(row)
|
||||
|
||||
i+=1
|
||||
if row is None:
|
||||
raise HTTPException(status_code=408, detail="GPS数据上报超时")
|
||||
|
||||
logger.info(
|
||||
"device location reported",
|
||||
extra={
|
||||
"event": "device_location_report",
|
||||
"request_id": getattr(request.state, "request_id", None),
|
||||
"device_id": device_id,
|
||||
"child_id": int(row["child_id"]),
|
||||
"lat": float(row["lat"]),
|
||||
"lng": float(row["lng"]),
|
||||
},
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=408, detail=f"GPS数据上报失败: {e}")
|
||||
|
||||
|
||||
logger.info(
|
||||
"device current location fetched",
|
||||
extra={
|
||||
"event": "device_current_location",
|
||||
"request_id": getattr(request.state, "request_id", None),
|
||||
"user_id": current_user_id,
|
||||
"device_id": device_id,
|
||||
"child_id": int(row["child_id"]),
|
||||
},
|
||||
)
|
||||
return _row_to_current_location_response(row)
|
||||
|
||||
|
||||
@router.get("/{device_id}/trajectory", response_model=DeviceLocationTrajectoryResponse)
|
||||
|
||||
@@ -10,13 +10,16 @@ from banban.schemas.mqtt_models import (
|
||||
NFCListenRequest,
|
||||
NFCUnreadRequest,
|
||||
NFCBindRequest,
|
||||
SleepScheduleRequest,
|
||||
RemoteSleepWakeRequest,
|
||||
OpenCommandRequest,
|
||||
CommandResponse,
|
||||
DeviceResponseData,
|
||||
DeviceStatusResponse,
|
||||
)
|
||||
from fastapi import Depends
|
||||
from handlers.mqtt_handler import TalkingQMQTTService
|
||||
from banban.security import get_current_user_id
|
||||
# from banban.security import get_current_user_id
|
||||
|
||||
|
||||
|
||||
@@ -35,7 +38,7 @@ async def _get_service() -> TalkingQMQTTService:
|
||||
@router.post("/gps", response_model=CommandResponse, summary="GPS位置查询")
|
||||
async def query_gps(
|
||||
req: GPSQueryRequest,
|
||||
current_user_id: int = Depends(get_current_user_id)
|
||||
# current_user_id: int = Depends(get_current_user_id)
|
||||
):
|
||||
service = await _get_service()
|
||||
msg_id = await service.send_gps_query(req.device_id)
|
||||
@@ -45,7 +48,7 @@ async def query_gps(
|
||||
@router.post("/volume", response_model=CommandResponse, summary="音量调节")
|
||||
async def set_volume(
|
||||
req: VolumeRequest,
|
||||
current_user_id: int = Depends(get_current_user_id)
|
||||
# current_user_id: int = Depends(get_current_user_id)
|
||||
):
|
||||
service = await _get_service()
|
||||
msg_id = await service.send_volume_command(req.device_id, req.level)
|
||||
@@ -55,7 +58,7 @@ async def set_volume(
|
||||
@router.post("/ota", response_model=CommandResponse, summary="OTA升级")
|
||||
async def start_ota(
|
||||
req: OTARequest,
|
||||
current_user_id: int = Depends(get_current_user_id)
|
||||
# current_user_id: int = Depends(get_current_user_id)
|
||||
):
|
||||
service = await _get_service()
|
||||
msg_id = await service.send_ota_command(req.device_id, req.url, req.version)
|
||||
@@ -65,32 +68,32 @@ async def start_ota(
|
||||
@router.post("/nfc/notice", response_model=CommandResponse, summary="NFC留言下发提示")
|
||||
async def send_nfc_notice(
|
||||
req: NFCNoticeRequest,
|
||||
current_user_id: int = Depends(get_current_user_id)
|
||||
# current_user_id: int = Depends(get_current_user_id)
|
||||
):
|
||||
service = await _get_service()
|
||||
msg_id = await service.send_nfc_notice(req.device_id, req.url)
|
||||
return CommandResponse(msg_id=msg_id, device_id=req.device_id)
|
||||
|
||||
|
||||
@router.post("/nfc/listen", response_model=CommandResponse, summary="NFC收听留言")
|
||||
async def nfc_listen(
|
||||
req: NFCListenRequest,
|
||||
current_user_id: int = Depends(get_current_user_id)
|
||||
):
|
||||
service = await _get_service()
|
||||
topic = f"device/{req.device_id}/event"
|
||||
payload = {
|
||||
"msg_id": "005",
|
||||
"params": {"uuid": req.uuid}
|
||||
}
|
||||
await service._publish(topic, payload)
|
||||
return CommandResponse(msg_id="005", device_id=req.device_id)
|
||||
# @router.post("/nfc/listen", response_model=CommandResponse, summary="NFC收听留言")
|
||||
# async def nfc_listen(
|
||||
# req: NFCListenRequest,
|
||||
# # current_user_id: int = Depends(get_current_user_id)
|
||||
# ):
|
||||
# service = await _get_service()
|
||||
# topic = f"device/{req.device_id}/event"
|
||||
# payload = {
|
||||
# "msg_id": "005",
|
||||
# "params": {"uuid": req.uuid}
|
||||
# }
|
||||
# await service._publish(topic, payload)
|
||||
# return CommandResponse(msg_id="005", device_id=req.device_id)
|
||||
|
||||
|
||||
@router.post("/nfc/unread", response_model=CommandResponse, summary="设置NFC未读留言")
|
||||
async def set_nfc_unread(
|
||||
req: NFCUnreadRequest,
|
||||
current_user_id: int = Depends(get_current_user_id)
|
||||
# current_user_id: int = Depends(get_current_user_id)
|
||||
):
|
||||
await offline_audio_cache.add_audio_url(req.device_id, req.url)
|
||||
return CommandResponse(msg_id="", device_id=req.device_id, message="已设置未读留言")
|
||||
@@ -99,7 +102,7 @@ async def set_nfc_unread(
|
||||
@router.delete("/nfc/unread/{device_id}", response_model=CommandResponse, summary="清除NFC未读留言定时推送")
|
||||
async def clear_nfc_unread(
|
||||
device_id: str,
|
||||
current_user_id: int = Depends(get_current_user_id)
|
||||
# current_user_id: int = Depends(get_current_user_id)
|
||||
):
|
||||
await offline_audio_cache.clear_audio_urls(device_id)
|
||||
return CommandResponse(msg_id="", device_id=device_id, message="已清除未读留言")
|
||||
@@ -108,14 +111,43 @@ async def clear_nfc_unread(
|
||||
@router.post("/nfc/bind", response_model=CommandResponse, summary="NFC绑定卡片")
|
||||
async def bind_nfc(
|
||||
req: NFCBindRequest,
|
||||
current_user_id: int = Depends(get_current_user_id)
|
||||
# current_user_id: int = Depends(get_current_user_id)
|
||||
):
|
||||
# 扫码绑定卡片需要发送NFC绑定命令
|
||||
service = await _get_service()
|
||||
msg_id = await service.send_bind_nfc_command(req.device_id, req.uuid)
|
||||
return CommandResponse(msg_id=msg_id, device_id=req.device_id)
|
||||
|
||||
|
||||
@router.post("/sleep/schedule", response_model=CommandResponse, summary="设置定时休眠")
|
||||
async def set_sleep_schedule(
|
||||
req: SleepScheduleRequest,
|
||||
# current_user_id: int = Depends(get_current_user_id)
|
||||
):
|
||||
service = await _get_service()
|
||||
msg_id = await service.send_sleep_schedule_command(req.device_id, req.start, req.end)
|
||||
return CommandResponse(msg_id=msg_id, device_id=req.device_id)
|
||||
|
||||
|
||||
@router.post("/sleep/remote", response_model=CommandResponse, summary="远程休眠唤醒")
|
||||
async def remote_sleep_wake(
|
||||
req: RemoteSleepWakeRequest,
|
||||
# current_user_id: int = Depends(get_current_user_id)
|
||||
):
|
||||
service = await _get_service()
|
||||
msg_id = await service.send_remote_sleep_wake_command(req.device_id, req.switch)
|
||||
return CommandResponse(msg_id=msg_id, device_id=req.device_id)
|
||||
|
||||
|
||||
@router.post("/device/open", response_model=CommandResponse, summary="设备开关控制")
|
||||
async def control_device_open(
|
||||
req: OpenCommandRequest,
|
||||
# current_user_id: int = Depends(get_current_user_id)
|
||||
):
|
||||
service = await _get_service()
|
||||
msg_id = await service.send_open_command(req.device_id, req.open_type, req.is_open)
|
||||
return CommandResponse(msg_id=msg_id, device_id=req.device_id)
|
||||
|
||||
|
||||
# @router.get("/nfc/unread", summary="查询所有未读留言设备列表")
|
||||
# async def get_nfc_unread_devices():
|
||||
# service = await _get_service()
|
||||
|
||||
@@ -37,6 +37,23 @@ class NFCBindRequest(BaseModel):
|
||||
uuid: str = Field(..., description="NFC卡片UUID")
|
||||
|
||||
|
||||
class SleepScheduleRequest(BaseModel):
|
||||
device_id: str = Field(..., description="设备ID")
|
||||
start: str = Field(..., description="休眠开始时间,格式 HH:MM,如 22:00")
|
||||
end: str = Field(..., description="休眠结束时间,格式 HH:MM,如 07:00")
|
||||
|
||||
|
||||
class RemoteSleepWakeRequest(BaseModel):
|
||||
device_id: str = Field(..., description="设备ID")
|
||||
switch: str = Field(..., description="开关状态,on=唤醒,off=休眠")
|
||||
|
||||
|
||||
class OpenCommandRequest(BaseModel):
|
||||
device_id: str = Field(..., description="设备ID")
|
||||
open_type: int = Field(..., ge=0, le=2, description="0=查询状态,1=控制打开,2=控制关闭")
|
||||
is_open: Optional[int] = Field(None, description="open_type=1时,1=打开,0=关闭")
|
||||
|
||||
|
||||
class CommandResponse(BaseModel):
|
||||
code: int = 0
|
||||
message: str = "success"
|
||||
|
||||
@@ -89,7 +89,7 @@ class LocationService(DatabaseServiceBase):
|
||||
db_session = await self.get_session()
|
||||
try:
|
||||
dao = LocationDAO(db_session)
|
||||
current_row = await dao.get_device_current_location(device_id=device_id)
|
||||
current_row = await dao.get_device_current_location_by_device_id(device_id=device_id)
|
||||
if current_row:
|
||||
await dao.update(device_id=device_id, latitude=location.lat, longitude=location.lng)
|
||||
# 更新成功加到历史记录表
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
from datetime import datetime
|
||||
try:
|
||||
from datetime import UTC # Python 3.11+
|
||||
except ImportError:
|
||||
from datetime import timezone
|
||||
UTC = timezone.utc # Python 3.10 及更早版本
|
||||
|
||||
from urllib.parse import urlparse
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
@@ -1,61 +1,117 @@
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
ports:
|
||||
- "8080:8080"
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
- ./assets:/app/assets
|
||||
- ./logs:/app/logs
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- talkingq-network
|
||||
# mysql:
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: Dockerfile-mysql
|
||||
# ports:
|
||||
# - "13306:3306" # 修改为 13306 或其他未使用的端口
|
||||
# networks:
|
||||
# - app-network
|
||||
# volumes:
|
||||
# - mysql_data:/var/lib/mysql # 持久化数据
|
||||
# healthcheck:
|
||||
# test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-padmin123"]
|
||||
# interval: 10s
|
||||
# timeout: 5s
|
||||
# retries: 5
|
||||
# start_period: 30s
|
||||
|
||||
backend_app:
|
||||
build: ../backend-v0.1
|
||||
ports:
|
||||
- "8001:8001"
|
||||
env_file:
|
||||
- ../backend-v0.1/.env
|
||||
volumes:
|
||||
- ../backend-v0.1/app/assets:/app/assets
|
||||
- ../backend-v0.1/app/logs:/app/logs
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- talkingq-network
|
||||
# redis:
|
||||
# image: redis:7.2-alpine
|
||||
# ports:
|
||||
# - "6379:6379"
|
||||
# networks:
|
||||
# - app-network
|
||||
# volumes:
|
||||
# - redis_data:/data
|
||||
# healthcheck:
|
||||
# test: ["CMD", "redis-cli", "ping"]
|
||||
# interval: 5s
|
||||
# timeout: 3s
|
||||
# retries: 5
|
||||
# start_period: 10s
|
||||
# command: redis-server --appendonly yes
|
||||
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
command: --default-authentication-plugin=caching_sha2_password
|
||||
restart: unless-stopped
|
||||
# node:
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: Dockerfile-node
|
||||
# depends_on:
|
||||
# mysql:
|
||||
# condition: service_healthy
|
||||
# ports:
|
||||
# - "8084:8084"
|
||||
# networks:
|
||||
# - app-network
|
||||
# environment:
|
||||
# - API_URL=http://server:8091
|
||||
|
||||
# server:
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: Dockerfile-server
|
||||
# # 添加构建缓存设置
|
||||
# cache_from:
|
||||
# - eclipse-temurin:21-jre
|
||||
# depends_on:
|
||||
# mysql:
|
||||
# condition: service_healthy
|
||||
# redis:
|
||||
# condition: service_healthy
|
||||
# networks:
|
||||
# - app-network
|
||||
# environment:
|
||||
# SPRING_DATASOURCE_URL: "jdbc:mysql://mysql:3306/talkingq?allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=UTC"
|
||||
# SPRING_DATASOURCE_USERNAME: "root"
|
||||
# SPRING_DATASOURCE_PASSWORD: "admin123"
|
||||
# SPRING_DATA_REDIS_HOST: "redis"
|
||||
# SPRING_DATA_REDIS_PORT: "6379"
|
||||
# SPRING_DATA_REDIS_PASSWORD: ""
|
||||
# volumes:
|
||||
# - maven_repo:/root/.m2/repository # 持久化Maven仓库
|
||||
# restart: on-failure # 添加重启策略,如果启动失败会自动重试
|
||||
|
||||
nginx:
|
||||
image: nginx:stable-alpine
|
||||
# depends_on:
|
||||
# server:
|
||||
# condition: service_started
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
networks:
|
||||
- app-network
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
- ./logs/nginx:/var/log/nginx
|
||||
- ./certbot/conf:/etc/letsencrypt
|
||||
- ./certbot/www:/var/www/certbot
|
||||
- ./web:/usr/share/nginx/html
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD}"
|
||||
MYSQL_DATABASE: talkingq
|
||||
MYSQL_USER: talkingq
|
||||
MYSQL_PASSWORD: "${MYSQL_PASSWORD}"
|
||||
TZ: Asia/Shanghai
|
||||
volumes:
|
||||
- mysql-data:/var/lib/mysql
|
||||
- ./mysql/init:/docker-entrypoint-initdb.d
|
||||
- ./mysql/conf.d:/etc/mysql/conf.d
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "talkingq", "-p${MYSQL_PASSWORD}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
- talkingq-network
|
||||
- TZ=Asia/Shanghai
|
||||
command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"
|
||||
|
||||
volumes:
|
||||
mysql-data:
|
||||
certbot:
|
||||
image: certbot/certbot
|
||||
networks:
|
||||
- app-network
|
||||
volumes:
|
||||
- ./certbot/conf:/etc/letsencrypt
|
||||
- ./certbot/www:/var/www/certbot
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
|
||||
|
||||
networks:
|
||||
talkingq-network:
|
||||
app-network:
|
||||
driver: bridge
|
||||
|
||||
# volumes:
|
||||
# mysql_data: # 定义持久化卷
|
||||
# maven_repo: # 持久化Maven仓库
|
||||
# vosk_models: # 持久化Vosk模型
|
||||
# redis_data: #redis持久卷
|
||||
|
||||
291
talkingq-url/docs/mqtt协议_V1.6.txt
Normal file
291
talkingq-url/docs/mqtt协议_V1.6.txt
Normal file
@@ -0,0 +1,291 @@
|
||||
device/TalkingQ_xxx/command # 服务器下发命令(下行)
|
||||
device/TalkingQ_xxx/response # 设备响应(上行)
|
||||
device/TalkingQ_xxx/event # 设备主动事件
|
||||
device/TalkingQ_xxx/event_resp # 事件返回
|
||||
|
||||
消息格式(JSON)
|
||||
device/TalkingQ_xxx/event
|
||||
0.设备定时10分钟上报信息
|
||||
{
|
||||
"msg_id": "000",
|
||||
"data": {
|
||||
"id":"TalkingQ_xxx",
|
||||
"power": 70, 每个间隔10,从10-100
|
||||
"signal": 3, 信号强度1是最弱的 5是最强
|
||||
"voice":60, 每个间隔20,从40-100
|
||||
"version":"v1.0"
|
||||
}
|
||||
}
|
||||
服务器返回
|
||||
device/TalkingQ_xxx/event_resp
|
||||
{
|
||||
"msg_id": "000",
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
1. GPS位置查询
|
||||
device/TalkingQ_xxx/command
|
||||
服务器下发:
|
||||
{
|
||||
"msg_id": "001"
|
||||
}
|
||||
|
||||
|
||||
device/TalkingQ_xxx/response
|
||||
4G模块响应:
|
||||
{
|
||||
"msg_id": "001",
|
||||
"status": "success",
|
||||
"data": {
|
||||
"latitude": 39.9042,
|
||||
"longitude": 116.4074
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
2. 音量调节
|
||||
device/TalkingQ_xxx/command
|
||||
服务器下发:
|
||||
{
|
||||
"msg_id": "002",
|
||||
"params": {
|
||||
"level": 70
|
||||
}
|
||||
}
|
||||
device/TalkingQ_xxx/response
|
||||
4G模块响应:
|
||||
{
|
||||
"msg_id": "002",
|
||||
"status": "success",
|
||||
"data": {
|
||||
"current_level": 70
|
||||
}
|
||||
}
|
||||
|
||||
3. OTA升级
|
||||
device/TalkingQ_xxx/command
|
||||
服务器下发:
|
||||
{
|
||||
"msg_id": "003",
|
||||
"params": {
|
||||
"url": "http://47.113.99.138:7050/EG800AKCN_91LC-beta1-20260430170621-fota.bin",
|
||||
"version": "2.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
device/TalkingQ_xxx/response
|
||||
4G模块响应(立即确认):
|
||||
{
|
||||
"msg_id": "003",
|
||||
"status": "accepted",
|
||||
"data": {
|
||||
"current_version": "2.0.5",
|
||||
"target_version": "2.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
升级完成
|
||||
{
|
||||
"msg_id": "003",
|
||||
"status": "success",
|
||||
"data": {
|
||||
"new_version": "2.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
4、NFC留言下发提示
|
||||
device/TalkingQ_xxx/command
|
||||
如果设备在线,且有未读留言服务器定时10分钟下发一次:
|
||||
{
|
||||
"msg_id": "004",
|
||||
"params": {
|
||||
"url": "http://xxx.mp3" //您有新的留言请注意查收
|
||||
}
|
||||
}
|
||||
device/TalkingQ_xxx/response
|
||||
4G模块响应:
|
||||
{
|
||||
"msg_id": "004",
|
||||
"status": "success",
|
||||
}
|
||||
|
||||
5、发送或收听留言
|
||||
device/TalkingQ_xxx/event
|
||||
NFC触发,设备端上报
|
||||
{
|
||||
"msg_id": "005",
|
||||
"params": {
|
||||
"uuid": "53C12B6DA20001"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//如果不是本机本地UUID,则下发“你好,你的朋友已在线请留言”
|
||||
//如果本机本地UUID但没有留言,"你好,已没可读留言"
|
||||
服务器下发
|
||||
device/TalkingQ_xxx/event_resp
|
||||
|
||||
{
|
||||
"msg_id": "005",
|
||||
"status": "success",
|
||||
"type": 0, //0代表收听留言 1 代表需要发送留言
|
||||
"params": {
|
||||
"url_1": "http://xxx.mp3"
|
||||
"url_2": "http://xxx.mp3"
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"msg_id": "005",
|
||||
"type": 0,
|
||||
"params":
|
||||
{
|
||||
"url_1": "http://175.24.73.253:8080/assets/audio/no_message.mp3"
|
||||
}
|
||||
}
|
||||
|
||||
6、NFC绑定uuid
|
||||
device/TalkingQ_xxx/command
|
||||
{
|
||||
"msg_id": "006",
|
||||
}
|
||||
device/TalkingQ_xxx/response
|
||||
4G模块响应:
|
||||
{
|
||||
"msg_id": "006",
|
||||
"status": "success",
|
||||
"data": {
|
||||
"uuid": "xxxx"
|
||||
}
|
||||
}
|
||||
|
||||
7、开关状态
|
||||
device/TalkingQ_xxx/command
|
||||
{
|
||||
"msg_id": "007",
|
||||
"type": 0 , //0查询状态 1 控制打开 2控制关闭
|
||||
}
|
||||
device/TalkingQ_xxx/response
|
||||
4G模块响应:
|
||||
{
|
||||
"msg_id": "007",
|
||||
"status": "success",
|
||||
"data": {
|
||||
"status": "on" //on或off 返回当前状态
|
||||
}
|
||||
}
|
||||
|
||||
8、定时休眠
|
||||
device/TalkingQ_xxx/command
|
||||
{
|
||||
"msg_id": "008",
|
||||
"params": {
|
||||
"start": "22:00" , //设置时和分
|
||||
"end": "07:00" //设置时和分
|
||||
}
|
||||
}
|
||||
device/TalkingQ_xxx/response
|
||||
4G模块响应:
|
||||
{
|
||||
"msg_id": "008",
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
9、远程休眠和唤醒
|
||||
device/TalkingQ_xxx/command
|
||||
{
|
||||
"msg_id": "009",
|
||||
"params": {
|
||||
"switch": "on" , // on 唤醒 off 休眠
|
||||
}
|
||||
}
|
||||
device/TalkingQ_xxx/response
|
||||
4G模块响应:
|
||||
{
|
||||
"msg_id": "009",
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
10、额外按键长按发送告警信息
|
||||
device/TalkingQ_xxx/event
|
||||
{
|
||||
"msg_id": "010" //发送告警
|
||||
}
|
||||
|
||||
device/TalkingQ_xxx/event_resp
|
||||
4G模块响应:
|
||||
{
|
||||
"msg_id": "010",
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
11、额外按键短按发送留言给家长
|
||||
device/TalkingQ_xxx/event
|
||||
{
|
||||
"msg_id": "011",
|
||||
"params": {
|
||||
"uuid": "TalkingQ_xxx"
|
||||
}
|
||||
}
|
||||
device/TalkingQ_xxx/event_resp
|
||||
{
|
||||
"msg_id": "011",
|
||||
"type": 0,
|
||||
"params":
|
||||
{
|
||||
"url": "http://175.24.73.253:8080/assets/audio/message_ok.mp3"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
TalkingQ_XQSN00001003 TQ_XQSN000000001003
|
||||
53C12B6DA20001
|
||||
53D92B6DA20001
|
||||
53DA2B6DA20001
|
||||
|
||||
set id TalkingQ_XQSN00001003
|
||||
set sn TQ_XQSN000000001003
|
||||
|
||||
TalkingQ_XQSN00001004 TQ_XQSN000000001004
|
||||
53E12B6DA20001
|
||||
53D12B6DA20001
|
||||
53D22B6DA20001
|
||||
|
||||
TalkingQ_XQSN00001005 TQ_XQSN000000001005
|
||||
53C22B6DA20001
|
||||
|
||||
1000000189ABCDEFGHIJKLMNOPQRSTUV
|
||||
1000000089ABCDEFGHIJKLMNOPQRSTUV
|
||||
|
||||
|
||||
1、额外按钮
|
||||
短按 直接与父母留言
|
||||
长按 播报紧急报警系统
|
||||
|
||||
2、休眠时间段设置功能
|
||||
在设定时间内,设备不可启动
|
||||
设定时间过后,可以正常使用
|
||||
|
||||
|
||||
3、远程一键休眠和一键唤醒
|
||||
|
||||
|
||||
mqtt地址
|
||||
http://175.24.73.253:18083/#/login?to=/clients
|
||||
admin
|
||||
emqx_DPWQyk
|
||||
|
||||
|
||||
{
|
||||
"device_id": "TalkingQ_XQSN00001005",
|
||||
"start": "22:00",
|
||||
"end": "07:00"
|
||||
}
|
||||
|
||||
{
|
||||
"device_id": "TalkingQ_XQSN00001005",
|
||||
"url": "http://175.24.73.253:80/assets/audio/test_zh.mp3"
|
||||
}
|
||||
295
talkingq-url/docs/mqtt协议_V1.7.txt
Normal file
295
talkingq-url/docs/mqtt协议_V1.7.txt
Normal file
@@ -0,0 +1,295 @@
|
||||
device/imei/command # 服务器下发命令(下行)
|
||||
device/imei/response # 设备响应(上行)
|
||||
device/imei/event # 设备主动事件
|
||||
device/imei/event_resp # 事件返回
|
||||
|
||||
消息格式(JSON)
|
||||
device/imei/event
|
||||
0.设备定时10分钟上报信息
|
||||
{
|
||||
"msg_id": "000",
|
||||
"data": {
|
||||
"id":"TalkingQ_xxx",
|
||||
"power": 70, 每个间隔10,从10-100
|
||||
"signal": 3, 信号强度1是最弱的 5是最强
|
||||
"voice":60, 每个间隔20,从40-100
|
||||
"version":"v1.0"
|
||||
}
|
||||
}
|
||||
服务器返回
|
||||
device/imei/event_resp
|
||||
{
|
||||
"msg_id": "000",
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
1. GPS位置查询
|
||||
device/imei/command
|
||||
服务器下发:
|
||||
{
|
||||
"msg_id": "001"
|
||||
}
|
||||
|
||||
|
||||
device/imei/response
|
||||
4G模块响应:
|
||||
{
|
||||
"msg_id": "001",
|
||||
"status": "success",
|
||||
"data": {
|
||||
"latitude": 39.9042,
|
||||
"longitude": 116.4074
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
2. 音量调节
|
||||
device/imei/command
|
||||
服务器下发:
|
||||
{
|
||||
"msg_id": "002",
|
||||
"params": {
|
||||
"level": 70
|
||||
}
|
||||
}
|
||||
device/imei/response
|
||||
4G模块响应:
|
||||
{
|
||||
"msg_id": "002",
|
||||
"status": "success",
|
||||
"data": {
|
||||
"current_level": 70
|
||||
}
|
||||
}
|
||||
|
||||
3. OTA升级
|
||||
device/imei/command
|
||||
服务器下发:
|
||||
{
|
||||
"msg_id": "003",
|
||||
"params": {
|
||||
"url": "http://47.113.99.138:7050/EG800AKCN_91LC-beta1-20260501205451-fota.bin",
|
||||
"version": "2.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
device/imei/response
|
||||
4G模块响应(立即确认):
|
||||
{
|
||||
"msg_id": "003",
|
||||
"status": "accepted",
|
||||
"data": {
|
||||
"current_version": "2.0.5",
|
||||
"target_version": "2.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
升级完成
|
||||
{
|
||||
"msg_id": "003",
|
||||
"status": "success",
|
||||
"data": {
|
||||
"new_version": "2.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
4、NFC留言下发提示
|
||||
device/imei/command
|
||||
如果设备在线,且有未读留言服务器定时10分钟下发一次:
|
||||
{
|
||||
"msg_id": "004",
|
||||
"params": {
|
||||
"url": "http://xxx.mp3" //您有新的留言请注意查收
|
||||
}
|
||||
}
|
||||
device/imei/response
|
||||
4G模块响应:
|
||||
{
|
||||
"msg_id": "004",
|
||||
"status": "success",
|
||||
}
|
||||
|
||||
5、发送或收听留言
|
||||
device/imei/event
|
||||
NFC触发,设备端上报
|
||||
{
|
||||
"msg_id": "005",
|
||||
"params": {
|
||||
"uuid": "53C12B6DA20001"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//如果不是本机本地UUID,则下发“你好,你的朋友已在线请留言”
|
||||
//如果本机本地UUID但没有留言,"你好,已没可读留言"
|
||||
服务器下发
|
||||
device/imei/event_resp
|
||||
|
||||
{
|
||||
"msg_id": "005",
|
||||
"status": "success",
|
||||
"type": 0, //0代表收听留言 1 代表需要发送留言
|
||||
"params": {
|
||||
"url_1": "http://xxx.mp3"
|
||||
"url_2": "http://xxx.mp3"
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"msg_id": "005",
|
||||
"type": 0,
|
||||
"params":
|
||||
{
|
||||
"url_1": "http://175.24.73.253:8080/assets/audio/no_message.mp3"
|
||||
}
|
||||
}
|
||||
|
||||
6、NFC绑定uuid
|
||||
device/imei/command
|
||||
{
|
||||
"msg_id": "006"
|
||||
}
|
||||
device/imei/response
|
||||
4G模块响应:
|
||||
{
|
||||
"msg_id": "006",
|
||||
"status": "success",
|
||||
"data": {
|
||||
"uuid": "xxxx"
|
||||
}
|
||||
}
|
||||
|
||||
7、开关状态
|
||||
//0查询状态 1 控制打开 2控制关闭
|
||||
device/imei/command
|
||||
{
|
||||
"msg_id": "007",
|
||||
"type": 0
|
||||
}
|
||||
device/imei/response
|
||||
4G模块响应:
|
||||
{
|
||||
"msg_id": "007",
|
||||
"status": "success",
|
||||
"data": {
|
||||
"status": "on" //on或off 返回当前状态
|
||||
}
|
||||
}
|
||||
|
||||
8、定时休眠
|
||||
device/imei/command
|
||||
{
|
||||
"msg_id": "008",
|
||||
"params": {
|
||||
"start": "22:00" , //设置时和分
|
||||
"end": "07:00" //设置时和分
|
||||
}
|
||||
}
|
||||
device/imei/response
|
||||
4G模块响应:
|
||||
{
|
||||
"msg_id": "008",
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
9、远程休眠和唤醒
|
||||
device/imei/command
|
||||
{
|
||||
"msg_id": "009",
|
||||
"params": {
|
||||
"switch": "on" // on 唤醒 off 休眠
|
||||
}
|
||||
}
|
||||
device/imei/response
|
||||
4G模块响应:
|
||||
{
|
||||
"msg_id": "009",
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
10、额外按键长按发送告警信息
|
||||
device/imei/event
|
||||
{
|
||||
"msg_id": "010" //发送告警
|
||||
}
|
||||
|
||||
device/imei/event_resp
|
||||
4G模块响应:
|
||||
{
|
||||
"msg_id": "010",
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
11、额外按键短按发送留言给家长
|
||||
device/imei/event
|
||||
{
|
||||
"msg_id": "011",
|
||||
"params": {
|
||||
"uuid": "TalkingQ_xxx"
|
||||
}
|
||||
}
|
||||
device/imei/event_resp
|
||||
{
|
||||
"msg_id": "011",
|
||||
"type": 0,
|
||||
"params":
|
||||
{
|
||||
"url": "http://175.24.73.253:8080/assets/audio/message_ok.mp3"
|
||||
}
|
||||
}
|
||||
|
||||
12、增加后台自动下发DeviceID和SN流程
|
||||
设备端上报
|
||||
device/imei/event
|
||||
{
|
||||
"msg_id": "012"
|
||||
}
|
||||
服务器下发
|
||||
device/imei/event_resp
|
||||
{
|
||||
"msg_id": "012",
|
||||
"params":
|
||||
{
|
||||
"url": "175.24.73.253:80",
|
||||
"device_id":"TalkingQ_XQSN00001003",
|
||||
"device_sn":"TQ_XQSN000000001003",
|
||||
}
|
||||
}
|
||||
|
||||
TalkingQ_XQSN00001003 TQ_XQSN000000001003
|
||||
53C12B6DA20001
|
||||
53D92B6DA20001
|
||||
53DA2B6DA20001
|
||||
|
||||
set id TalkingQ_XQSN00001003
|
||||
set sn TQ_XQSN000000001003
|
||||
|
||||
TalkingQ_XQSN00001004 TQ_XQSN000000001004
|
||||
53E12B6DA20001
|
||||
53D12B6DA20001
|
||||
53D22B6DA20001
|
||||
|
||||
TalkingQ_XQSN00001005 TQ_XQSN000000001005
|
||||
53C22B6DA20001
|
||||
|
||||
1000000189ABCDEFGHIJKLMNOPQRSTUV
|
||||
1000000089ABCDEFGHIJKLMNOPQRSTUV
|
||||
|
||||
|
||||
1、额外按钮
|
||||
短按 直接与父母留言
|
||||
长按 播报紧急报警系统
|
||||
|
||||
2、休眠时间段设置功能
|
||||
在设定时间内,设备不可启动
|
||||
设定时间过后,可以正常使用
|
||||
|
||||
|
||||
3、远程一键休眠和一键唤醒
|
||||
|
||||
|
||||
mqtt地址
|
||||
http://175.24.73.253:18083/#/login?to=/clients
|
||||
admin
|
||||
emqx_DPWQyk
|
||||
@@ -5,15 +5,53 @@ from utils.logger import session_logger
|
||||
message_audio_storage_service = MessageAudioStorageService()
|
||||
|
||||
|
||||
# async def save_audio_file(audio_data: bytes, device_id: str) -> str:
|
||||
# """Upload device audio to COS and return its object key."""
|
||||
# try:
|
||||
# stored = await message_audio_storage_service.upload_audio(
|
||||
# device_id=device_id,
|
||||
# content=audio_data,
|
||||
# )
|
||||
# session_logger.info(device_id, "audio", f"audio uploaded to COS: {stored.file_key}")
|
||||
# return stored.file_key
|
||||
# except Exception as e:
|
||||
# session_logger.error(device_id, "audio", f"failed to store audio: {e}", exc_info=True)
|
||||
# raise
|
||||
|
||||
|
||||
import os
|
||||
import uuid
|
||||
from config import settings
|
||||
from utils.logger import session_logger
|
||||
# from utils.audio_denoiser import reduce_background_noise
|
||||
|
||||
|
||||
async def save_audio_file(audio_data: bytes, device_id: str) -> str:
|
||||
"""Upload device audio to COS and return its object key."""
|
||||
"""
|
||||
保存音频数据到 assets/audio 目录
|
||||
|
||||
Args:
|
||||
audio_data: 音频二进制数据
|
||||
device_id: 设备ID
|
||||
|
||||
Returns:
|
||||
音频文件的相对路径
|
||||
"""
|
||||
try:
|
||||
stored = await message_audio_storage_service.upload_audio(
|
||||
device_id=device_id,
|
||||
content=audio_data,
|
||||
)
|
||||
session_logger.info(device_id, "audio", f"audio uploaded to COS: {stored.file_key}")
|
||||
return stored.file_key
|
||||
audio_dir = os.path.join(settings.assets_dir, "audio")
|
||||
os.makedirs(audio_dir, exist_ok=True)
|
||||
|
||||
filename = f"{device_id}_{uuid.uuid4().hex[:8]}.mp3"
|
||||
filepath = os.path.join(audio_dir, filename)
|
||||
|
||||
with open(filepath, 'wb') as f:
|
||||
f.write(audio_data)
|
||||
|
||||
# relative_path = f"assets/audio/{filename}"
|
||||
session_logger.info(device_id, "audio", f"音频文件已保存: {filepath}")
|
||||
|
||||
# reduce_background_noise(filepath, relative_path,noise_path='assets/audio/noise_sample.wav',normalize_volume=True)
|
||||
return filepath
|
||||
except Exception as e:
|
||||
session_logger.error(device_id, "audio", f"failed to store audio: {e}", exc_info=True)
|
||||
session_logger.error(device_id, "audio", f"保存音频文件时出错: {e}", exc_info=True)
|
||||
raise
|
||||
@@ -43,6 +43,10 @@ class TalkingQMQTTService:
|
||||
"005": self._handle_nfc_listen_report,
|
||||
"006": self._handle_bind_response,
|
||||
"007": self._handle_open_response,
|
||||
"008": self._handle_sleep_schedule_response,
|
||||
"009": self._handle_remote_sleep_wake_response,
|
||||
"010": self._handle_alarm_report,
|
||||
"011": self._handle_short_press_message,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
@@ -161,11 +165,11 @@ class TalkingQMQTTService:
|
||||
await self._send_nfc_listen_response(device_id, nfc_uuid)
|
||||
|
||||
async def _handle_bind_response(self, device_id: str, payload: dict):
|
||||
params = payload.get("params", {})
|
||||
params = payload
|
||||
status = params.get("status")
|
||||
logger.info(device_id, "", f"[NFC绑定] 设备 {device_id} 请求绑定卡片返回状态: {status}")
|
||||
|
||||
nfc_uuid = params.get("uuid")
|
||||
data = params.get("data", {})
|
||||
nfc_uuid = data.get("uuid")
|
||||
# 卡片与设备绑定
|
||||
# await card_service.activate_card(device_id=device_id, card_uuid=nfc_uuid)
|
||||
service = BindingService()
|
||||
@@ -176,13 +180,44 @@ class TalkingQMQTTService:
|
||||
logger.info(device_id, "", f"[NFC绑定] 设备 {device_id} 请求绑定卡片, UUID={nfc_uuid}, result={result}")
|
||||
|
||||
async def _handle_open_response(self, device_id: str, payload: dict):
|
||||
params = payload.get("params", {})
|
||||
params = payload
|
||||
status = params.get("status")
|
||||
logger.info(device_id, "", f"[设备状态] 设备 {device_id} 请求返回设备状态: {status}")
|
||||
data = params.get("data", {})
|
||||
device_status = data.get("status")
|
||||
logger.info(device_id, "", f"[设备状态] 设备 {device_id} 请求设备状态: {device_status}")
|
||||
|
||||
async def _handle_sleep_schedule_response(self, device_id: str, payload: dict):
|
||||
status = payload.get("status")
|
||||
if status == "success":
|
||||
logger.info(device_id, "", f"[定时休眠] 设备 {device_id} 休眠时间设置成功")
|
||||
else:
|
||||
logger.warning(device_id, "", f"[定时休眠] 设备 {device_id} 休眠时间设置失败: {payload}")
|
||||
|
||||
async def _handle_remote_sleep_wake_response(self, device_id: str, payload: dict):
|
||||
status = payload.get("status")
|
||||
if status == "success":
|
||||
logger.info(device_id, "", f"[远程休眠唤醒] 设备 {device_id} 操作成功")
|
||||
else:
|
||||
logger.warning(device_id, "", f"[远程休眠唤醒] 设备 {device_id} 操作失败: {payload}")
|
||||
|
||||
async def _handle_alarm_report(self, device_id: str, payload: dict):
|
||||
logger.info(device_id, "", f"[告警] 设备 {device_id} 发送紧急报警")
|
||||
await self._publish(f"device/{device_id}/event_resp", {"msg_id": "010", "status": "success"})
|
||||
|
||||
async def _handle_short_press_message(self, device_id: str, payload: dict):
|
||||
params = payload.get("params", {})
|
||||
nfc_uuid = params.get("uuid")
|
||||
logger.info(device_id, "", f"[短按留言] 设备 {device_id} 短按发送留言, UUID={nfc_uuid}")
|
||||
payload = {
|
||||
"msg_id": "011",
|
||||
"type": 0,
|
||||
"params": {
|
||||
"url": f"http://{settings.server_host}:{settings.server_port}/assets/audio/message_ok.mp3"
|
||||
}
|
||||
}
|
||||
await self._publish(f"device/{device_id}/event_resp", payload)
|
||||
|
||||
async def _send_nfc_listen_response(self, device_id: str, nfc_uuid: str):
|
||||
topic = f"device/{device_id}/event_resp"
|
||||
card = await card_service.get_card_by_uuid(nfc_uuid)
|
||||
@@ -201,7 +236,7 @@ class TalkingQMQTTService:
|
||||
is_owner = await card_service.check_card_ownership(nfc_uuid, device_id)
|
||||
if is_owner:
|
||||
has_pending = await offline_audio_cache.has_pending_audio(device_id)
|
||||
if nfc_uuid == "53D92B6DA20001":
|
||||
if nfc_uuid == "53C22B6DA20001":
|
||||
payload = {
|
||||
"msg_id": "005",
|
||||
"type": 0,
|
||||
@@ -214,12 +249,12 @@ class TalkingQMQTTService:
|
||||
if has_pending:
|
||||
audio_urls = await offline_audio_cache.get_audio_urls(device_id)
|
||||
# 53D92B6DA20001 测试卡片
|
||||
if nfc_uuid == "53D92B6DA20001":
|
||||
if nfc_uuid == "53C22B6DA20001":
|
||||
payload = {
|
||||
"msg_id": "005",
|
||||
"type": 0,
|
||||
"params": {
|
||||
"url_1": f"http://{settings.server_host}:{settings.server_port}/assets/audio/test_zh.mp3"
|
||||
"url_1": f"http://{settings.server_host}:{settings.server_port}/assets/audio/TalkingQ_XQSN00001005_2f654480.mp3"
|
||||
}
|
||||
}
|
||||
await self._publish(topic, payload)
|
||||
@@ -366,7 +401,7 @@ class TalkingQMQTTService:
|
||||
topic = f"device/{device_id}/command"
|
||||
payload = {
|
||||
"msg_id": "004",
|
||||
"params": {"url_1": url}
|
||||
"params": {"url": url}
|
||||
}
|
||||
await self._publish(topic, payload)
|
||||
return "004"
|
||||
@@ -375,15 +410,18 @@ class TalkingQMQTTService:
|
||||
async def send_bind_nfc_command(self, device_id: str, uuid: str) -> str:
|
||||
topic = f"device/{device_id}/command"
|
||||
payload = {
|
||||
"msg_id": "006"
|
||||
"msg_id": "006",
|
||||
"params": {
|
||||
"url": f"http://{settings.server_host}:{settings.server_port}/assets/audio/bind_nfc_ready_zh.mp3"
|
||||
}
|
||||
}
|
||||
await self._publish(topic, payload)
|
||||
return "006"
|
||||
|
||||
async def send_open_command(self, device_id: str, open_type: int, is_open: int) -> str:
|
||||
topic = f"device/{device_id}/command"
|
||||
if open_type not in [0, 1]:
|
||||
raise ValueError("open_type must be 0 or 1")
|
||||
if open_type not in [0, 1, 2]:
|
||||
raise ValueError("open_type must be 0, 1 or 2")
|
||||
if open_type == 0:
|
||||
payload = {
|
||||
"msg_id": "007",
|
||||
@@ -392,8 +430,37 @@ class TalkingQMQTTService:
|
||||
elif open_type == 1:
|
||||
payload = {
|
||||
"msg_id": "007",
|
||||
"type": open_type,
|
||||
"status": is_open
|
||||
"type": open_type
|
||||
}
|
||||
elif open_type == 2:
|
||||
payload = {
|
||||
"msg_id": "007",
|
||||
"type": open_type
|
||||
}
|
||||
await self._publish(topic, payload)
|
||||
return "007"
|
||||
|
||||
async def send_sleep_schedule_command(self, device_id: str, start: str, end: str) -> str:
|
||||
topic = f"device/{device_id}/command"
|
||||
payload = {
|
||||
"msg_id": "008",
|
||||
"params": {
|
||||
"start": start,
|
||||
"end": end
|
||||
}
|
||||
}
|
||||
await self._publish(topic, payload)
|
||||
return "008"
|
||||
|
||||
async def send_remote_sleep_wake_command(self, device_id: str, switch: str) -> str:
|
||||
if switch not in ["on", "off"]:
|
||||
raise ValueError("switch must be 'on' or 'off'")
|
||||
topic = f"device/{device_id}/command"
|
||||
payload = {
|
||||
"msg_id": "009",
|
||||
"params": {
|
||||
"switch": switch
|
||||
}
|
||||
}
|
||||
await self._publish(topic, payload)
|
||||
return "009"
|
||||
|
||||
@@ -18,6 +18,8 @@ from handlers.prompt_sound_handler import handle_prompt_sound_request
|
||||
from handlers.session_cleanup_handler import handle_old_session_cleanup
|
||||
from config import settings
|
||||
from banban.service.im import im_service as im_conversation_service
|
||||
from handlers.audio_file_handler import message_audio_storage_service, save_audio_file
|
||||
from fastapi import HTTPException
|
||||
|
||||
async def handle_websocket_messages(websocket: WebSocket, device_id: str, serial_number: str):
|
||||
"""
|
||||
@@ -265,11 +267,14 @@ async def process_cached_audio(device_id: str, target_device_id: str, serial_num
|
||||
return
|
||||
|
||||
# 保存音频文件
|
||||
audio_path = await save_audio_file(cached_audio, device_id)
|
||||
audio_url = f"http://{settings.server_host}:{settings.server_port}/{audio_path}"
|
||||
audio_file_key = await save_audio_file(cached_audio, device_id)
|
||||
audio_url = f"http://{settings.server_host}:{settings.server_port}/{audio_file_key}"
|
||||
# 将音频URL保存到数据库 im_conversation和im_message
|
||||
await im_conversation_service.create_device_message(device_id=device_id, serial_number=serial_number, target_device_id=target_device_id, audio_url=audio_url)
|
||||
|
||||
# await im_conversation_service.create_device_message(device_id=device_id, serial_number=serial_number, target_device_id=target_device_id, audio_url=audio_file_key)
|
||||
# try:
|
||||
# audio_url = await message_audio_storage_service.get_audio_url(audio_file_key)
|
||||
# except Exception:
|
||||
# audio_url = audio_file_key
|
||||
# 发送URL给目标设备
|
||||
# target_websocket = await connection_manager.get_connection(target_device_id)
|
||||
# if target_websocket and target_websocket.client_state.name == "CONNECTED":
|
||||
@@ -290,7 +295,7 @@ async def process_cached_audio(device_id: str, target_device_id: str, serial_num
|
||||
await websocket.send_text(f"PROMPT_SOUND_URL:{success_audio_url}")
|
||||
session_logger.info(device_id, "device", f"留言已收到音频URL给设备 {device_id}")
|
||||
else:
|
||||
session_logger.warning(device_id, "device", f"设备 {device_id} 不在线,发送留言已收到音频URL失败")
|
||||
session_logger.warning(device_id, "device", f"设备 {device_id} 不在线,暂不发送留言已收到音频URL")
|
||||
# # # 目标设备不在线,保存到离线缓存
|
||||
# await offline_audio_cache.add_audio_url(target_device_id, audio_url)
|
||||
# session_logger.warning(device_id, "target", f"目标设备 {target_device_id} 不在线,保存音频URL到离线缓存")
|
||||
@@ -302,7 +307,19 @@ async def process_cached_audio(device_id: str, target_device_id: str, serial_num
|
||||
# else:
|
||||
# session_logger.warning(device_id, "device", f"设备 {device_id} 不在线,发送留言成功音频失败")
|
||||
except Exception as e:
|
||||
session_logger.error(device_id, "target", f"处理缓存音频时出错: {e}", exc_info=True)
|
||||
# 处理HTTPException异常
|
||||
if isinstance(e, HTTPException):
|
||||
session_logger.error(device_id, "target", f"保存音频文件时出错,返回HTTPException: {e}")
|
||||
websocket = await connection_manager.get_connection(device_id)
|
||||
if e.status_code == 404 or e.status_code == 400:
|
||||
if websocket and websocket.client_state.name == "CONNECTED":
|
||||
success_audio_url = f"http://{settings.server_host}:{settings.server_port}/assets/audio/save_audio_fail.mp3"
|
||||
await websocket.send_text(f"PROMPT_SOUND_URL:{success_audio_url}")
|
||||
session_logger.info(device_id, "device", f"留言已收到音频URL给设备 {device_id}")
|
||||
else:
|
||||
session_logger.warning(device_id, "device", f"设备 {device_id} 不在线,暂不发送留言已收到音频URL")
|
||||
else:
|
||||
session_logger.error(device_id, "target", f"处理缓存音频时出错: {e}", exc_info=True)
|
||||
finally:
|
||||
# 清除缓存
|
||||
await target_audio_cache.clear_audio_data(target_device_id)
|
||||
|
||||
65
talkingq-url/init-letsencrypt.sh
Executable file
65
talkingq-url/init-letsencrypt.sh
Executable file
@@ -0,0 +1,65 @@
|
||||
#!/bin/bash
|
||||
|
||||
if ! docker compose version >/dev/null 2>&1; then
|
||||
echo 'Error: docker compose is not installed.' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
domains=(banban.api.talkingq.com)
|
||||
rsa_key_size=4096
|
||||
data_path="./certbot"
|
||||
email="admin2345@talkingq.com" # 修改为您的邮箱地址
|
||||
staging=0 # 设置为1表示测试模式,0表示生产模式
|
||||
|
||||
if [ -d "$data_path" ]; then
|
||||
read -p "已存在证书数据。是否继续? (y/N) " decision
|
||||
if [ "$decision" != "Y" ] && [ "$decision" != "y" ]; then
|
||||
exit
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "### 创建证书目录结构 ###"
|
||||
sudo mkdir -p "$data_path/conf/live/$domains"
|
||||
sudo mkdir -p "$data_path/www"
|
||||
|
||||
echo "### 创建临时自签名证书 ###"
|
||||
path="/etc/letsencrypt/live/$domains"
|
||||
sudo mkdir -p "$data_path/conf/live/$domains"
|
||||
sudo docker compose run --rm --entrypoint "\
|
||||
openssl req -x509 -nodes -newkey rsa:$rsa_key_size -days 1\
|
||||
-keyout '$path/privkey.pem' \
|
||||
-out '$path/fullchain.pem' \
|
||||
-subj '/CN=localhost'" certbot
|
||||
|
||||
echo "### 启动nginx ###"
|
||||
sudo docker compose up --force-recreate -d nginx
|
||||
|
||||
echo "### 删除临时证书 ###"
|
||||
sudo docker compose run --rm --entrypoint "\
|
||||
rm -Rf /etc/letsencrypt/live/$domains && \
|
||||
rm -Rf /etc/letsencrypt/archive/$domains && \
|
||||
rm -Rf /etc/letsencrypt/renewal/$domains.conf" certbot
|
||||
|
||||
echo "### 申请Let's Encrypt证书 ###"
|
||||
domain_args=""
|
||||
for domain in "${domains[@]}"; do
|
||||
domain_args="$domain_args -d $domain"
|
||||
done
|
||||
|
||||
# 选择是否使用staging环境
|
||||
case "$staging" in
|
||||
0) staging_arg="";;
|
||||
1) staging_arg="--staging";;
|
||||
esac
|
||||
|
||||
sudo docker compose run --rm --entrypoint "\
|
||||
certbot certonly --webroot -w /var/www/certbot \
|
||||
$staging_arg \
|
||||
$domain_args \
|
||||
--email $email \
|
||||
--rsa-key-size $rsa_key_size \
|
||||
--agree-tos \
|
||||
--force-renewal" certbot
|
||||
|
||||
echo "### 重启nginx ###"
|
||||
sudo docker compose exec nginx nginx -s reload
|
||||
@@ -135,7 +135,7 @@ configure_static_assets(app)
|
||||
|
||||
# 安装中间件
|
||||
install_request_logging_middleware(app)
|
||||
install_auth_middleware(app)
|
||||
# install_auth_middleware(app)
|
||||
|
||||
app.include_router(api_router)
|
||||
app.include_router(banban_router)
|
||||
|
||||
114
talkingq-url/nginx.conf
Normal file
114
talkingq-url/nginx.conf
Normal file
@@ -0,0 +1,114 @@
|
||||
# 默认服务器配置已移至HTTPS服务器块
|
||||
|
||||
# 添加JavaScript MIME类型映射
|
||||
# types {
|
||||
# application/javascript js mjs ts;
|
||||
# text/javascript js mjs ts;
|
||||
# }
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name banban.api.talkingq.com;
|
||||
# location = /msUqvicTHS.txt {
|
||||
# root /usr/share/nginx/html;
|
||||
# }
|
||||
# location / {
|
||||
# root /usr/share/nginx/html;
|
||||
# }
|
||||
# HTTP 跳转到 HTTPS
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
# Let's Encrypt HTTP-01 校验
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name banban.api.talkingq.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/banban.api.talkingq.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/banban.api.talkingq.com/privkey.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
|
||||
access_log /var/log/nginx/access.log;
|
||||
error_log /var/log/nginx/error.log;
|
||||
|
||||
location = /msUqvicTHS.txt {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
# 将API请求转发到Java后端服务
|
||||
location / {
|
||||
proxy_pass http://175.24.73.253:80;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Original-URI $request_uri;
|
||||
proxy_set_header Authorization $http_authorization;
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate";
|
||||
expires -1;
|
||||
}
|
||||
# # 将API请求转发到Java后端服务
|
||||
# location /api/ {
|
||||
# proxy_pass http://server:8091;
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# proxy_set_header X-Original-URI $request_uri;
|
||||
# add_header Cache-Control "no-store, no-cache, must-revalidate";
|
||||
# expires -1;
|
||||
# }
|
||||
|
||||
# # 静态资源处理
|
||||
# location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|mp3|mp4|avi|pdf|txt)$ {
|
||||
# proxy_pass http://node:8084;
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# expires 1d;
|
||||
# proxy_buffering off;
|
||||
# add_header Cache-Control "public, max-age=86400";
|
||||
# }
|
||||
|
||||
# # Vite特定的虚拟资源处理
|
||||
# location ~ ^(/@id|/@vite|/node_modules) {
|
||||
# proxy_pass http://node:8084;
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# add_header Cache-Control "no-store, no-cache, must-revalidate";
|
||||
# expires -1;
|
||||
# }
|
||||
|
||||
# # WebSocket支持(Vite HMR)
|
||||
# location / {
|
||||
# proxy_pass http://node:8084;
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_buffering off;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# proxy_set_header X-Original-URI $request_uri;
|
||||
# add_header Cache-Control "no-store, no-cache, must-revalidate";
|
||||
# expires -1;
|
||||
|
||||
# # WebSocket配置
|
||||
# proxy_http_version 1.1;
|
||||
# proxy_set_header Upgrade $http_upgrade;
|
||||
# proxy_set_header Connection "upgrade";
|
||||
# proxy_read_timeout 86400;
|
||||
# }
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
431
talkingq-url/test/minimax_tts_gui.py
Normal file
431
talkingq-url/test/minimax_tts_gui.py
Normal file
@@ -0,0 +1,431 @@
|
||||
import os
|
||||
import asyncio
|
||||
import threading
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox, filedialog
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
import aiohttp
|
||||
import aiofiles
|
||||
import binascii
|
||||
import json
|
||||
import logging
|
||||
import subprocess
|
||||
import platform
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
logger = logging.getLogger("minimax_tts_gui")
|
||||
|
||||
class MiniMaxTTSGUI:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.root.title("MiniMax TTS 工具")
|
||||
self.root.geometry("750x700")
|
||||
|
||||
self.current_audio_file = None
|
||||
self.synthesis_in_progress = False
|
||||
self.text_entries = []
|
||||
self.config_collapsed = False
|
||||
|
||||
self.load_env()
|
||||
self.setup_ui()
|
||||
|
||||
def load_env(self):
|
||||
env_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "minimax.env")
|
||||
if os.path.exists(env_path):
|
||||
load_dotenv(env_path)
|
||||
logger.info(f"已加载环境变量文件: {env_path}")
|
||||
else:
|
||||
logger.warning(f"环境变量文件不存在: {env_path}")
|
||||
|
||||
def setup_ui(self):
|
||||
main_frame = ttk.Frame(self.root, padding="10")
|
||||
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
||||
|
||||
self.root.columnconfigure(0, weight=1)
|
||||
self.root.rowconfigure(0, weight=1)
|
||||
main_frame.columnconfigure(1, weight=1)
|
||||
|
||||
row = 0
|
||||
|
||||
config_header_frame = ttk.Frame(main_frame)
|
||||
config_header_frame.grid(row=row, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 5))
|
||||
|
||||
ttk.Label(config_header_frame, text="配置信息", font=("Arial", 12, "bold")).pack(side=tk.LEFT)
|
||||
self.toggle_btn = ttk.Button(config_header_frame, text="▼", width=3, command=self.toggle_config)
|
||||
self.toggle_btn.pack(side=tk.RIGHT)
|
||||
|
||||
row += 1
|
||||
|
||||
self.config_frame = ttk.Frame(main_frame)
|
||||
self.config_frame.grid(row=row, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))
|
||||
|
||||
config_row = 0
|
||||
|
||||
ttk.Label(self.config_frame, text="API Key:").grid(row=config_row, column=0, sticky=tk.W, pady=5)
|
||||
self.api_key_var = tk.StringVar(value=os.getenv("MINIMAX_API_KEY", ""))
|
||||
ttk.Entry(self.config_frame, textvariable=self.api_key_var, width=60).grid(row=config_row, column=1, sticky=(tk.W, tk.E), pady=5)
|
||||
config_row += 1
|
||||
|
||||
ttk.Label(self.config_frame, text="Group ID:").grid(row=config_row, column=0, sticky=tk.W, pady=5)
|
||||
self.group_id_var = tk.StringVar(value=os.getenv("MINIMAX_GROUP_ID", "1915292410024300630"))
|
||||
ttk.Entry(self.config_frame, textvariable=self.group_id_var, width=60).grid(row=config_row, column=1, sticky=(tk.W, tk.E), pady=5)
|
||||
config_row += 1
|
||||
|
||||
ttk.Label(self.config_frame, text="Base URL:").grid(row=config_row, column=0, sticky=tk.W, pady=5)
|
||||
self.base_url_var = tk.StringVar(value=os.getenv("MINIMAX_BASE_URL", "https://api.minimax.chat/v1/t2a_v2"))
|
||||
ttk.Entry(self.config_frame, textvariable=self.base_url_var, width=60).grid(row=config_row, column=1, sticky=(tk.W, tk.E), pady=5)
|
||||
config_row += 1
|
||||
|
||||
ttk.Label(self.config_frame, text="音色 ID:").grid(row=config_row, column=0, sticky=tk.W, pady=5)
|
||||
self.voice_id_var = tk.StringVar(value=os.getenv("MINIMAX_VOICE_ID", "cartoon-boy-01"))
|
||||
ttk.Entry(self.config_frame, textvariable=self.voice_id_var, width=60).grid(row=config_row, column=1, sticky=(tk.W, tk.E), pady=5)
|
||||
config_row += 1
|
||||
|
||||
self.config_frame.columnconfigure(1, weight=1)
|
||||
|
||||
row += 1
|
||||
|
||||
ttk.Separator(main_frame, orient='horizontal').grid(row=row, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=10)
|
||||
row += 1
|
||||
|
||||
text_list_header = ttk.Frame(main_frame)
|
||||
text_list_header.grid(row=row, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 5))
|
||||
|
||||
ttk.Label(text_list_header, text="文本列表", font=("Arial", 12, "bold")).pack(side=tk.LEFT)
|
||||
add_btn = ttk.Button(text_list_header, text="➕", width=3, command=self.add_text_entry)
|
||||
add_btn.pack(side=tk.RIGHT)
|
||||
row += 1
|
||||
|
||||
self.text_list_container = ttk.Frame(main_frame)
|
||||
self.text_list_container.grid(row=row, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), pady=5)
|
||||
self.text_list_container.columnconfigure(1, weight=1)
|
||||
|
||||
self.add_text_entry()
|
||||
|
||||
row += 1
|
||||
|
||||
ttk.Separator(main_frame, orient='horizontal').grid(row=row, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=10)
|
||||
row += 1
|
||||
|
||||
ttk.Label(main_frame, text="合成参数", font=("Arial", 12, "bold")).grid(row=row, column=0, columnspan=2, sticky=tk.W, pady=(0, 10))
|
||||
row += 1
|
||||
|
||||
ttk.Label(main_frame, text="输出目录:").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.output_dir_var = tk.StringVar(value="assets")
|
||||
ttk.Entry(main_frame, textvariable=self.output_dir_var, width=50).grid(row=row, column=1, sticky=(tk.W, tk.E), pady=5)
|
||||
row += 1
|
||||
|
||||
ttk.Label(main_frame, text="语速:").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.speed_var = tk.DoubleVar(value=1.0)
|
||||
ttk.Scale(main_frame, from_=0.5, to=2.0, variable=self.speed_var, orient=tk.HORIZONTAL, length=300).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
ttk.Label(main_frame, textvariable=self.speed_var).grid(row=row, column=1, sticky=tk.E, pady=5)
|
||||
row += 1
|
||||
|
||||
ttk.Label(main_frame, text="音量:").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.vol_var = tk.DoubleVar(value=1.0)
|
||||
ttk.Scale(main_frame, from_=0.5, to=2.0, variable=self.vol_var, orient=tk.HORIZONTAL, length=300).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
ttk.Label(main_frame, textvariable=self.vol_var).grid(row=row, column=1, sticky=tk.E, pady=5)
|
||||
row += 1
|
||||
|
||||
ttk.Label(main_frame, text="音调:").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.pitch_var = tk.IntVar(value=0)
|
||||
ttk.Scale(main_frame, from_=-10, to=10, variable=self.pitch_var, orient=tk.HORIZONTAL, length=300).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
ttk.Label(main_frame, textvariable=self.pitch_var).grid(row=row, column=1, sticky=tk.E, pady=5)
|
||||
row += 1
|
||||
|
||||
button_frame = ttk.Frame(main_frame)
|
||||
button_frame.grid(row=row, column=0, columnspan=2, pady=15)
|
||||
|
||||
self.synthesize_btn = ttk.Button(button_frame, text="开始合成", command=self.start_synthesis)
|
||||
self.synthesize_btn.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
self.play_btn = ttk.Button(button_frame, text="播放最后一个", command=self.play_audio, state=tk.DISABLED)
|
||||
self.play_btn.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
self.open_folder_btn = ttk.Button(button_frame, text="打开输出目录", command=self.open_output_folder)
|
||||
self.open_folder_btn.pack(side=tk.LEFT, padx=5)
|
||||
row += 1
|
||||
|
||||
ttk.Separator(main_frame, orient='horizontal').grid(row=row, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=10)
|
||||
row += 1
|
||||
|
||||
ttk.Label(main_frame, text="日志", font=("Arial", 12, "bold")).grid(row=row, column=0, columnspan=2, sticky=tk.W, pady=(0, 10))
|
||||
row += 1
|
||||
|
||||
self.log_text = tk.Text(main_frame, height=8, width=70, state=tk.DISABLED)
|
||||
self.log_text.grid(row=row, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), pady=5)
|
||||
|
||||
log_scrollbar = ttk.Scrollbar(main_frame, orient=tk.VERTICAL, command=self.log_text.yview)
|
||||
log_scrollbar.grid(row=row, column=2, sticky=(tk.N, tk.S))
|
||||
self.log_text['yscrollcommand'] = log_scrollbar.set
|
||||
|
||||
main_frame.rowconfigure(row, weight=1)
|
||||
|
||||
def toggle_config(self):
|
||||
self.config_collapsed = not self.config_collapsed
|
||||
if self.config_collapsed:
|
||||
self.config_frame.grid_remove()
|
||||
self.toggle_btn.config(text="▶")
|
||||
else:
|
||||
self.config_frame.grid()
|
||||
self.toggle_btn.config(text="▼")
|
||||
|
||||
def add_text_entry(self):
|
||||
entry_frame = ttk.Frame(self.text_list_container)
|
||||
entry_row = len(self.text_entries)
|
||||
|
||||
ttk.Label(entry_frame, text=f"#{entry_row + 1}").grid(row=0, column=0, sticky=tk.W, padx=2)
|
||||
|
||||
ttk.Label(entry_frame, text="文件名:").grid(row=0, column=1, sticky=tk.W, padx=2)
|
||||
filename_var = tk.StringVar(value=f"text_{entry_row + 1}")
|
||||
filename_entry = ttk.Entry(entry_frame, textvariable=filename_var, width=15)
|
||||
filename_entry.grid(row=0, column=2, sticky=tk.W, padx=2)
|
||||
|
||||
ttk.Label(entry_frame, text="文本:").grid(row=0, column=3, sticky=tk.W, padx=2)
|
||||
text_var = tk.StringVar(value="")
|
||||
text_entry = ttk.Entry(entry_frame, textvariable=text_var, width=35)
|
||||
text_entry.grid(row=0, column=4, sticky=(tk.W, tk.E), padx=2)
|
||||
|
||||
remove_btn = ttk.Button(entry_frame, text="✕", width=3, command=lambda: self.remove_text_entry(entry_frame))
|
||||
remove_btn.grid(row=0, column=5, sticky=tk.W, padx=2)
|
||||
|
||||
entry_frame.columnconfigure(4, weight=1)
|
||||
entry_frame.grid(row=entry_row, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=3)
|
||||
|
||||
self.text_entries.append({
|
||||
'frame': entry_frame,
|
||||
'filename_var': filename_var,
|
||||
'text_var': text_var
|
||||
})
|
||||
|
||||
def remove_text_entry(self, frame):
|
||||
if len(self.text_entries) <= 1:
|
||||
messagebox.showwarning("提示", "至少保留一个文本条目")
|
||||
return
|
||||
|
||||
for i, entry in enumerate(self.text_entries):
|
||||
if entry['frame'] == frame:
|
||||
frame.destroy()
|
||||
self.text_entries.pop(i)
|
||||
self.reorder_entries()
|
||||
break
|
||||
|
||||
def reorder_entries(self):
|
||||
for i, entry in enumerate(self.text_entries):
|
||||
entry['frame'].grid(row=i, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=3)
|
||||
for widget in entry['frame'].winfo_children():
|
||||
if isinstance(widget, ttk.Label) and widget.cget("text").startswith("#"):
|
||||
widget.config(text=f"#{i + 1}")
|
||||
break
|
||||
|
||||
def log_message(self, message):
|
||||
self.log_text.config(state=tk.NORMAL)
|
||||
self.log_text.insert(tk.END, message + "\n")
|
||||
self.log_text.see(tk.END)
|
||||
self.log_text.config(state=tk.DISABLED)
|
||||
logger.info(message)
|
||||
|
||||
def start_synthesis(self):
|
||||
if self.synthesis_in_progress:
|
||||
messagebox.showwarning("提示", "合成正在进行中,请稍候...")
|
||||
return
|
||||
|
||||
text_list = []
|
||||
for entry in self.text_entries:
|
||||
text = entry['text_var'].get().strip()
|
||||
filename = entry['filename_var'].get().strip()
|
||||
if text:
|
||||
text_list.append({
|
||||
'text': text,
|
||||
'filename': filename or "output"
|
||||
})
|
||||
|
||||
if not text_list:
|
||||
messagebox.showwarning("提示", "请输入要合成的文本")
|
||||
return
|
||||
|
||||
api_key = self.api_key_var.get().strip()
|
||||
group_id = self.group_id_var.get().strip()
|
||||
if not api_key or not group_id:
|
||||
messagebox.showwarning("提示", "请填写 API Key 和 Group ID")
|
||||
return
|
||||
|
||||
self.synthesis_in_progress = True
|
||||
self.synthesize_btn.config(state=tk.DISABLED)
|
||||
self.play_btn.config(state=tk.DISABLED)
|
||||
|
||||
thread = threading.Thread(target=self.run_batch_synthesis, args=(text_list,))
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
def run_batch_synthesis(self, text_list):
|
||||
try:
|
||||
output_dir = Path(self.output_dir_var.get())
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
self.log_message(f"开始批量合成 {len(text_list)} 个文本...")
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
success_count = 0
|
||||
last_file = None
|
||||
|
||||
for i, item in enumerate(text_list):
|
||||
output_file = output_dir / f"{item['filename']}.mp3"
|
||||
self.log_message(f"[{i+1}/{len(text_list)}] 正在合成: {item['text'][:30]}...")
|
||||
|
||||
result = loop.run_until_complete(
|
||||
self.synthesize_speech(
|
||||
text=item['text'],
|
||||
output_file=str(output_file),
|
||||
api_key=self.api_key_var.get(),
|
||||
group_id=self.group_id_var.get(),
|
||||
base_url=self.base_url_var.get(),
|
||||
voice_id=self.voice_id_var.get(),
|
||||
speed=self.speed_var.get(),
|
||||
vol=self.vol_var.get(),
|
||||
pitch=self.pitch_var.get()
|
||||
)
|
||||
)
|
||||
|
||||
if result:
|
||||
success_count += 1
|
||||
last_file = result
|
||||
self.log_message(f" ✓ 成功: {result}")
|
||||
else:
|
||||
self.log_message(f" ✗ 失败")
|
||||
|
||||
loop.close()
|
||||
|
||||
if success_count > 0:
|
||||
self.current_audio_file = last_file
|
||||
self.log_message(f"\n批量合成完成!成功 {success_count}/{len(text_list)} 个")
|
||||
self.root.after(0, lambda: self.play_btn.config(state=tk.NORMAL))
|
||||
self.root.after(0, lambda: messagebox.showinfo(
|
||||
"合成完成",
|
||||
f"批量合成完成!\n成功 {success_count}/{len(text_list)} 个\n输出目录: {output_dir.resolve()}"
|
||||
))
|
||||
else:
|
||||
self.log_message("所有合成都失败了")
|
||||
self.root.after(0, lambda: messagebox.showerror("错误", "所有合成都失败了,请查看日志"))
|
||||
|
||||
except Exception as e:
|
||||
self.log_message(f"批量合成出错: {str(e)}")
|
||||
self.root.after(0, lambda: messagebox.showerror("错误", f"批量合成出错: {str(e)}"))
|
||||
finally:
|
||||
self.synthesis_in_progress = False
|
||||
self.root.after(0, lambda: self.synthesize_btn.config(state=tk.NORMAL))
|
||||
|
||||
async def synthesize_speech(self, text, output_file, api_key, group_id, base_url, voice_id, speed, vol, pitch):
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
payload = {
|
||||
"model": "speech-02-turbo",
|
||||
"text": text,
|
||||
"stream": True,
|
||||
"voice_setting": {
|
||||
"voice_id": voice_id,
|
||||
"speed": speed,
|
||||
"vol": vol,
|
||||
"pitch": pitch
|
||||
},
|
||||
"audio_setting": {
|
||||
"sample_rate": 16000,
|
||||
"bitrate": 32000,
|
||||
"format": "mp3",
|
||||
"channel": 1
|
||||
}
|
||||
}
|
||||
|
||||
url = f"{base_url}?GroupId={group_id}"
|
||||
|
||||
try:
|
||||
audio_buffer = bytearray()
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(url, json=payload, headers=headers) as response:
|
||||
if response.status != 200:
|
||||
error_text = await response.text()
|
||||
self.log_message(f" 请求失败: {response.status}, {error_text}")
|
||||
return None
|
||||
|
||||
async for line in response.content:
|
||||
line = line.strip()
|
||||
if line.startswith(b'data:'):
|
||||
try:
|
||||
json_str = line[5:].decode('utf-8').strip()
|
||||
if not json_str:
|
||||
continue
|
||||
|
||||
data_json = json.loads(json_str)
|
||||
|
||||
if "data" in data_json and "audio" in data_json["data"]:
|
||||
status = data_json["data"].get("status", 1)
|
||||
audio_hex = data_json["data"]["audio"]
|
||||
|
||||
if status == 1:
|
||||
if audio_hex:
|
||||
audio_binary = binascii.unhexlify(audio_hex)
|
||||
audio_buffer.extend(audio_binary)
|
||||
elif status == 2:
|
||||
break
|
||||
except json.JSONDecodeError as e:
|
||||
self.log_message(f" JSON解析失败: {e}")
|
||||
except Exception as e:
|
||||
self.log_message(f" 处理响应出错: {str(e)}")
|
||||
|
||||
if len(audio_buffer) == 0:
|
||||
self.log_message(" 音频缓冲区为空")
|
||||
return None
|
||||
|
||||
async with aiofiles.open(output_file, 'wb') as f:
|
||||
await f.write(audio_buffer)
|
||||
|
||||
return output_file
|
||||
|
||||
except Exception as e:
|
||||
self.log_message(f" 合成失败: {str(e)}")
|
||||
return None
|
||||
|
||||
def play_audio(self):
|
||||
if not self.current_audio_file or not os.path.exists(self.current_audio_file):
|
||||
messagebox.showwarning("提示", "请先合成音频")
|
||||
return
|
||||
|
||||
try:
|
||||
if platform.system() == "Darwin":
|
||||
subprocess.run(["open", self.current_audio_file])
|
||||
elif platform.system() == "Windows":
|
||||
os.startfile(self.current_audio_file)
|
||||
else:
|
||||
subprocess.run(["xdg-open", self.current_audio_file])
|
||||
self.log_message(f"正在播放: {self.current_audio_file}")
|
||||
except Exception as e:
|
||||
messagebox.showerror("错误", f"播放失败: {str(e)}")
|
||||
|
||||
def open_output_folder(self):
|
||||
output_dir = self.output_dir_var.get()
|
||||
if not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
try:
|
||||
if platform.system() == "Darwin":
|
||||
subprocess.run(["open", output_dir])
|
||||
elif platform.system() == "Windows":
|
||||
os.startfile(output_dir)
|
||||
else:
|
||||
subprocess.run(["xdg-open", output_dir])
|
||||
except Exception as e:
|
||||
messagebox.showerror("错误", f"打开目录失败: {str(e)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
root = tk.Tk()
|
||||
app = MiniMaxTTSGUI(root)
|
||||
root.mainloop()
|
||||
260
talkingq-url/test/minimax_tts_more.py
Normal file
260
talkingq-url/test/minimax_tts_more.py
Normal file
@@ -0,0 +1,260 @@
|
||||
import os
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import aiofiles
|
||||
import binascii
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
logger = logging.getLogger("minimax_tts_test")
|
||||
|
||||
# 加载环境变量
|
||||
env_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "minimax.env")
|
||||
if os.path.exists(env_path):
|
||||
load_dotenv(env_path)
|
||||
logger.info(f"已加载环境变量文件: {env_path}")
|
||||
else:
|
||||
logger.warning(f"环境变量文件不存在: {env_path}")
|
||||
|
||||
async def test_minimax_tts():
|
||||
# 获取配置
|
||||
api_key = os.getenv("MINIMAX_API_KEY", "")
|
||||
group_id = os.getenv("MINIMAX_GROUP_ID", "")
|
||||
base_url = os.getenv("MINIMAX_BASE_URL", "https://api.minimax.chat/v1/t2a_v2")
|
||||
voice_id = os.getenv("MINIMAX_VOICE_ID", "cartoon-boy-01")
|
||||
|
||||
if not api_key or not group_id:
|
||||
logger.error("缺少必要的配置: MINIMAX_API_KEY 或 MINIMAX_GROUP_ID")
|
||||
return
|
||||
|
||||
# 定义中英文短语对应表
|
||||
phrases_dict = {
|
||||
# "upgrading": {
|
||||
# "zh": "升级中,请勿断电",
|
||||
# "en": "Upgrading, please do not power off"
|
||||
# },
|
||||
# "upgrade_success": {
|
||||
# "zh": "升级成功,正在重启设备",
|
||||
# "en": "Upgrade successful, restarting device"
|
||||
# },
|
||||
# "upgrade_failed": {
|
||||
# "zh": "升级失败,正在重启设备",
|
||||
# "en": "Upgrade failed, restarting device"
|
||||
# },
|
||||
# "low_energy": {
|
||||
# "zh": "感觉没能量了,罢工",
|
||||
# "en": "Feeling out of energy, going on strike"
|
||||
# },
|
||||
# "network_connected": {
|
||||
# "zh": "连上网络开始上班了",
|
||||
# "en": "Connected to network, starting work"
|
||||
# },
|
||||
# "network_lost": {
|
||||
# "zh": "没网了,下班了",
|
||||
# "en": "No network, time to get off work"
|
||||
# },
|
||||
# "volume_down": {
|
||||
# "zh": "音量已调小一点",
|
||||
# "en": "Volume turned down a bit"
|
||||
# },
|
||||
# "max_volume": {
|
||||
# "zh": "音量已最大",
|
||||
# "en": "Volume is at maximum"
|
||||
# },
|
||||
# "volume_up": {
|
||||
# "zh": "音量已调大一点",
|
||||
# "en": "Volume turned up a bit"
|
||||
# },
|
||||
# "wakeup": {
|
||||
# "zh": "我起床了",
|
||||
# "en": "I'm awake now"
|
||||
# },
|
||||
# "enter_network_config": {
|
||||
# "zh": "进入配网",
|
||||
# "en": "Entering network configuration"
|
||||
# },
|
||||
# "exit_network_config": {
|
||||
# "zh": "退出配网",
|
||||
# "en": "Exiting network configuration"
|
||||
# }
|
||||
# "error_card":{
|
||||
# "zh": "你好,卡片有误,请使用本人卡片收听留言"
|
||||
# },
|
||||
# "no_message":{
|
||||
# "zh": "你好,已没可读留言"
|
||||
# },
|
||||
# "new_message":{
|
||||
# "zh": "您有新的留言请注意查收"
|
||||
# }
|
||||
# "test":{
|
||||
# "zh": "橙子我们今天去动物园玩吧?"
|
||||
# }
|
||||
# "welcome":{
|
||||
# "zh": "你好,小朋友,想要和你的好朋友说些什么呢?"
|
||||
# }
|
||||
# "tts_error":{
|
||||
# "zh": "没听到你的声音喔,本轮对话结束"
|
||||
# },
|
||||
# "message_ok":{
|
||||
# "zh": "留言已收到"
|
||||
# }
|
||||
# "audio_save_fail":{
|
||||
# "zh": "留言已收到,但是无法发送。"
|
||||
# }
|
||||
# "network_ok":{
|
||||
# "zh": "已连上网络,可以按键或刷卡跟我对话喔。"
|
||||
# },
|
||||
# "network_connect":{
|
||||
# "zh": "系统启动中,正在等待网络连接。"
|
||||
# },
|
||||
# "have_rest":{
|
||||
# "zh": "小憩一下,待会儿见。"
|
||||
# }
|
||||
"bind_nfc_ready":{
|
||||
"zh": "现在开始刷卡绑定设备吧。"
|
||||
},
|
||||
"bind_nfc_finish":{
|
||||
"zh": "卡片绑定成功。"
|
||||
}
|
||||
}
|
||||
|
||||
# 创建输出目录
|
||||
assets_dir = os.getenv("ASSETS_DIR", "assets")
|
||||
output_dir = Path(assets_dir) / (voice_id + "_tts")
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
logger.info(f"开始测试MiniMax TTS服务,音色: {voice_id}")
|
||||
|
||||
# 遍历所有短语进行合成
|
||||
for phrase_key, phrases in phrases_dict.items():
|
||||
for lang, text in phrases.items():
|
||||
await synthesize_speech(
|
||||
text=text,
|
||||
language=lang,
|
||||
file_prefix=f"{phrase_key}_{lang}",
|
||||
output_dir=output_dir,
|
||||
api_key=api_key,
|
||||
group_id=group_id,
|
||||
base_url=base_url,
|
||||
voice_id=voice_id
|
||||
)
|
||||
|
||||
async def synthesize_speech(text, language, file_prefix, output_dir, api_key, group_id, base_url, voice_id):
|
||||
"""合成单个语音文件"""
|
||||
output_file = output_dir / f"{file_prefix}.mp3"
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
# 语言映射
|
||||
lang_mapping = {
|
||||
"zh": "Chinese",
|
||||
# "en": "English",
|
||||
# "fr": "French",
|
||||
# "de": "German",
|
||||
# "es": "Spanish",
|
||||
# "yue": "Chinese,Yue"
|
||||
}
|
||||
|
||||
payload = {
|
||||
"model": "speech-02-turbo",
|
||||
"text": text,
|
||||
"stream": True,
|
||||
"voice_setting": {
|
||||
"voice_id": voice_id,
|
||||
"speed": 1.0,
|
||||
"vol": 1.0,
|
||||
"pitch": 0
|
||||
},
|
||||
"audio_setting": {
|
||||
"sample_rate": 16000,
|
||||
"bitrate": 32000,
|
||||
"format": "mp3",
|
||||
"channel": 1
|
||||
},
|
||||
"language_boost": lang_mapping.get(language, "auto")
|
||||
}
|
||||
|
||||
url = f"{base_url}?GroupId={group_id}"
|
||||
|
||||
try:
|
||||
logger.info(f"开始合成语音: [{language}] {text}")
|
||||
|
||||
audio_buffer = bytearray()
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(url, json=payload, headers=headers) as response:
|
||||
if response.status != 200:
|
||||
error_text = await response.text()
|
||||
logger.error(f"MiniMax TTS请求失败: {response.status}, {error_text}")
|
||||
return None
|
||||
|
||||
logger.info("开始接收MiniMax TTS流式响应")
|
||||
|
||||
line_count = 0
|
||||
async for line in response.content:
|
||||
line_count += 1
|
||||
logger.debug(f"收到第{line_count}行原始数据: {line}")
|
||||
|
||||
line = line.strip() # 去除换行符
|
||||
if line.startswith(b'data:'):
|
||||
try:
|
||||
# 解析JSON数据
|
||||
json_str = line[5:].decode('utf-8').strip()
|
||||
if not json_str:
|
||||
logger.debug("空的JSON字符串,跳过")
|
||||
continue
|
||||
|
||||
data_json = json.loads(json_str)
|
||||
|
||||
if "data" in data_json and "audio" in data_json["data"]:
|
||||
status = data_json["data"].get("status", 1)
|
||||
audio_hex = data_json["data"]["audio"]
|
||||
|
||||
if status == 1: # 只处理status=1(合成中)的音频数据,忽略status=2(合成结束)的汇总数据
|
||||
if audio_hex:
|
||||
try:
|
||||
audio_binary = binascii.unhexlify(audio_hex)
|
||||
audio_buffer.extend(audio_binary)
|
||||
except binascii.Error as e:
|
||||
logger.error(f"音频数据解码失败: {e}")
|
||||
elif status == 2: # 合成结束
|
||||
logger.info("MiniMax TTS流式合成完成")
|
||||
break
|
||||
else:
|
||||
logger.warning(f"响应中没有音频数据: {data_json}")
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"JSON解析失败: {e}, 原始数据: {line}")
|
||||
except Exception as e:
|
||||
logger.error(f"处理MiniMax TTS流式响应出错: {str(e)}")
|
||||
else:
|
||||
logger.debug(f"非data行,跳过: {line}")
|
||||
|
||||
|
||||
# 检查音频缓冲区大小
|
||||
if len(audio_buffer) == 0:
|
||||
logger.error("音频缓冲区为空,合成可能失败")
|
||||
return None
|
||||
|
||||
logger.info(f"音频合成完成,总大小: {len(audio_buffer)} 字节")
|
||||
|
||||
# 保存音频文件
|
||||
async with aiofiles.open(output_file, 'wb') as f:
|
||||
await f.write(audio_buffer)
|
||||
|
||||
logger.info(f"TTS合成成功! 保存音频到: {output_file}")
|
||||
return str(output_file)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"MiniMax TTS合成失败: {str(e)}")
|
||||
return None
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(test_minimax_tts())
|
||||
Reference in New Issue
Block a user