From 08e57936fcff8b26e5e3d4722c23c4a4f840addd Mon Sep 17 00:00:00 2001 From: Jad Date: Mon, 17 Mar 2025 13:45:49 +0800 Subject: [PATCH 01/17] =?UTF-8?q?feat:=20ASR=E5=A2=9E=E5=8A=A0sherpa-onnx?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=20#315=20(#379)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 +- README.md | 1 + README_en.md | 1 + main/xiaozhi-server/config.yaml | 4 + .../core/providers/asr/sherpa_onnx_local.py | 164 ++++++++++++++++++ main/xiaozhi-server/requirements.txt | 4 +- 6 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 main/xiaozhi-server/core/providers/asr/sherpa_onnx_local.py diff --git a/.gitignore b/.gitignore index 698d638c..a25b3401 100644 --- a/.gitignore +++ b/.gitignore @@ -141,8 +141,8 @@ music/ # Cython debug symbols cython_debug/ *.iml -model.pt tmp +.history .DS_Store main/xiaozhi-server/data main/manager-web/node_modules @@ -151,3 +151,6 @@ main/manager-web/node_modules .private_config.yaml .env.development +# model files +main/xiaozhi-server/models/SenseVoiceSmall/model.pt +main/xiaozhi-server/models/sherpa-onnx* diff --git a/README.md b/README.md index 121dfbe6..efc45101 100644 --- a/README.md +++ b/README.md @@ -208,6 +208,7 @@ server: | 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 | |:---:|:---------:|:----:|:----:|:--:| | ASR | FunASR | 本地使用 | 免费 | | +| ASR | SherpaASR | 本地使用 | 免费 | | | ASR | DoubaoASR | 接口调用 | 收费 | | --- diff --git a/README_en.md b/README_en.md index 8c251a44..88306675 100644 --- a/README_en.md +++ b/README_en.md @@ -165,6 +165,7 @@ In fact, any LLM that supports OpenAI API calls can be integrated. | Type | Platform Name | Usage Method | Pricing Model | Remarks | |:----:|:-------------------:|:------------:|:-------------:|:-------:| | ASR | FunASR | Local | Free | | +| ASR | SherpaASR | Local | Free | | | ASR | DoubaoASR | API call | Paid | | --- diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index 7fa8a24a..31210e9e 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -120,6 +120,10 @@ ASR: type: fun_local model_dir: models/SenseVoiceSmall output_dir: tmp/ + SherpaASR: + type: sherpa_onnx_local + model_dir: models/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17 + output_dir: tmp/ DoubaoASR: type: doubao appid: 你的火山引擎语音合成服务appid diff --git a/main/xiaozhi-server/core/providers/asr/sherpa_onnx_local.py b/main/xiaozhi-server/core/providers/asr/sherpa_onnx_local.py new file mode 100644 index 00000000..3dfa3162 --- /dev/null +++ b/main/xiaozhi-server/core/providers/asr/sherpa_onnx_local.py @@ -0,0 +1,164 @@ +import time +import wave +import os +import sys +import io +from config.logger import setup_logging +from typing import Optional, Tuple, List +import uuid +import opuslib_next +from core.providers.asr.base import ASRProviderBase + +import numpy as np +import sherpa_onnx + +from modelscope.hub.file_download import model_file_download + +TAG = __name__ +logger = setup_logging() + + +# 捕获标准输出 +class CaptureOutput: + def __enter__(self): + self._output = io.StringIO() + self._original_stdout = sys.stdout + sys.stdout = self._output + + def __exit__(self, exc_type, exc_value, traceback): + sys.stdout = self._original_stdout + self.output = self._output.getvalue() + self._output.close() + + # 将捕获到的内容通过 logger 输出 + if self.output: + logger.bind(tag=TAG).info(self.output.strip()) + + +class ASRProvider(ASRProviderBase): + def __init__(self, config: dict, delete_audio_file: bool): + self.model_dir = config.get("model_dir") + self.output_dir = config.get("output_dir") + self.delete_audio_file = delete_audio_file + + # 确保输出目录存在 + os.makedirs(self.output_dir, exist_ok=True) + + # 初始化模型文件路径 + model_files = { + "model.int8.onnx": os.path.join(self.model_dir, "model.int8.onnx"), + "tokens.txt": os.path.join(self.model_dir, "tokens.txt") + } + + # 下载并检查模型文件 + try: + for file_name, file_path in model_files.items(): + if not os.path.isfile(file_path): + logger.bind(tag=TAG).info(f"正在下载模型文件: {file_name}") + model_file_download( + model_id="pengzhendong/sherpa-onnx-sense-voice-zh-en-ja-ko-yue", + file_path=file_name, + local_dir=self.model_dir + ) + + if not os.path.isfile(file_path): + raise FileNotFoundError(f"模型文件下载失败: {file_path}") + + self.model_path = model_files["model.int8.onnx"] + self.tokens_path = model_files["tokens.txt"] + + except Exception as e: + logger.bind(tag=TAG).error(f"模型文件处理失败: {str(e)}") + raise + + with CaptureOutput(): + self.model = sherpa_onnx.OfflineRecognizer.from_sense_voice( + model=self.model_path, + tokens=self.tokens_path, + num_threads=2, + sample_rate=16000, + feature_dim=80, + decoding_method="greedy_search", + debug=False, + use_itn=True, + ) + + def save_audio_to_file(self, opus_data: List[bytes], session_id: str) -> str: + """将Opus音频数据解码并保存为WAV文件""" + file_name = f"asr_{session_id}_{uuid.uuid4()}.wav" + file_path = os.path.join(self.output_dir, file_name) + + decoder = opuslib_next.Decoder(16000, 1) # 16kHz, 单声道 + pcm_data = [] + + for opus_packet in opus_data: + try: + pcm_frame = decoder.decode(opus_packet, 960) # 960 samples = 60ms + pcm_data.append(pcm_frame) + except opuslib_next.OpusError as e: + logger.bind(tag=TAG).error(f"Opus解码错误: {e}", exc_info=True) + + with wave.open(file_path, "wb") as wf: + wf.setnchannels(1) + wf.setsampwidth(2) # 2 bytes = 16-bit + wf.setframerate(16000) + wf.writeframes(b"".join(pcm_data)) + + return file_path + + def read_wave(self, wave_filename: str) -> Tuple[np.ndarray, int]: + """ + Args: + wave_filename: + Path to a wave file. It should be single channel and each sample should + be 16-bit. Its sample rate does not need to be 16kHz. + Returns: + Return a tuple containing: + - A 1-D array of dtype np.float32 containing the samples, which are + normalized to the range [-1, 1]. + - sample rate of the wave file + """ + + with wave.open(wave_filename) as f: + assert f.getnchannels() == 1, f.getnchannels() + assert f.getsampwidth() == 2, f.getsampwidth() # it is in bytes + num_samples = f.getnframes() + samples = f.readframes(num_samples) + samples_int16 = np.frombuffer(samples, dtype=np.int16) + samples_float32 = samples_int16.astype(np.float32) + + samples_float32 = samples_float32 / 32768 + return samples_float32, f.getframerate() + + async def speech_to_text(self, opus_data: List[bytes], session_id: str) -> Tuple[Optional[str], Optional[str]]: + """语音转文本主处理逻辑""" + file_path = None + try: + # 保存音频文件 + start_time = time.time() + file_path = self.save_audio_to_file(opus_data, session_id) + logger.bind(tag=TAG).debug(f"音频文件保存耗时: {time.time() - start_time:.3f}s | 路径: {file_path}") + + # 语音识别 + start_time = time.time() + s = self.model.create_stream() + samples, sample_rate = self.read_wave(file_path) + s.accept_waveform(sample_rate, samples) + self.model.decode_stream(s) + text = s.result.text + logger.bind(tag=TAG).debug(f"语音识别耗时: {time.time() - start_time:.3f}s | 结果: {text}") + + return text, file_path + + except Exception as e: + logger.bind(tag=TAG).error(f"语音识别失败: {e}", exc_info=True) + return "", None + + finally: + # 文件清理逻辑 + if self.delete_audio_file and file_path and os.path.exists(file_path): + try: + os.remove(file_path) + logger.bind(tag=TAG).debug(f"已删除临时音频文件: {file_path}") + except Exception as e: + logger.bind(tag=TAG).error(f"文件删除失败: {file_path} | 错误: {e}") diff --git a/main/xiaozhi-server/requirements.txt b/main/xiaozhi-server/requirements.txt index bcd6b4ee..58362e02 100755 --- a/main/xiaozhi-server/requirements.txt +++ b/main/xiaozhi-server/requirements.txt @@ -19,4 +19,6 @@ loguru==0.7.3 requests==2.32.3 cozepy==0.12.0 mem0ai==0.1.62 -bs4==0.0.2 \ No newline at end of file +bs4==0.0.2 +modelscope==1.23.2 +sherpa_onnx==1.11.0 \ No newline at end of file From 2c1fc30bfb2b562fea63b77ae152b99dc43ffc08 Mon Sep 17 00:00:00 2001 From: aileenfun Date: Tue, 18 Mar 2025 00:53:03 +0800 Subject: [PATCH 02/17] =?UTF-8?q?gemini=E5=8F=AF=E4=BB=A5=E5=8D=95?= =?UTF-8?q?=E7=8B=AC=E8=B5=B0=E4=BB=A3=E7=90=86=EF=BC=8C=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E5=85=A8=E5=B1=80=E4=BB=A3=E7=90=86=E3=80=82=20=E5=85=A8?= =?UTF-8?q?=E5=B1=80=E4=BB=A3=E7=90=86=E5=AE=B9=E6=98=93=E8=AE=A9=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E4=B8=AD=E5=85=B6=E4=BB=96=E6=A8=A1=E5=9D=97=E4=B9=9F?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E4=BB=A3=E7=90=86=EF=BC=8C=E9=80=A0=E6=88=90?= =?UTF-8?q?=E6=97=A0=E6=B3=95=E8=AE=BF=E9=97=AE=E7=9A=84=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E3=80=82=20=E6=B5=8B=E8=AF=95=E6=9C=80=E6=96=B0=E7=9A=84=20gem?= =?UTF-8?q?ini=20flash=202.0=E5=8F=AF=E4=BB=A5=E4=BD=BF=E7=94=A8=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/config.yaml | 4 +- .../core/providers/llm/gemini/gemini.py | 81 +++++++++++++------ 2 files changed, 59 insertions(+), 26 deletions(-) diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index 21f37c5b..cc2545e3 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -193,7 +193,9 @@ LLM: # token申请地址: https://aistudio.google.com/apikey # 若部署地无法访问接口,需要开启科学上网 api_key: 你的gemini web key - model_name: "gemini-1.5-pro" # gemini-1.5-pro 是免费的 + model_name: "gemini-2.0-flash" + http_proxy: "" #"http://127.0.0.1:10808" + https_proxy: "" #http://127.0.0.1:10808" CozeLLM: # 定义LLM API类型 type: coze diff --git a/main/xiaozhi-server/core/providers/llm/gemini/gemini.py b/main/xiaozhi-server/core/providers/llm/gemini/gemini.py index f753ac3e..e559bd8e 100644 --- a/main/xiaozhi-server/core/providers/llm/gemini/gemini.py +++ b/main/xiaozhi-server/core/providers/llm/gemini/gemini.py @@ -1,14 +1,19 @@ import google.generativeai as genai from core.utils.util import check_model_key from core.providers.llm.base import LLMProviderBase - +from config.logger import setup_logging +import requests +import json +TAG = __name__ +logger = setup_logging() class LLMProvider(LLMProviderBase): def __init__(self, config): """初始化Gemini LLM Provider""" self.model_name = config.get("model_name", "gemini-1.5-pro") self.api_key = config.get("api_key") - + self.http_proxy=config.get("http_proxy") + self.https_proxy = config.get("https_proxy") have_key = check_model_key("LLM", self.api_key) if not have_key: @@ -16,6 +21,19 @@ class LLMProvider(LLMProviderBase): try: # 初始化Gemini客户端 + # 配置代理(如果提供了代理配置) + self.proxies=None + if self.http_proxy is not "" or self.https_proxy is not "": + + self.proxies = { + "http": self.http_proxy, + "https": self.https_proxy, + } + logger.bind(tag=TAG).info(f"Gemini set proxys:{self.proxies}") + # 使用猴子补丁修改 google-generativeai 库的请求会话 + + # 使用 session 对象配置 genai + genai.configure(api_key=self.api_key) self.model = genai.GenerativeModel(self.model_name) @@ -46,35 +64,48 @@ class LLMProvider(LLMProviderBase): if content: chat_history.append({ "role": role, - "parts": [content] + "parts": [{"text":content}] + }) # 获取当前消息 current_msg = dialogue[-1]["content"] - # 创建新的聊天会话 - chat = self.model.start_chat(history=chat_history) + # 构建请求体 + request_body = { + "contents": chat_history + [{"role": "user", "parts": [{"text":current_msg}]}], + "generationConfig": self.generation_config + } - # 发送消息并获取流式响应 - response = chat.send_message( - current_msg, - stream=True, - generation_config=self.generation_config - ) + # 构建请求URL + url = f"https://generativelanguage.googleapis.com/v1beta/models/{self.model_name}:generateContent?key={self.api_key}" - # 处理流式响应 - for chunk in response: - if hasattr(chunk, 'text') and chunk.text: - yield chunk.text + # 构建请求头 + headers = { + "Content-Type": "application/json", + } - except Exception as e: - error_msg = str(e) - logger.bind(tag=TAG).error(f"Gemini响应生成错误: {error_msg}") - - # 针对不同错误返回友好提示 - if "Rate limit" in error_msg: - yield "【Gemini服务请求太频繁,请稍后再试】" - elif "Invalid API key" in error_msg: - yield "【Gemini API key无效】" + # 发送POST请求,经测试手动 request 无法使用 stream 模式,但是文本消息其实也很快,stream 与否也无所谓 + if self.proxies: + response = requests.post(url, headers=headers, json=request_body, stream=False, proxies=self.proxies) else: - yield f"【Gemini服务响应异常: {error_msg}】" + response = requests.post(url, headers=headers, json=request_body, stream=False) + response.raise_for_status() + try: + data = response.json() # 直接解析JSON + if 'candidates' in data and data['candidates']: + yield data['candidates'][0]['content']['parts'][0]['text'] + else: + yield "未找到候选回复。" + except json.JSONDecodeError as e: + yield f"JSON解码错误:{e}" + except Exception as e: + yield f"发生错误:{e}" + + + except requests.exceptions.RequestException as e: + yield f"请求失败:{e}" + except json.JSONDecodeError as e: + yield f"JSON解码错误:{e}" + except Exception as e: + yield f"发生错误:{e}" From 3d97fab16d94a3374eac3c64b0baa888f9102b5a Mon Sep 17 00:00:00 2001 From: HaoDuoYu <63943547+diaoling6665@users.noreply.github.com> Date: Tue, 18 Mar 2025 12:05:33 +0800 Subject: [PATCH 03/17] Add files via upload --- .../core/providers/llm/openai/openai.py | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/main/xiaozhi-server/core/providers/llm/openai/openai.py b/main/xiaozhi-server/core/providers/llm/openai/openai.py index 95cd85f9..2feb4847 100644 --- a/main/xiaozhi-server/core/providers/llm/openai/openai.py +++ b/main/xiaozhi-server/core/providers/llm/openai/openai.py @@ -7,19 +7,27 @@ class LLMProvider(LLMProviderBase): def __init__(self, config): self.model_name = config.get("model_name") self.api_key = config.get("api_key") - if 'base_url' in config: - self.base_url = config.get("base_url") - else: - self.base_url = config.get("url") + self.url = config.get("url") + self.top_p = config.get("top_p") + self.top_k = config.get("top_k") + self.temperature = config.get("temperature") + self.max_tokens = config.get("max_tokens") + self.frequency_penalty = config.get("frequency_penalty") + check_model_key("LLM", self.api_key) - self.client = openai.OpenAI(api_key=self.api_key, base_url=self.base_url) + self.client = openai.OpenAI(api_key=self.api_key, base_url=self.url) def response(self, session_id, dialogue): try: responses = self.client.chat.completions.create( model=self.model_name, messages=dialogue, - stream=True + stream=True, + temperature=self.temperature, + max_tokens=self.max_tokens, + top_p=self.top_p, + top_k=self.top_k, + frequency_penalty=self.frequency_penalty ) is_active = True @@ -51,6 +59,11 @@ class LLMProvider(LLMProviderBase): messages=dialogue, stream=True, tools=functions, + temperature=self.temperature, + max_tokens=self.max_tokens, + top_p=self.top_p, + top_k=self.top_k, + frequency_penalty=self.frequency_penalty ) for chunk in stream: From f5d9b478d57c4c4b07e428e052a3bc2d40ffb0cd Mon Sep 17 00:00:00 2001 From: HaoDuoYu <63943547+diaoling6665@users.noreply.github.com> Date: Tue, 18 Mar 2025 12:07:53 +0800 Subject: [PATCH 04/17] Update config.yaml --- main/xiaozhi-server/config.yaml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index 21f37c5b..fcae7485 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -141,6 +141,8 @@ VAD: min_silence_duration_ms: 700 # 如果说话停顿比较长,可以把这个值设置大一些 LLM: + #所有openai类型均可以修改超参,以AliLLM为例 + # 当前支持的type为openai、dify、ollama,可自行适配 AliLLM: # 定义LLM API类型 @@ -149,6 +151,11 @@ LLM: base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 model_name: qwen-turbo api_key: 你的deepseek web key + temperature: 0.7 # 温度值 + max_tokens: 500 # 最大生成token数 + top_p: 1 + top_k: 50 + frequency_penalty: 0 # 频率惩罚 DoubaoLLM: # 定义LLM API类型 type: openai @@ -469,4 +476,4 @@ manager: enabled: false ip: 0.0.0.0 port: 8002 -use_private_config: false \ No newline at end of file +use_private_config: false From e58da80f8e097664c989a0fa44584d023fedb552 Mon Sep 17 00:00:00 2001 From: Xvsenfeng <1458612070@qq.com> Date: Tue, 18 Mar 2025 15:58:31 +0800 Subject: [PATCH 05/17] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E6=B5=81=E5=92=8C=E6=96=87=E6=9C=AC=E7=94=9F=E6=88=90=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/config.yaml | 4 ++ .../core/providers/llm/dify/dify.py | 61 ++++++++++++++----- 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index 21f37c5b..89a6f7f9 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -186,6 +186,10 @@ LLM: # 如果使用DifyLLM,配置文件里prompt(提示词)是无效的,需要在dify控制台设置提示词 base_url: https://api.dify.cn/v1 api_key: 你的DifyLLM web key + # 使用的对话模式 可以选择工作流 workflows/run 对话模式 chat-messages 文本生成 completion-messages + # 使用workflows进行返回的时候输入参数为 query 返回参数的名字要设置为 answer + # 文本生成的默认输入参数也是query + mode: workflows/run GeminiLLM: type: gemini # 谷歌Gemini API,需要先在Google Cloud控制台创建API密钥并获取api_key diff --git a/main/xiaozhi-server/core/providers/llm/dify/dify.py b/main/xiaozhi-server/core/providers/llm/dify/dify.py index f3c62639..02ef5588 100644 --- a/main/xiaozhi-server/core/providers/llm/dify/dify.py +++ b/main/xiaozhi-server/core/providers/llm/dify/dify.py @@ -9,6 +9,7 @@ logger = setup_logging() class LLMProvider(LLMProviderBase): def __init__(self, config): self.api_key = config["api_key"] + self.mode = config.get("mode", "workflows/run") self.base_url = config.get("base_url", "https://api.dify.ai/v1").rstrip('/') self.session_conversation_map = {} # 存储session_id和conversation_id的映射 @@ -19,27 +20,59 @@ class LLMProvider(LLMProviderBase): conversation_id = self.session_conversation_map.get(session_id) # 发起流式请求 - with requests.post( - f"{self.base_url}/chat-messages", - headers={"Authorization": f"Bearer {self.api_key}"}, - json={ + if self.mode == "chat-messages": + request_json = { "query": last_msg["content"], "response_mode": "streaming", "user": session_id, "inputs": {}, "conversation_id": conversation_id - }, + } + elif self.mode == "workflows/run": + request_json = { + "inputs": {"query": last_msg["content"]}, + "response_mode": "streaming", + "user": session_id + } + elif self.mode == "completion-messages": + request_json = { + "inputs": {"query": last_msg["content"]}, + "response_mode": "streaming", + "user": session_id + } + + with requests.post( + f"{self.base_url}/{self.mode}", + headers={"Authorization": f"Bearer {self.api_key}"}, + json=request_json, stream=True ) as r: - for line in r.iter_lines(): - if line.startswith(b'data: '): - event = json.loads(line[6:]) - # 如果没有找到conversation_id,则获取此次conversation_id - if not conversation_id: - conversation_id = event.get('conversation_id') - self.session_conversation_map[session_id] = conversation_id # 更新映射 - if event.get('answer'): - yield event['answer'] + if self.mode == "chat-messages": + for line in r.iter_lines(): + if line.startswith(b'data: '): + event = json.loads(line[6:]) + # 如果没有找到conversation_id,则获取此次conversation_id + if not conversation_id: + conversation_id = event.get('conversation_id') + self.session_conversation_map[session_id] = conversation_id # 更新映射 + if event.get('answer'): + yield event['answer'] + elif self.mode == "workflows/run": + for line in r.iter_lines(): + # logger.bind(tag=TAG).info(f"chat message response: {line}") + if line.startswith(b'data: '): + event = json.loads(line[6:]) + if event.get('event') == "workflow_finished": + if event['data']['status'] == "succeeded": + yield event['data']['outputs']['answer'] + else: + yield "【服务响应异常】" + elif self.mode == "completion-messages": + for line in r.iter_lines(): + if line.startswith(b'data: '): + event = json.loads(line[6:]) + if event.get('answer'): + yield event['answer'] except Exception as e: logger.bind(tag=TAG).error(f"Error in response generation: {e}") From 34fa65132d8b13e651b668093faf6d210a74477a Mon Sep 17 00:00:00 2001 From: aileenfun Date: Tue, 18 Mar 2025 21:04:21 +0800 Subject: [PATCH 06/17] =?UTF-8?q?gemini=E7=8B=AC=E7=AB=8B=E4=BB=A3?= =?UTF-8?q?=E7=90=86=E6=A8=A1=E5=BC=8F=20=E5=9C=A8=E4=BB=A3=E7=90=86?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E4=B8=8B=E6=89=8B=E5=8A=A8=E7=BB=84=E6=88=90?= =?UTF-8?q?request=EF=BC=8C=E6=97=A0=E8=AE=BA=20Steam=20=E6=98=AF=E5=90=A6?= =?UTF-8?q?=E4=B8=BA=20True=EF=BC=8C=E8=BF=94=E5=9B=9E=E7=9A=84=E5=9D=87?= =?UTF-8?q?=E6=98=AF=E9=9D=9Esteam=E5=80=BC=E3=80=82=20=E4=B8=8D=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E4=BB=A3=E7=90=86=E6=97=B6=E8=87=AA=E5=8A=A8=E5=88=87?= =?UTF-8?q?=E6=8D=A2=E4=B8=BAgoogle=E7=9A=84=E5=AE=98=E6=96=B9api=EF=BC=8C?= =?UTF-8?q?=E8=B5=B0=20stream=E6=A8=A1=E5=BC=8F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 全局代理容易让系统中其他模块也使用代理,造成无法访问的问题。 测试最新的 gemini flash 2.0可以使用。 --- .../core/providers/llm/gemini/gemini.py | 52 ++++++++++++++----- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/main/xiaozhi-server/core/providers/llm/gemini/gemini.py b/main/xiaozhi-server/core/providers/llm/gemini/gemini.py index e559bd8e..298bf71e 100644 --- a/main/xiaozhi-server/core/providers/llm/gemini/gemini.py +++ b/main/xiaozhi-server/core/providers/llm/gemini/gemini.py @@ -85,22 +85,48 @@ class LLMProvider(LLMProviderBase): "Content-Type": "application/json", } - # 发送POST请求,经测试手动 request 无法使用 stream 模式,但是文本消息其实也很快,stream 与否也无所谓 + # 发送POST请求,经测试手动 request 无法使用 stream 模式 if self.proxies: + logger.bind(tag=TAG).info(f"Gemini response mode ") response = requests.post(url, headers=headers, json=request_body, stream=False, proxies=self.proxies) + try: + data = response.json() # 直接解析JSON + if 'candidates' in data and data['candidates']: + yield data['candidates'][0]['content']['parts'][0]['text'] + else: + yield "未找到候选回复。" + except json.JSONDecodeError as e: + yield f"JSON解码错误:{e}" + except Exception as e: + yield f"发生错误:{e}" else: - response = requests.post(url, headers=headers, json=request_body, stream=False) - response.raise_for_status() - try: - data = response.json() # 直接解析JSON - if 'candidates' in data and data['candidates']: - yield data['candidates'][0]['content']['parts'][0]['text'] - else: - yield "未找到候选回复。" - except json.JSONDecodeError as e: - yield f"JSON解码错误:{e}" - except Exception as e: - yield f"发生错误:{e}" + logger.bind(tag=TAG).info(f"Gemini stream mode ") + chat = self.model.start_chat(history=chat_history) + + # 发送消息并获取流式响应 + response = chat.send_message( + current_msg, + stream=True, + generation_config=self.generation_config + ) + # 处理流式响应 + for chunk in response: + if hasattr(chunk, 'text') and chunk.text: + yield chunk.text + + except Exception as e: + error_msg = str(e) + logger.bind(tag=TAG).error(f"Gemini响应生成错误: {error_msg}") + + # 针对不同错误返回友好提示 + if "Rate limit" in error_msg: + yield "【Gemini服务请求太频繁,请稍后再试】" + elif "Invalid API key" in error_msg: + yield "【Gemini API key无效】" + else: + yield f"【Gemini服务响应异常: {error_msg}】" + + except requests.exceptions.RequestException as e: From 0b744a25990f871827704c9936bb10151cee21d2 Mon Sep 17 00:00:00 2001 From: Huang <32005838+openrz@users.noreply.github.com> Date: Wed, 19 Mar 2025 00:20:08 +0800 Subject: [PATCH 07/17] =?UTF-8?q?update:dify=E9=BB=98=E8=AE=A4=E5=BA=94?= =?UTF-8?q?=E6=98=AFchat=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index 36e660f7..96ae437a 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -186,7 +186,7 @@ LLM: # 使用的对话模式 可以选择工作流 workflows/run 对话模式 chat-messages 文本生成 completion-messages # 使用workflows进行返回的时候输入参数为 query 返回参数的名字要设置为 answer # 文本生成的默认输入参数也是query - mode: workflows/run + mode: chat-messages GeminiLLM: type: gemini # 谷歌Gemini API,需要先在Google Cloud控制台创建API密钥并获取api_key From bf3aa2df09905343b8a6dd33176d04fe80b0bd1c Mon Sep 17 00:00:00 2001 From: Huang <32005838+openrz@users.noreply.github.com> Date: Wed, 19 Mar 2025 00:20:47 +0800 Subject: [PATCH 08/17] =?UTF-8?q?dify=E9=BB=98=E8=AE=A4=E5=BA=94=E8=AF=A5?= =?UTF-8?q?=E6=98=AFchat=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/core/providers/llm/dify/dify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/xiaozhi-server/core/providers/llm/dify/dify.py b/main/xiaozhi-server/core/providers/llm/dify/dify.py index 02ef5588..5300fc79 100644 --- a/main/xiaozhi-server/core/providers/llm/dify/dify.py +++ b/main/xiaozhi-server/core/providers/llm/dify/dify.py @@ -9,7 +9,7 @@ logger = setup_logging() class LLMProvider(LLMProviderBase): def __init__(self, config): self.api_key = config["api_key"] - self.mode = config.get("mode", "workflows/run") + self.mode = config.get("mode", "chat-messages") self.base_url = config.get("base_url", "https://api.dify.ai/v1").rstrip('/') self.session_conversation_map = {} # 存储session_id和conversation_id的映射 From ed775405c6c5d8a530ff3273eefa82ebd0a1b62d Mon Sep 17 00:00:00 2001 From: jzhuang Date: Wed, 19 Mar 2025 13:57:42 +0800 Subject: [PATCH 09/17] =?UTF-8?q?add:=E6=94=AF=E6=8C=81Gizwits=20API?= =?UTF-8?q?=E6=B8=A0=E9=81=93=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 10 +- main/xiaozhi-server/config.yaml | 16 + .../core/providers/asr/volcengine.py | 300 ++++++++++++++++++ 3 files changed, 322 insertions(+), 4 deletions(-) create mode 100644 main/xiaozhi-server/core/providers/asr/volcengine.py diff --git a/README.md b/README.md index cdf31847..f3f7944b 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,7 @@ server: | TTS | CosyVoiceSiliconflow | 接口调用 | 消耗 token | 需申请硅基流动 API 密钥;输出格式为 wav | | TTS | TTS302AI | 接口调用 | 消耗 token | [点击创建密钥](https://dash.302.ai/apis/list) | | TTS | CozeCnTTS | 接口调用 | 消耗 token | 需提供 Coze API key;输出格式为 wav | +| TTS | GizwitsTTS | 接口调用 | 消耗 token | [点击创建密钥](https://agentrouter.gizwitsapi.com) | | TTS | ACGNTTS | 接口调用 | 消耗 token | [联系网站管理员购买密钥](www.ttson.cn) | | TTS | OpenAITTS | 接口调用 | 消耗 token | 境外使用,境外购买 | | TTS | FishSpeech | 接口调用 | 免费/自定义 | 本地启动 TTS 服务;启动方法见配置文件内说明 | @@ -220,10 +221,11 @@ server: ### ASR 语音识别 -| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 | -|:---:|:---------:|:----:|:----:|:--:| -| ASR | FunASR | 本地使用 | 免费 | | -| ASR | DoubaoASR | 接口调用 | 收费 | | +| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 | +|:---:|:----------:|:----:|:----:|:------------------------------------------------------:| +| ASR | FunASR | 本地使用 | 免费 | | +| ASR | DoubaoASR | 接口调用 | 收费 | | +| ASR | GizwitsASR | 接口调用 | 消耗 token | [点击创建密钥](https://agentrouter.gizwitsapi.com),支持语音识别大模型 | --- diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index f3ca5706..0aa92490 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -131,6 +131,13 @@ ASR: access_token: 你的火山引擎语音合成服务access_token cluster: volcengine_input_common output_dir: tmp/ + GizwitsASR: + type: volcengine + # 火山引擎作为基座,可以完全使用企业级火山引擎语音识别大模型服务,识别率更准确 + # 获取API Key地址:https://agentrouter.gizwitsapi.com/panel/token + base_host: bytedance.gizwitsapi.com + access_token: "你的机智云API key" + output_dir: tmp/ VAD: SileroVAD: threshold: 0.5 @@ -399,6 +406,15 @@ TTS: voice: "zh_female_wanwanxiaohe_moon_bigtts" output_dir: tmp/ access_token: "你的302API密钥" + GizwitsTTS: + type: doubao + # 火山引擎作为基座,可以完全使用企业级火山引擎语音合成服务 + # 获取API Key地址:https://agentrouter.gizwitsapi.com/panel/token + api_url: https://bytedance.gizwitsapi.com/api/v1/tts + authorization: "Bearer " + voice: "zh_female_wanwanxiaohe_moon_bigtts" + output_file: tmp/ + access_token: "你的机智云API key" ACGNTTS: #在线网址:https://acgn.ttson.cn/ #token购买:www.ttson.cn diff --git a/main/xiaozhi-server/core/providers/asr/volcengine.py b/main/xiaozhi-server/core/providers/asr/volcengine.py new file mode 100644 index 00000000..9592aa18 --- /dev/null +++ b/main/xiaozhi-server/core/providers/asr/volcengine.py @@ -0,0 +1,300 @@ +import time +import io +import wave +import os +from typing import Optional, Tuple, List +import uuid +import websockets +import json +import gzip + +import opuslib_next +from core.utils.util import check_model_key +from core.providers.asr.base import ASRProviderBase + +from config.logger import setup_logging + +TAG = __name__ +logger = setup_logging() + +PROTOCOL_VERSION = 0b0001 +DEFAULT_HEADER_SIZE = 0b0001 + +# Message Type: +FULL_CLIENT_REQUEST = 0b0001 +AUDIO_ONLY_REQUEST = 0b0010 +FULL_SERVER_RESPONSE = 0b1001 +SERVER_ERROR_RESPONSE = 0b1111 + +# Message Type Specific Flags +FLAG_NO_SEQUENCE = 0b0000 +FLAG_WITH_SEQUENCE = 0b0001 +FLAG_LAST_PACKET = 0b0010 + +# Message Serialization +NO_SERIALIZATION = 0b0000 +JSON = 0b0001 + +# Message Compression +NO_COMPRESSION = 0b0000 +GZIP = 0b0001 + + +class ASRProvider(ASRProviderBase): + def __init__(self, config: dict, delete_audio_file: bool): + self.host = config.get("base_host", "openspeech.bytedance.com") + self.ws_url = f"wss://{self.host}/api/v3/sauc/bigmodel" + + self.appid = config.get("appid") + self.access_token = config.get("access_token") + self.resource_id = config.get("resource_id", "volc.bigasr.sauc.duration") + check_model_key("ASR", self.access_token) + + self.seg_duration = 15000 + # 确保输出目录存在 + self.output_dir = config.get("output_dir") + os.makedirs(self.output_dir, exist_ok=True) + + def save_audio_to_file(self, opus_data: List[bytes], session_id: str) -> str: + """将Opus音频数据解码并保存为WAV文件""" + file_name = f"asr_{session_id}_{uuid.uuid4()}.wav" + file_path = os.path.join(self.output_dir, file_name) + + decoder = opuslib_next.Decoder(16000, 1) # 16kHz, 单声道 + pcm_data = [] + + for opus_packet in opus_data: + try: + pcm_frame = decoder.decode(opus_packet, 960) # 960 samples = 60ms + pcm_data.append(pcm_frame) + except opuslib_next.OpusError as e: + logger.bind(tag=TAG).error(f"Opus解码错误: {e}", exc_info=True) + + with wave.open(file_path, "wb") as wf: + wf.setnchannels(1) + wf.setsampwidth(2) # 2 bytes = 16-bit + wf.setframerate(16000) + wf.writeframes(b"".join(pcm_data)) + + return file_path + + @staticmethod + def _generate_header(message_type=FULL_CLIENT_REQUEST, message_type_specific_flags=FLAG_NO_SEQUENCE, + serial_method=JSON, compression_type=GZIP, reserved_data=0x00) -> bytearray: + """ + protocol_version(4 bits), header_size(4 bits), + message_type(4 bits), message_type_specific_flags(4 bits) + serialization_method(4 bits) message_compression(4 bits) + reserved(8bits) 保留字段 + """ + header = bytearray() + header.append((PROTOCOL_VERSION << 4) | DEFAULT_HEADER_SIZE) + header.append((message_type << 4) | message_type_specific_flags) + header.append((serial_method << 4) | compression_type) + header.append(reserved_data) + return header + + @staticmethod + def _construct_request() -> dict: + """Construct the request payload.""" + return { + "user": { + "uid": str(uuid.uuid4()), + }, + "audio": { + "format": "wav", + "codec": "raw", + "rate": 16000, + "bits": 16, + "channel": 1, + "language": "zh-CN", + }, + "request": { + "model_name": "bigmodel", + "enable_itn": False, + "enable_ddc": False, + "enable_punc": False, + "show_utterances": False, + }, + } + + @staticmethod + def _parse_response(res): + """ + protocol_version(4 bits), header_size(4 bits), + message_type(4 bits), message_type_specific_flags(4 bits) + serialization_method(4 bits) message_compression(4 bits) + reserved(8 bits)保留字段 + header_extensions 扩展头(大小等于 8 * 4 * (header_size - 1)) + payload 类似与http 请求体 + """ + protocol_version = res[0] >> 4 + if protocol_version != PROTOCOL_VERSION: + return None + header_size = res[0] & 0x0f + message_type = res[1] >> 4 + message_type_specific_flags = res[1] & 0x0f + serialization_method = res[2] >> 4 + message_compression = res[2] & 0x0f + payload = res[header_size * 4:] + + result = {} + payload_msg = None + if message_type == FULL_SERVER_RESPONSE: + if message_type_specific_flags & FLAG_WITH_SEQUENCE: + # receive frame with sequence + result['payload_sequence'] = int.from_bytes(payload[:4], "big", signed=True) + payload = payload[4:] + if message_type_specific_flags & FLAG_LAST_PACKET: + # receive last package + result['is_last_package'] = True + payload_size = int.from_bytes(payload[:4], "big", signed=True) + payload_msg = payload[4:4 + payload_size] + elif message_type == SERVER_ERROR_RESPONSE: + result['code'] = int.from_bytes(payload[:4], "big", signed=False) + payload_size = int.from_bytes(payload[4:8], "big", signed=False) + payload_msg = payload[8:8 + payload_size] + if payload_msg is not None: + if message_compression == GZIP: + payload_msg = gzip.decompress(payload_msg) + if serialization_method == JSON: + payload_msg = json.loads(str(payload_msg, "utf-8")) + elif serialization_method != NO_SERIALIZATION: + payload_msg = str(payload_msg, "utf-8") + result['payload_msg'] = payload_msg + return result + + async def _send_request(self, audio_data: List[bytes], segment_size: int) -> Optional[str]: + """Send request to VolcEngine ASR service.""" + try: + auth_header = { + "X-Api-App-Key": self.appid, + "X-Api-Access-Key": self.access_token, + "X-Api-Resource-Id": self.resource_id, + "X-Api-Request-Id": str(uuid.uuid4()) + } + async with websockets.connect(self.ws_url, additional_headers=auth_header, + max_size=128 * 1024 * 1024) as websocket: + sequence = 1 + + # Send header and metadata + request_params = self._construct_request() + payload_bytes = str.encode(json.dumps(request_params)) + payload_bytes = gzip.compress(payload_bytes) + full_client_request = self._generate_header(message_type_specific_flags=FLAG_WITH_SEQUENCE) + full_client_request.extend(sequence.to_bytes(4, 'big', signed=True)) + full_client_request.extend((len(payload_bytes)).to_bytes(4, 'big')) # payload size(4 bytes) + full_client_request.extend(payload_bytes) # payload + await websocket.send(full_client_request) + res = await websocket.recv() + result = self._parse_response(res) + if 'code' in result: + logger.bind(tag=TAG).error(f"ASR error: {result['payload_msg']}") + return None + + for _, (chunk, last) in enumerate(self.slice_data(audio_data, segment_size), 1): + sequence += 1 + if last: + sequence = -sequence # last package + audio_only_request = self._generate_header( + message_type=AUDIO_ONLY_REQUEST, + message_type_specific_flags=FLAG_WITH_SEQUENCE | FLAG_LAST_PACKET + ) + else: + audio_only_request = self._generate_header( + message_type=AUDIO_ONLY_REQUEST, + message_type_specific_flags=FLAG_WITH_SEQUENCE + ) + audio_only_request.extend(sequence.to_bytes(4, 'big', signed=True)) # sequence + payload_bytes = gzip.compress(chunk) + audio_only_request.extend((len(payload_bytes)).to_bytes(4, 'big')) # payload size(4 bytes) + audio_only_request.extend(payload_bytes) # payload + # Send audio data + await websocket.send(audio_only_request) + + # Receive response + for _ in range(1, -sequence): + response = await websocket.recv() + result = self._parse_response(response) + if 'code' in result: + logger.bind(tag=TAG).error(f"ASR error: {result['payload_msg']}") + return None + if 'is_last_package' in result and result['is_last_package'] is True: + return result.get('payload_msg', {}).get('result', {}).get('text') + + raise Exception("not received last package") + + except Exception as e: + logger.bind(tag=TAG).error(f"ASR request failed: {e}", exc_info=True) + return None + + @staticmethod + def decode_opus(opus_data: List[bytes], session_id: str) -> List[bytes]: + decoder = opuslib_next.Decoder(16000, 1) # 16kHz, 单声道 + pcm_data = [] + + for opus_packet in opus_data: + try: + pcm_frame = decoder.decode(opus_packet, 960) # 960 samples = 60ms + pcm_data.append(pcm_frame) + except opuslib_next.OpusError as e: + logger.bind(tag=TAG).error(f"Opus解码错误: {e}", exc_info=True) + + return pcm_data + + @staticmethod + def read_wav_info(data: io.BytesIO = None) -> (int, int, int, int, int): + with io.BytesIO(data) as _f: + wave_fp = wave.open(_f, 'rb') + nchannels, sampwidth, framerate, nframes = wave_fp.getparams()[:4] + wave_bytes = wave_fp.readframes(nframes) + return nchannels, sampwidth, framerate, nframes, len(wave_bytes) + + @staticmethod + def slice_data(data: bytes, chunk_size: int) -> (list, bool): + """ + slice data + :param data: wav data + :param chunk_size: the segment size in one request + :return: segment data, last flag + """ + data_len = len(data) + offset = 0 + while offset + chunk_size < data_len: + yield data[offset: offset + chunk_size], False + offset += chunk_size + else: + yield data[offset: data_len], True + + async def speech_to_text(self, opus_data: List[bytes], session_id: str) -> Tuple[Optional[str], Optional[str]]: + """将语音数据转换为文本""" + try: + # 合并所有opus数据包 + pcm_data = self.decode_opus(opus_data, session_id) + combined_pcm_data = b''.join(pcm_data) + + wav_buffer = io.BytesIO() + + with wave.open(wav_buffer, "wb") as wav_file: + wav_file.setnchannels(1) # 设置声道数 + wav_file.setsampwidth(2) # 设置采样宽度 + wav_file.setframerate(16000) # 设置采样率 + wav_file.writeframes(combined_pcm_data) # 写入 PCM 数据 + + # 获取封装后的 WAV 数据 + wav_data = wav_buffer.getvalue() + nchannels, sampwidth, framerate, nframes, wav_len = self.read_wav_info(wav_data) + size_per_sec = nchannels * sampwidth * framerate + segment_size = int(size_per_sec * self.seg_duration / 1000) + + # 语音识别 + start_time = time.time() + text = await self._send_request(wav_data, segment_size) + if text: + logger.bind(tag=TAG).debug(f"语音识别耗时: {time.time() - start_time:.3f}s | 结果: {text}") + return text, None + return "", None + + except Exception as e: + logger.bind(tag=TAG).error(f"语音识别失败: {e}", exc_info=True) + return "", None From 4431e7470fafc3dca52f90899a2f37c1bc86b174 Mon Sep 17 00:00:00 2001 From: CGD <3030332422@qq.com> Date: Wed, 19 Mar 2025 16:16:29 +0800 Subject: [PATCH 10/17] =?UTF-8?q?=E5=AE=8C=E6=88=90=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E7=94=A8=E6=88=B7=E4=BF=A1=E6=81=AF=E3=80=81?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E6=99=BA=E8=83=BD=E4=BD=93=E5=88=97=E8=A1=A8?= =?UTF-8?q?=EF=BC=8C=E6=90=9C=E7=B4=A2=E5=8A=9F=E8=83=BD=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/manager-web/src/apis/module/user.js | 45 +++++++++++------ .../manager-web/src/components/DeviceItem.vue | 8 ++-- main/manager-web/src/components/HeaderBar.vue | 48 +++++++++++++++++-- main/manager-web/src/views/home.vue | 34 +++++++++---- 4 files changed, 103 insertions(+), 32 deletions(-) diff --git a/main/manager-web/src/apis/module/user.js b/main/manager-web/src/apis/module/user.js index 444d5d76..889a84e4 100755 --- a/main/manager-web/src/apis/module/user.js +++ b/main/manager-web/src/apis/module/user.js @@ -18,20 +18,6 @@ export default { }) }).send() }, - // 获取用户信息 - getUserInfo(callback) { - RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/info`) - .method('GET') - .success((res) => { - RequestService.clearRequestTime() - callback(res) - }) - .fail(() => { - RequestService.reAjaxFun(() => { - this.getUserInfo() - }) - }).send() - }, // 获取设备信息 getHomeList(callback) { RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/device/bind`) @@ -174,4 +160,35 @@ export default { }).send(); }, + // 获取智能体列表 + getAgentList(callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/api/v1/user/agent`) + .method('GET') + .success((res) => { + RequestService.clearRequestTime(); + callback(res); + }) + .fail(() => { + RequestService.reAjaxFun(() => { + this.getAgentList(callback); + }); + }).send(); + }, + getUserInfo(callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/api/v1/user/info`) + .method('GET') + .success((res) => { + RequestService.clearRequestTime() + callback(res) + }) + .fail((err) => { + console.error('接口请求失败:', err) + RequestService.reAjaxFun(() => { + this.getUserInfo(callback) + }) + }).send() + }, + } diff --git a/main/manager-web/src/components/DeviceItem.vue b/main/manager-web/src/components/DeviceItem.vue index e830555f..95a95b00 100644 --- a/main/manager-web/src/components/DeviceItem.vue +++ b/main/manager-web/src/components/DeviceItem.vue @@ -2,7 +2,7 @@
- {{ device.mac }} + {{ device.agentName }}
- 设备型号:{{ device.model }} + 设备型号:{{ device.ttsModelName }}
- 音色模型:{{ device.voiceModel }} + 音色模型:{{ device.ttsVoiceName }}
@@ -31,7 +31,7 @@
-
最近对话:{{ device.lastConversation }}
+
最近对话:{{ device.lastConnectedAt }}
diff --git a/main/manager-web/src/components/HeaderBar.vue b/main/manager-web/src/components/HeaderBar.vue index 758959b3..78b18970 100644 --- a/main/manager-web/src/components/HeaderBar.vue +++ b/main/manager-web/src/components/HeaderBar.vue @@ -19,13 +19,13 @@
- + + style="width: 14px;height: 14px;margin-right: 11px;cursor: pointer;" @click="handleSearch" />
@@ -33,17 +33,57 @@ diff --git a/main/manager-web/src/views/home.vue b/main/manager-web/src/views/home.vue index 4b9c6eb5..15a215b9 100644 --- a/main/manager-web/src/views/home.vue +++ b/main/manager-web/src/views/home.vue @@ -1,7 +1,7 @@ diff --git a/main/manager-web/src/views/home.vue b/main/manager-web/src/views/home.vue index 4b9c6eb5..0c914e77 100644 --- a/main/manager-web/src/views/home.vue +++ b/main/manager-web/src/views/home.vue @@ -1,7 +1,7 @@