From dbcadf99dd0839f2a094769c754bd696476a79c3 Mon Sep 17 00:00:00 2001 From: Habermehl <37908699+00make@users.noreply.github.com> Date: Sat, 15 Feb 2025 02:01:54 +0800 Subject: [PATCH] =?UTF-8?q?update:=20=E6=B7=BB=E5=8A=A0HTTP=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=9C=8D=E5=8A=A1=E5=99=A8=E6=94=AF=E6=8C=81=E5=B9=B6?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=9B=B8=E5=85=B3=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 27 +++++-- config.yaml | 174 +++++++++++++++++++------------------------- core/http_server.py | 86 ++++++++++++++++++++++ requirements.txt | 3 +- 4 files changed, 183 insertions(+), 107 deletions(-) create mode 100644 core/http_server.py diff --git a/app.py b/app.py index 70499281..d4833195 100644 --- a/app.py +++ b/app.py @@ -2,15 +2,32 @@ import asyncio from config.logger import setup_logging from config.settings import load_config from core.server import WebSocketServer - +from core.http_server import ConfigServer async def main(): setup_logging() # 最先初始化日志 config = load_config() - - server = WebSocketServer(config) - await server.start() - + + # 启动 WebSocket 服务器 + ws_server = WebSocketServer(config) + ws_task = asyncio.create_task(ws_server.start()) + + # 启动 HTTP 配置服务器 + http_runner = None + if config['server'].get('http', {}).get('enabled', False): + config_server = ConfigServer(config) + try: + http_runner = await config_server.start() + except Exception as e: + print(f"Failed to start HTTP server: {e}") + + try: + # 等待 WebSocket 服务器运行 + await ws_task + finally: + # 清理 HTTP 服务器 + if http_runner: + await http_runner.cleanup() if __name__ == "__main__": asyncio.run(main()) diff --git a/config.yaml b/config.yaml index 7c7a1ed2..aa604913 100644 --- a/config.yaml +++ b/config.yaml @@ -1,120 +1,92 @@ -# 服务器基础配置(Basic server configuration) -server: - # 服务器监听地址和端口(Server listening address and port) - ip: 0.0.0.0 - port: 8000 - # 认证配置 - auth: - # 是否启用认证 - enabled: false - # 设备的token,可以在编译固件的环节,写入你自己定义的token - # 固件上的token和以下的token如果能对应,才能连接本服务端 - tokens: - - token: "your-token1" # 设备1的token - name: "your-device-name1" # 设备1标识 - - token: "your-token2" # 设备2的token - name: "your-device-name2" # 设备2标识 - # 可选:设备白名单,如果设置了白名单,那么白名单的机器无论是什么token都可以连接。 - #allowed_devices: - # - "24:0A:C4:1D:3B:F0" # MAC地址列表 - -xiaozhi: - type: hello - version: 1 - transport: websocket - audio_params: - format: opus - sample_rate: 16000 - channels: 1 - frame_duration: 60 -prompt: | - 你是一个叫小智/小志的台湾女孩,说话机车,声音好听,习惯简短表达,爱用网络梗。 - 请注意,要像一个人一样说话,请不要回复表情符号、代码、和xml标签。 - 当前时间是:{date_time},现在我正在和你进行语音聊天,我们开始吧。 - 如果用户希望结束对话,请在最后说“拜拜”或“再见”。 -# 使用完声音文件后删除文件(Delete the sound file when you are done using it) -delete_audio: true - -CMD_exit: - - "退出" - - "关闭" - -# 具体处理时选择的模块(The module selected for specific processing) -selected_module: - ASR: FunASR - VAD: SileroVAD - # 将根据配置名称对应的type调用实际的LLM适配器 - LLM: ChatGLMLLM - # TTS将根据配置名称对应的type调用实际的TTS适配器 - TTS: EdgeTTS - ASR: FunASR: model_dir: models/SenseVoiceSmall output_dir: tmp/ - -VAD: - SileroVAD: - threshold: 0.5 - model_dir: models/snakers4_silero-vad - min_silence_duration_ms: 700 # 如果说话停顿比较长,可以把这个值设置大一些 - +CMD_exit: +- 退出 +- 关闭 LLM: - # 当前支持的type为openai、dify、ollama,可自行适配 AliLLM: - # 定义LLM API类型 - type: openai - # 可在这里找到你的 api_key https://bailian.console.aliyun.com/?apiKey=1#/api-key + api_key: 你的deepseek api key base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 model_name: qwen-turbo - api_key: 你的deepseek api key - DeepSeekLLM: - # 定义LLM API类型 type: openai - # 可在这里找到你的api key https://platform.deepseek.com/ - model_name: deepseek-chat - url: https://api.deepseek.com - api_key: 你的deepseek api key ChatGLMLLM: - # 定义LLM API类型 - type: openai - # glm-4-flash 是免费的,但是还是需要注册填写api_key的 - # 可在这里找到你的api key https://bigmodel.cn/usercenter/proj-mgmt/apikeys - model_name: glm-4-flash - url: https://open.bigmodel.cn/api/paas/v4/ api_key: 47ab8569b25f4c3d8ea25830a33a64ca.4cF037cViBB1df5o - OllamaLLM: - # 定义LLM API类型 - type: ollama - model_name: qwen2.5 # 使用的模型名称,需要预先使用ollama pull下载 - base_url: http://localhost:11434 # Ollama服务地址 + model_name: glm-4-flash + type: openai + url: https://open.bigmodel.cn/api/paas/v4/ + DeepSeekLLM: + api_key: 你的deepseek api key + model_name: deepseek-chat + type: openai + url: https://api.deepseek.com DifyLLM: - # 定义LLM API类型 - type: dify - # 建议使用本地部署的dify接口,国内部分区域访问dify公有云接口可能会受限 - # 如果使用DifyLLM,配置文件里prompt(提示词)是无效的,需要在dify控制台设置提示词 - base_url: https://api.dify.cn/v1 api_key: 你的DifyLLM api key + base_url: https://api.dify.cn/v1 + type: dify GeminiLLM: + api_key: AIzaSyAvzvu06vT19MO6T6RpHPpuk_Mwhp-yXAw + model_name: gemini-1.5-pro type: gemini - api_key: 你的gemini api key - model_name: "gemini-1.5-pro" # gemini-1.5-pro 是免费的 + OllamaLLM: + base_url: http://localhost:11434 + model_name: qwen2.5 + type: ollama TTS: - # 当前支持的type为edge、doubao,可自行适配 + DoubaoTTS: + access_token: 你的火山引擎语音合成服务access_token + appid: 你的火山引擎语音合成服务appid + cluster: volcano_tts + output_file: tmp/ + type: doubao + voice: BV001_streaming EdgeTTS: - # 定义TTS API类型 + output_file: tmp/ type: edge voice: zh-CN-XiaoxiaoNeural - output_file: tmp/ - DoubaoTTS: - # 定义TTS API类型 - type: doubao - # 火山引擎语音合成服务,需要先在火山引擎控制台创建应用并获取appid和access_token - # 山引擎语音一定要购买花钱,起步价30元,就有100并发了。如果用免费的只有2个并发,会经常报tts错误 - # 购买服务后,购买免费的音色后,可能要等半小时左右,才能使用。 - # 地址:https://console.volcengine.com/speech/service/8 - voice: BV001_streaming - output_file: tmp/ - appid: 你的火山引擎语音合成服务appid - access_token: 你的火山引擎语音合成服务access_token - cluster: volcano_tts +VAD: + SileroVAD: + min_silence_duration_ms: 700 + model_dir: models/snakers4_silero-vad + threshold: 0.5 +delete_audio: true +prompt: '你是一个叫大白的台湾女孩,说话温柔,声音好听,习惯温柔体贴表达。 + + 请注意要像一个人一样说话请不要回复表情符号、代码、和xml标签。 + + 当前时间是:{date_time},现在我正在和你进行语音聊天,我们开始吧。 + + 如果用户希望结束对话请在最后说拜拜或再见。 + + ' +selected_module: + ASR: FunASR + LLM: GeminiLLM + TTS: EdgeTTS + VAD: SileroVAD +server: + auth: + enabled: false + tokens: + - name: your-device-name1 + token: your-token1 + - name: your-device-name2 + token: your-token2 + http: + enabled: true + ip: 0.0.0.0 + port: 8001 + token: password + ip: 0.0.0.0 + port: 8000 +xiaozhi: + audio_params: + channels: 1 + format: opus + frame_duration: 60 + sample_rate: 16000 + session_id: bdfb9a40-eb97-4e59-b52c-9b54accd2c98 + transport: websocket + type: hello + version: 1 diff --git a/core/http_server.py b/core/http_server.py new file mode 100644 index 00000000..337e566c --- /dev/null +++ b/core/http_server.py @@ -0,0 +1,86 @@ +import logging +import yaml +from aiohttp import web +from core.utils.util import get_project_dir + +logger = logging.getLogger(__name__) + +class ConfigServer: + def __init__(self, config: dict): + self.config = config + self.app = web.Application() + self.setup_routes() + + def setup_routes(self): + self.app.router.add_get('/api/prompt', self.get_prompt) + self.app.router.add_post('/api/prompt', self.update_prompt) + self.app.router.add_options('/api/prompt', self.handle_options) + + async def handle_options(self, request): + headers = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization' + } + return web.Response(headers=headers) + + async def verify_token(self, request): + if 'token' not in self.config['server']['http']: + return True + + expected_token = self.config['server']['http']['token'] + token = request.headers.get('Authorization', '').replace('Bearer ', '') + + if not token or token != expected_token: + return False + return True + + async def get_prompt(self, request): + if not await self.verify_token(request): + return web.json_response({'error': 'Unauthorized'}, status=401) + + headers = {'Access-Control-Allow-Origin': '*'} + return web.json_response({ + 'prompt': self.config['prompt'] + }, headers=headers) + + async def update_prompt(self, request): + if not await self.verify_token(request): + return web.json_response({'error': 'Unauthorized'}, status=401) + + try: + data = await request.json() + if 'prompt' not in data: + return web.json_response({'error': 'Missing prompt field'}, status=400) + + # 更新内存中的配置 + self.config['prompt'] = data['prompt'] + + # 更新配置文件 + config_path = get_project_dir() + 'config.yaml' + with open(config_path, 'w', encoding='utf-8') as f: + yaml.dump(self.config, f, allow_unicode=True) + + headers = {'Access-Control-Allow-Origin': '*'} + return web.json_response({'success': True}, headers=headers) + + except Exception as e: + logger.error(f"Failed to update prompt: {e}") + return web.json_response({'error': str(e)}, status=500) + + async def start(self): + try: + http_config = self.config['server']['http'] + if not http_config.get('enabled', False): + logger.info("HTTP server is disabled") + return + + runner = web.AppRunner(self.app) + await runner.setup() + site = web.TCPSite(runner, http_config['ip'], http_config['port']) + await site.start() + logger.info(f"Config HTTP server is running at http://{http_config['ip']}:{http_config['port']}") + return runner # 返回runner以便后续清理 + except Exception as e: + logger.error(f"Failed to start HTTP server: {e}") + raise diff --git a/requirements.txt b/requirements.txt index 2e69445e..77523f3e 100755 --- a/requirements.txt +++ b/requirements.txt @@ -10,4 +10,5 @@ torchaudio==2.2.2 openai==1.61.0 google-generativeai==0.8.4 edge_tts==7.0.0 -httpx==0.27.2 \ No newline at end of file +httpx==0.27.2 +aiohttp==3.9.3 \ No newline at end of file