diff --git a/.gitignore b/.gitignore
index 8be615c0..c09248c5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
# Byte-compiled / optimized / DLL files
__pycache__/
+.idea/
*.py[cod]
*$py.class
diff --git a/Dockerfile-pip b/Dockerfile-pip
index 00ad57f0..61286673 100755
--- a/Dockerfile-pip
+++ b/Dockerfile-pip
@@ -25,7 +25,7 @@ COPY requirements.txt .
# 优化apt安装
RUN pip install --no-cache-dir -r requirements.txt \
- -i https://pypi.tuna.tsinghua.edu.cn/simple
+ -i https://mirrors.aliyun.com/pypi/simple/
# 第三阶段:生产镜像
FROM python:3.10-slim
@@ -33,8 +33,8 @@ FROM python:3.10-slim
WORKDIR /opt/xiaozhi-esp32-server
# 优化apt安装
-RUN echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm main contrib non-free non-free-firmware" > /etc/apt/sources.list && \
- echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm-updates main contrib non-free non-free-firmware" >> /etc/apt/sources.list && \
+RUN echo "deb https://mirrors.aliyun.com/debian/ bookworm main contrib non-free non-free-firmware" > /etc/apt/sources.list && \
+ echo "deb https://mirrors.aliyun.com/debian/ bookworm-updates main contrib non-free non-free-firmware" >> /etc/apt/sources.list && \
apt-get update && \
apt-get install -y --no-install-recommends libopus0 ffmpeg && \
apt-get clean && \
diff --git a/README.md b/README.md
index c77bd07f..578b13dc 100644
--- a/README.md
+++ b/README.md
@@ -14,15 +14,64 @@
本项目需要配合 ESP32 硬件设备使用。如果您已经购买了 ESP32 相关硬件,且成功对接过虾哥部署的后端服务,并希望独立搭建自己的
`xiaozhi-esp32` 后端服务,那么本项目非常适合您。
-跳转到[使用方式](#使用方式-)
+想看使用效果?请猛戳视频 🎥
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-想看使用效果?请猛戳这个视频 🎥
-
-
-
-
-
---
@@ -128,8 +177,22 @@ server:
### 一、[部署文档](./docs/Deployment.md)
-1.**[本地源码运行](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%89%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C)**
-
+本项目支持以下三种部署方式,您可根据实际需求选择:
+
+1. **[Docker 快速部署](./docs/Deployment.md)**
+ 适合快速体验,不需过多环境配置。缺点是,拉取镜像有点慢。
+2.
+ *
+
+*[借助 Docker 环境运行部署](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E5%80%9F%E5%8A%A9docker%E7%8E%AF%E5%A2%83%E8%BF%90%E8%A1%8C%E9%83%A8%E7%BD%B2)
+**
+适用于已安装 Docker 且希望对代码进行自定义修改的用户。
+
+3.
+ *
+
+*[本地源码运行](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%89%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C)
+**
适合熟悉 Conda 环境或希望从零搭建运行环境的用户。
对于对响应速度要求较高的场景,推荐使用本地源码运行方式以降低额外开销。
@@ -166,13 +229,15 @@ VAD:
### 4、为什么我说的话,小智识别出来很多韩文、日文、英文?🇰🇷
-建议:检查一下`models/SenseVoiceSmall`是否已经有`model.pt`文件,如果没有就要下载,查看这里[下载语音识别模型文件](docs/Deployment.md#模型文件)
+建议:检查一下`models/SenseVoiceSmall`是否已经有`model.pt`
+文件,如果没有就要下载,查看这里[下载语音识别模型文件](docs/Deployment.md#模型文件)
### 5、为什么会出现“TTS 任务出错 文件不存在”?📁
建议:检查一下是否正确使用`conda` 安装了`libopus`和`ffmpeg`库。
如果没有安装,就安装
+
```
conda install conda-forge::libopus
conda install conda-forge::ffmpeg
diff --git a/config.yaml b/config.yaml
index a0ce2e1b..6a6e8b9c 100644
--- a/config.yaml
+++ b/config.yaml
@@ -59,7 +59,7 @@ CMD_exit:
# 具体处理时选择的模块(The module selected for specific processing)
selected_module:
- ASR: FunASR
+ ASR: DoubaoASR
VAD: SileroVAD
# 将根据配置名称对应的type调用实际的LLM适配器
LLM: ChatGLMLLM
@@ -68,9 +68,15 @@ selected_module:
ASR:
FunASR:
+ type: fun_local
model_dir: models/SenseVoiceSmall
output_dir: tmp/
-
+ DoubaoASR:
+ type: doubao
+ appid: 你的火山引擎语音合成服务appid
+ access_token: 你的火山引擎语音合成服务access_token
+ cluster: volcengine_input_common
+ output_dir: tmp/
VAD:
SileroVAD:
threshold: 0.5
diff --git a/config/logger.py b/config/logger.py
index 8a5a3e6e..cddd80d6 100644
--- a/config/logger.py
+++ b/config/logger.py
@@ -9,7 +9,7 @@ def setup_logging(log_dir='tmp', data_dir='data'):
# 设置日志格式,时间、日志级别、标签、消息
log_format = (
- # "{time:YYYY-MM-DD HH:mm:ss}"
+ "{time:YY-MM-DD HH:mm:ss}"
"[{extra[tag]}]"
" - {level} - "
"{message}"
diff --git a/core/handle/audioHandle.py b/core/handle/audioHandle.py
index 394e7fe5..67615a95 100644
--- a/core/handle/audioHandle.py
+++ b/core/handle/audioHandle.py
@@ -28,19 +28,22 @@ async def handleAudioMessage(conn, audio):
if conn.client_voice_stop:
conn.client_abort = False
conn.asr_server_receive = False
- text, file_path = conn.asr.speech_to_text(conn.asr_audio, conn.session_id)
- logger.bind(tag=TAG).info(f"识别文本: {text}")
- text_len, text_without_punctuation = remove_punctuation_and_length(text)
- if text_len <= conn.max_cmd_length and await handleCMDMessage(conn, text_without_punctuation):
- return
- if text_len > 0:
- await startToChat(conn, text)
- else:
+ # 音频太短了,无法识别
+ if len(conn.asr_audio) < 3:
conn.asr_server_receive = True
+ else:
+ text, file_path = await conn.asr.speech_to_text(conn.asr_audio, conn.session_id)
+ logger.bind(tag=TAG).info(f"识别文本: {text}")
+ text_len, text_without_punctuation = remove_punctuation_and_length(text)
+ if text_len <= conn.max_cmd_length and await handleCMDMessage(conn, text_without_punctuation):
+ return
+ if text_len > 0:
+ await startToChat(conn, text)
+ else:
+ conn.asr_server_receive = True
conn.asr_audio.clear()
conn.reset_vad_states()
-
async def handleCMDMessage(conn, text):
cmd_exit = conn.cmd_exit
for cmd in cmd_exit:
@@ -161,4 +164,4 @@ async def no_voice_close_connect(conn):
conn.client_abort = False
conn.asr_server_receive = False
prompt = "时间过得真快,我都好久没说话了。请你用十个字左右话跟我告别,以“再见”或“拜拜拜”为结尾"
- await startToChat(conn, prompt)
+ await startToChat(conn, prompt)
\ No newline at end of file
diff --git a/core/providers/asr/base.py b/core/providers/asr/base.py
new file mode 100644
index 00000000..d8db22a8
--- /dev/null
+++ b/core/providers/asr/base.py
@@ -0,0 +1,19 @@
+from abc import ABC, abstractmethod
+from typing import Optional, Tuple, List
+
+from config.logger import setup_logging
+
+TAG = __name__
+logger = setup_logging()
+
+
+class ASRProviderBase(ABC):
+ @abstractmethod
+ def save_audio_to_file(self, opus_data: List[bytes], session_id: str) -> str:
+ """解码Opus数据并保存为WAV文件"""
+ pass
+
+ @abstractmethod
+ async def speech_to_text(self, opus_data: List[bytes], session_id: str) -> Tuple[Optional[str], Optional[str]]:
+ """将语音数据转换为文本"""
+ pass
diff --git a/core/providers/asr/doubao.py b/core/providers/asr/doubao.py
new file mode 100644
index 00000000..56108079
--- /dev/null
+++ b/core/providers/asr/doubao.py
@@ -0,0 +1,286 @@
+import time
+import io
+import wave
+import os
+from typing import Optional, Tuple, List
+import uuid
+import websockets
+import json
+import gzip
+
+import opuslib
+from core.providers.asr.base import ASRProviderBase
+
+from config.logger import setup_logging
+
+TAG = __name__
+logger = setup_logging()
+
+CLIENT_FULL_REQUEST = 0b0001
+CLIENT_AUDIO_ONLY_REQUEST = 0b0010
+
+NO_SEQUENCE = 0b0000
+NEG_SEQUENCE = 0b0010
+
+SERVER_FULL_RESPONSE = 0b1001
+SERVER_ACK = 0b1011
+SERVER_ERROR_RESPONSE = 0b1111
+
+NO_SERIALIZATION = 0b0000
+JSON = 0b0001
+THRIFT = 0b0011
+CUSTOM_TYPE = 0b1111
+NO_COMPRESSION = 0b0000
+GZIP = 0b0001
+CUSTOM_COMPRESSION = 0b1111
+
+
+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 (8bits) 保留字段
+ header_extensions 扩展头(大小等于 8 * 4 * (header_size - 1) )
+ payload 类似与http 请求体
+ """
+ protocol_version = res[0] >> 4
+ 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
+ reserved = res[3]
+ header_extensions = res[4:header_size * 4]
+ payload = res[header_size * 4:]
+ result = {}
+ payload_msg = None
+ payload_size = 0
+ if message_type == SERVER_FULL_RESPONSE:
+ payload_size = int.from_bytes(payload[:4], "big", signed=True)
+ payload_msg = payload[4:]
+ elif message_type == SERVER_ACK:
+ seq = int.from_bytes(payload[:4], "big", signed=True)
+ result['seq'] = seq
+ if len(payload) >= 8:
+ payload_size = int.from_bytes(payload[4:8], "big", signed=False)
+ payload_msg = payload[8:]
+ elif message_type == SERVER_ERROR_RESPONSE:
+ code = int.from_bytes(payload[:4], "big", signed=False)
+ result['code'] = code
+ payload_size = int.from_bytes(payload[4:8], "big", signed=False)
+ payload_msg = payload[8:]
+ if payload_msg is None:
+ return result
+ 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
+ result['payload_size'] = payload_size
+ return result
+
+
+class ASRProvider(ASRProviderBase):
+ def __init__(self, config: dict, delete_audio_file: bool):
+ self.appid = config.get("appid")
+ self.cluster = config.get("cluster")
+ self.access_token = config.get("access_token")
+ self.output_dir = config.get("output_dir")
+
+ self.host = "openspeech.bytedance.com"
+ self.ws_url = f"wss://{self.host}/api/v2/asr"
+ self.success_code = 1000
+ self.seg_duration = 15000
+
+ # 确保输出目录存在
+ 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.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.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=CLIENT_FULL_REQUEST, message_type_specific_flags=NO_SEQUENCE) -> bytearray:
+ """Generate protocol header."""
+ header = bytearray()
+ header_size = 1
+ header.append((0b0001 << 4) | header_size) # Protocol version
+ header.append((message_type << 4) | message_type_specific_flags)
+ header.append((0b0001 << 4) | 0b0001) # JSON serialization & GZIP compression
+ header.append(0x00) # reserved
+ return header
+
+ def _construct_request(self, reqid) -> dict:
+ """Construct the request payload."""
+ return {
+ "app": {
+ "appid": f"{self.appid}",
+ "cluster": self.cluster,
+ "token": self.access_token,
+ },
+ "user": {
+ "uid": str(uuid.uuid4()),
+ },
+ "request": {
+ "reqid": reqid,
+ "show_utterances": False,
+ "sequence": 1
+ },
+ "audio": {
+ "format": "wav",
+ "rate": 16000,
+ "language": "zh-CN",
+ "bits": 16,
+ "channel": 1,
+ "codec": "raw",
+ },
+ }
+
+ async def _send_request(self, audio_data: List[bytes], segment_size: int) -> Optional[str]:
+ """Send request to Volcano ASR service."""
+ try:
+ auth_header = {'Authorization': 'Bearer; {}'.format(self.access_token)}
+ async with websockets.connect(self.ws_url, additional_headers=auth_header) as websocket:
+ # Prepare request data
+ request_params = self._construct_request(str(uuid.uuid4()))
+ print(request_params)
+ payload_bytes = str.encode(json.dumps(request_params))
+ payload_bytes = gzip.compress(payload_bytes)
+ full_client_request = self._generate_header()
+ full_client_request.extend((len(payload_bytes)).to_bytes(4, 'big')) # payload size(4 bytes)
+ full_client_request.extend(payload_bytes) # payload
+
+ # Send header and metadata
+ # full_client_request
+ await websocket.send(full_client_request)
+ res = await websocket.recv()
+ result = parse_response(res)
+ if 'payload_msg' in result and result['payload_msg']['code'] != self.success_code:
+ logger.bind(tag=TAG).error(f"ASR error: {result}")
+ return None
+
+ for seq, (chunk, last) in enumerate(self.slice_data(audio_data, segment_size), 1):
+ if last:
+ audio_only_request = self._generate_header(
+ message_type=CLIENT_AUDIO_ONLY_REQUEST,
+ message_type_specific_flags=NEG_SEQUENCE
+ )
+ else:
+ audio_only_request = self._generate_header(
+ message_type=CLIENT_AUDIO_ONLY_REQUEST
+ )
+ 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
+ response = await websocket.recv()
+ result = parse_response(response)
+
+ if 'payload_msg' in result and result['payload_msg']['code'] == self.success_code:
+ if len(result['payload_msg']['result']) > 0:
+ return result['payload_msg']['result'][0]["text"]
+ return None
+ else:
+ logger.bind(tag=TAG).error(f"ASR error: {result}")
+ return None
+
+ 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.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.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
diff --git a/core/providers/asr/fun_local.py b/core/providers/asr/fun_local.py
new file mode 100644
index 00000000..82d09ab5
--- /dev/null
+++ b/core/providers/asr/fun_local.py
@@ -0,0 +1,110 @@
+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
+
+from funasr import AutoModel
+from funasr.utils.postprocess_utils import rich_transcription_postprocess
+
+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)
+ with CaptureOutput():
+ self.model = AutoModel(
+ model=self.model_dir,
+ vad_kwargs={"max_single_segment_time": 30000},
+ disable_update=True,
+ hub="hf"
+ # device="cuda:0", # 启用GPU加速
+ )
+
+ 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
+
+ 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()
+ result = self.model.generate(
+ input=file_path,
+ cache={},
+ language="auto",
+ use_itn=True,
+ batch_size_s=60,
+ )
+ text = rich_transcription_postprocess(result[0]["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/core/providers/tts/doubao.py b/core/providers/tts/doubao.py
index f6eca974..74ddde2a 100644
--- a/core/providers/tts/doubao.py
+++ b/core/providers/tts/doubao.py
@@ -25,7 +25,7 @@ class TTSProvider(TTSProviderBase):
async def text_to_speak(self, text, output_file):
request_json = {
"app": {
- "appid": self.appid,
+ "appid": f"{self.appid}",
"token": "access_token",
"cluster": self.cluster
},
@@ -49,8 +49,13 @@ class TTSProvider(TTSProviderBase):
}
}
- resp = requests.post(self.api_url, json.dumps(request_json), headers=self.header)
- if "data" in resp.json():
- data = resp.json()["data"]
- file_to_save = open(output_file, "wb")
- file_to_save.write(base64.b64decode(data))
+ try:
+ resp = requests.post(self.api_url, json.dumps(request_json), headers=self.header)
+ if "data" in resp.json():
+ data = resp.json()["data"]
+ file_to_save = open(output_file, "wb")
+ file_to_save.write(base64.b64decode(data))
+ else:
+ raise Exception(f"{__name__} status_code: {resp.status_code} response: {resp.content}")
+ except Exception as e:
+ raise Exception(f"{__name__} error: {e}")
diff --git a/core/providers/tts/fishspeech.py b/core/providers/tts/fishspeech.py
index 1cbe0810..5794a2e3 100644
--- a/core/providers/tts/fishspeech.py
+++ b/core/providers/tts/fishspeech.py
@@ -89,7 +89,7 @@ class TTSProvider(TTSProviderBase):
self.channels = config.get("channels",1)
self.rate = config.get("rate",44100)
self.api_key = config.get("api_key","YOUR_API_KEY")
- if not self.api_key or "你" in self.api_key:
+ if "你" in self.api_key:
logger.bind(tag=TAG).error("你还没配置FishSpeech TTS的密钥,请在配置文件中配置密钥,否则无法正常工作")
return
self.normalize = config.get("normalize",True)
diff --git a/core/utils/asr.py b/core/utils/asr.py
index ab1c35c4..983114d3 100644
--- a/core/utils/asr.py
+++ b/core/utils/asr.py
@@ -1,132 +1,24 @@
-import time
-import wave
+import importlib
+import logging
import os
import sys
-import io
-from abc import ABC, abstractmethod
-from config.logger import setup_logging
-from typing import Optional, Tuple, List
+import time
+import wave
import uuid
-
-import opuslib_next
-from funasr import AutoModel
-from funasr.utils.postprocess_utils import rich_transcription_postprocess
+from abc import ABC, abstractmethod
+from typing import Optional, Tuple, List
+from core.providers.asr.base import ASRProviderBase
+from config.logger import setup_logging
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 ASR(ABC):
- @abstractmethod
- def save_audio_to_file(self, opus_data: List[bytes], session_id: str) -> str:
- """解码Opus数据并保存为WAV文件"""
- pass
-
- @abstractmethod
- def speech_to_text(self, opus_data: List[bytes], session_id: str) -> Tuple[Optional[str], Optional[str]]:
- """将语音数据转换为文本"""
- pass
-
-
-class FunASR(ASR):
- 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)
- with CaptureOutput():
- self.model = AutoModel(
- model=self.model_dir,
- vad_kwargs={"max_single_segment_time": 30000},
- disable_update=True,
- hub="hf"
- # device="cuda:0", # 启用GPU加速
- )
-
- 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 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()
- result = self.model.generate(
- input=file_path,
- cache={},
- language="auto",
- use_itn=True,
- batch_size_s=60,
- )
- text = rich_transcription_postprocess(result[0]["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, 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}")
-
-
-def create_instance(class_name: str, *args, **kwargs) -> ASR:
+def create_instance(class_name: str, *args, **kwargs) -> ASRProviderBase:
"""工厂方法创建ASR实例"""
- cls_map = {
- "FunASR": FunASR,
- # 可扩展其他ASR实现
- }
+ if os.path.exists(os.path.join('core', 'providers', 'asr', f'{class_name}.py')):
+ lib_name = f'core.providers.asr.{class_name}'
+ if lib_name not in sys.modules:
+ sys.modules[lib_name] = importlib.import_module(f'{lib_name}')
+ return sys.modules[lib_name].ASRProvider(*args, **kwargs)
- if cls := cls_map.get(class_name):
- return cls(*args, **kwargs)
- raise ValueError(f"不支持的ASR类型: {class_name}")
+ raise ValueError(f"不支持的ASR类型: {class_name},请检查该配置的type是否设置正确")
\ No newline at end of file
diff --git a/core/websocket_server.py b/core/websocket_server.py
index d8dce04d..8e09e0fe 100644
--- a/core/websocket_server.py
+++ b/core/websocket_server.py
@@ -7,6 +7,7 @@ from core.utils import asr, vad, llm, tts
TAG = __name__
+
class WebSocketServer:
def __init__(self, config: dict):
self.config = config
@@ -21,7 +22,10 @@ class WebSocketServer:
self.config["VAD"][self.config["selected_module"]["VAD"]]
),
asr.create_instance(
- self.config["selected_module"]["ASR"],
+ self.config["selected_module"]["ASR"]
+ if not 'type' in self.config["ASR"][self.config["selected_module"]["ASR"]]
+ else
+ self.config["ASR"][self.config["selected_module"]["ASR"]]["type"],
self.config["ASR"][self.config["selected_module"]["ASR"]],
self.config["delete_audio"]
),
diff --git a/docker-compose-example.yml b/docker-compose-example.yml
deleted file mode 100755
index ab97b6eb..00000000
--- a/docker-compose-example.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-services:
- xiaozhi-esp32-server:
- image: ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest
- container_name: xiaozhi-esp32-server
- restart: always
- #security_opt:
- # - seccomp:unconfined
- ports:
- - "8000:8000" # ws服务端
- - "8002:8002" # 管理后台
- volumes:
- # 配置文件 记得创建文件夹
- - ./data:/opt/xiaozhi-esp32-server/data
- # 模型文件 //可以挂载本地下载好的模型
- #- ./models:/opt/xiaozhi-esp32-server/models
- # 临时文件 //可以看log和音频文件 记得创建文件夹
- #- ./tmp:/opt/xiaozhi-esp32-server/tmp
diff --git a/docker-compose.yml b/docker-compose.yml
index 49e26d81..d56d8bba 100755
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,13 +1,17 @@
services:
xiaozhi-esp32-server:
- image: ghcr.io/kalicyh/xiaozhi-esp32-server:latest
+ image: ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:latest
container_name: xiaozhi-esp32-server
restart: always
- #security_opt:
- # - seccomp:unconfined
+ security_opt:
+ - seccomp:unconfined
ports:
+ # ws服务端
- "8000:8000"
+ # 管理后台
- "8002:8002"
volumes:
+ # 配置文件目录
- ./data:/app/data
+ # 模型文件挂接,很重要
- ./models/SenseVoiceSmall/model.pt:/app/models/SenseVoiceSmall/model.pt
\ No newline at end of file
diff --git a/docs/Deployment-silm.md b/docs/Deployment-silm.md
deleted file mode 100644
index 404106ef..00000000
--- a/docs/Deployment-silm.md
+++ /dev/null
@@ -1,217 +0,0 @@
-# 部署指南
-
-本项目支持 **x86** 和 **arm64** 架构,可运行于国产操作系统。
-
-## 方式一:快速部署(推荐)
-
-### 1. 安装 Docker
-
-如果未安装 Docker,可参考 [Docker 安装教程](https://www.runoob.com/docker/ubuntu-docker-install.html)。
-
-### 2. 创建项目目录
-
-选择一个空目录作为 **项目目录**。
-
-### 3. 下载配置文件
-
-访问 [config.yaml](https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/config.yaml),点击 `RAW` 旁的下载按钮,保存到 **项目目录**。
-
-### 4. 运行 Docker
-
-打开终端,`cd` 进入 **项目目录**,执行:
-
-```sh
-docker run -d --name xiaozhi-esp32-server --restart always --security-opt seccomp:unconfined \
- -p 8000:8000 \
- -v $(pwd)/config.yaml:/opt/xiaozhi-esp32-server/config.yaml \
- ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest
-```
-
-### 5. 确认运行状态
-
-```sh
-docker ps # 查看容器状态
-docker logs -f xiaozhi-esp32-server # 查看日志
-```
-
-## 方式二:使用 Docker Compose 部署
-
-### 1. 创建 `docker-compose.yml`
-在 `项目目录` 下创建 `docker-compose.yml` 文件,内容如下:
-
-```yaml
-version: '3'
-services:
- xiaozhi-esp32-server:
- image: ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest
- container_name: xiaozhi-esp32-server
- restart: always
- security_opt:
- - seccomp:unconfined
- ports:
- - "8000:8000"
- volumes:
- - ./config.yaml:/opt/xiaozhi-esp32-server/config.yaml
-```
-
-### 2. 启动服务
-```sh
-docker-compose up -d
-```
-
-### 3. 确认运行状态
-```sh
-docker-compose ps
-docker-compose logs -f
-```
-
-### 4. 停止和删除容器
-```sh
-docker-compose down
-```
-
-### 5. 版本升级
-
-#### 备份配置文件
-```sh
-cp config.yaml config_backup.yaml
-```
-
-#### 更新 Docker 镜像
-```sh
-docker-compose pull
-docker-compose down
-docker-compose up -d
-```
-
-
-## 方式三:源码部署(免环境部署/可修改代码)
-
-### 1. 下载源码
-
-```sh
-git clone https://github.com/xinnan-tech/xiaozhi-esp32-server.git
-cd xiaozhi-esp32-server
-```
-
-或手动下载 [ZIP 包](https://github.com/xinnan-tech/xiaozhi-esp32-server/archive/refs/heads/main.zip),解压后重命名为 `xiaozhi-esp32-server`。
-
-## 2.运行docker
-
-修改完配置后,打开命令行工具,`cd`进入到你的项目目录下,执行以下命令
-
-```sh
-docker run -it --name xiaozhi-env --restart always --security-opt seccomp:unconfined \
- -p 8000:8000 \
- -p 8002:8002 \
- -v ./:/app \
- kalicyh/poetry:v3.10_latest
-```
-
-然后就和正常开发一样了
-
-## 3.安装依赖
-
-在刚刚的打开的终端运行
-
-```sh
-poetry install --no-root
-```
-
-```sh
-apt-get update
-apt-get install -y --no-install-recommends libopus0 ffmpeg
-```
-
-速度慢可以尝试使用清华镜像
-
-```sh
-echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm main contrib non-free non-free-firmware" > /etc/apt/sources.list
-echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm-updates main contrib non-free non-free-firmware" >> /etc/apt/sources.list
-echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm-backports main contrib non-free non-free-firmware" >> /etc/apt/sources.list
-echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian-security bookworm-security main contrib non-free non-free-firmware" >> /etc/apt/sources.list
-apt-get update
-apt-get install -y --no-install-recommends libopus0 ffmpeg
-```
-
-## 4.运行项目
-
-```sh
-poetry run python app.py
-```
-
-## 方式三:本地运行(适用于开发)
-
-### 1. 安装环境
-
-```sh
-conda create -n xiaozhi-esp32-server python=3.10 -y
-conda activate xiaozhi-esp32-server
-```
-
-#### Mac/Windows:
-
-```sh
-conda install conda-forge::libopus conda-forge::ffmpeg
-```
-
-#### Ubuntu:
-
-```sh
-apt-get install libopus0 ffmpeg
-```
-
-### 2. 下载源码 & 安装依赖
-
-```sh
-git clone https://github.com/xinnan-tech/xiaozhi-esp32-server.git
-cd xiaozhi-esp32-server
-pip install -r requirements.txt
-```
-
-### 3. 运行项目
-
-```sh
-python app.py
-```
-
-## 版本升级
-
-```sh
-docker stop xiaozhi-esp32-server
-docker rm xiaozhi-esp32-server
-docker rmi ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest
-```
-
-然后重新按照 **方式一** 或 **方式二** 运行 Docker。
-
-## 重要配置
-
-修改 `config.yaml` 以适配不同 LLM 和 TTS 组件。
-
-示例:
-
-```yaml
-selected_module:
- ASR: FunASR
- VAD: SileroVAD
- LLM: ChatGLMLLM
- TTS: EdgeTTS
-```
-
-如需使用 `Dify` 或 `DeepSeekLLM`,修改 `LLM` 部分并填写密钥。
-
-## 模型文件下载
-
-语音转文字默认使用 `SenseVoiceSmall`,需下载 `model.pt` 并存放至 `model/SenseVoiceSmall` 目录。
-
-- [阿里魔塔下载](https://modelscope.cn/models/iic/SenseVoiceSmall/resolve/master/model.pt)
-- [百度网盘下载](https://pan.baidu.com/share/init?surl=QlgM58FHhYv1tFnUT_A8Sg&pwd=qvna) 提取码:`qvna`
-
----
-
-如遇问题,可参考 `docker logs -f xiaozhi-esp32-server` 进行排查。
-
-**至此,部署完成!** 🚀
-
-
diff --git a/docs/Deployment.md b/docs/Deployment.md
index 02c0ec41..a1f31fdf 100644
--- a/docs/Deployment.md
+++ b/docs/Deployment.md
@@ -1,30 +1,172 @@
+# 方式一:docker快速部署
-# 本地源码运行
+docker镜像已支持x86架构、arm64架构的CPU,支持在国产操作系统上运行。
+
+## 1. 安装docker
+
+如果您的电脑还没安装docker,可以按照这里的教程安装:[docker安装](https://www.runoob.com/docker/ubuntu-docker-install.html)
+
+## 2. 创建目录
+
+安装完后,你需要为这个项目找一个安放配置文件的目录,我们暂且称它为`项目目录`,这个目录最好是一个新建的空的目录。
+
+创建好目录后,你需要在`项目目录`下面创建`data`文件夹和`models`文件夹,`models`下面还要再创建`SenseVoiceSmall`文件夹。
+
+最终目录结构如下所示:
+
+```
+你的项目根目录
+ ├─ data
+ ├─ models
+ ├─ SenseVoiceSmall
+```
+
+## 4. 下载语音识别模型文件
+
+你需要下载语音识别的模型文件,因为本项目的默认语音识别用的是本地离线语音识别方案。可通过这个方式下载
+[跳转到下载语音识别模型文件](#模型文件)
+
+下载完后,回到本教程。
+
+## 3. 下载docker-compose.yaml
+
+用浏览器打开[这个链接](https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docker-compose.yml)。
+
+在页面的右侧找到名称为`RAW`按钮,在`RAW`按钮的旁边,找到下载的图标,点击下载按钮,下载`docker-compose.yml`文件。 把文件下载到你的
+`项目目录`中。
+
+下载完后,回到本教程继续往下。
+
+## 3. 下载配置文件
+
+用浏览器打开[这个链接](https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/config.yaml)。
+
+在页面的右侧找到名称为`RAW`按钮,在`RAW`按钮的旁边,找到下载的图标,点击下载按钮,下载`config.yaml`文件。 把文件下载到你的
+`项目目录`下面的`data`文件夹中,然后把`config.yaml`文件重命名为`.config.yaml`。
+
+下载完配置文件后,我们确认一下整个`项目目录`里面的文件如下所示:
+
+```
+你的项目根目录
+ ├─ docker-compose.yml
+ ├─ data
+ ├─ .config.yaml
+ ├─ models
+ ├─ SenseVoiceSmall
+ ├─ model.pt
+```
+
+如果你的文件目录结构也是上面的,就继续往下。如果不是,你就再仔细看看是不是漏操作了什么。
+
+## 4. 配置项目文件
+
+接下里,程序还不能直接运行,你需要配置一下,你到底使用的是什么模型。你可以看这个教程:
+[跳转到配置项目文件](#配置项目)
+
+配置完项目文件后,回到本教程继续往下。
+
+## 5. 执行docker命令
+
+打开命令行工具,使用`终端`或`命令行`工具 进入到你的`项目目录`,执行以下命令
+
+```
+docker-compose up -d
+```
+
+执行完后,再执行以下命令,查看日志信息。
+
+```
+docker logs -f xiaozhi-esp32-server
+```
+
+这时,你就要留意日志信息,可以根据这个教程,判断是否成功了。[跳转到运行状态确认](#运行状态确认)
+
+## 6.版本升级操作
+
+如果后期想升级版本,可以这么操作
+
+1、备份好`data`文件夹中的`.config.yaml`文件,一些关键的配置到时复制到新的`.config.yaml`文件里。
+请注意是对关键密钥逐个复制,不要直接覆盖。因为新的`.config.yaml`文件可能有一些新的配置项,旧的`.config.yaml`文件不一定有。
+
+2、执行以下命令
+
+```
+docker stop xiaozhi-esp32-server
+docker rm xiaozhi-esp32-server
+docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:latest
+```
+
+3、重新按docker方式部署
+
+# 方式二:借助Docker环境运行部署
+
+开发人员如果不想安装`conda`环境,可以使用这种方法管理好依赖。
+
+## 1.克隆项目
+
+## 2.[跳转到下载语音识别模型文件](#模型文件)
+
+## 3.[跳转到配置项目文件](#配置项目)
+
+## 4.运行docker
+
+修改完配置后,打开命令行工具,`cd`进入到你的项目目录下,执行以下命令
+
+```sh
+docker run -it --name xiaozhi-env --restart always --security-opt seccomp:unconfined \
+ -p 8000:8000 \
+ -p 8002:8002 \
+ -v ./:/app \
+ ccr.ccs.tencentyun.com/kalicyh/poetry:v3.10_latest
+```
+
+然后就和正常开发一样了
+
+## 5.安装依赖
+
+在刚刚的打开的终端运行
+
+```sh
+poetry install --no-root
+```
+
+```sh
+apt-get update
+apt-get install -y --no-install-recommends libopus0 ffmpeg
+```
+
+速度慢可以尝试使用清华镜像
+
+```sh
+echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm main contrib non-free non-free-firmware" > /etc/apt/sources.list
+echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm-updates main contrib non-free non-free-firmware" >> /etc/apt/sources.list
+echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm-backports main contrib non-free non-free-firmware" >> /etc/apt/sources.list
+echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian-security bookworm-security main contrib non-free non-free-firmware" >> /etc/apt/sources.list
+apt-get update
+apt-get install -y --no-install-recommends libopus0 ffmpeg
+```
+
+## 6.运行项目
+
+```sh
+poetry run python app.py
+```
+
+# 方式三:本地源码运行
## 1.安装基础环境
-本项目使用`conda`管理依赖环境,安装好后,开始执行以下命令。
+本项目使用`conda`管理依赖环境。如果不方便安装`conda`,需要根据实际的操作系统安装好`libopus`和`ffmpeg`。
+如果确定使用`conda`,则安装好后,开始执行以下命令。
```
conda remove -n xiaozhi-esp32-server --all -y
conda create -n xiaozhi-esp32-server python=3.10 -y
conda activate xiaozhi-esp32-server
-```
-
-执行以上命令后, 如果你的电脑是Windows或Mac,执行下面的语句:
-
-```
-conda activate xiaozhi-esp32-server
conda install conda-forge::libopus
conda install conda-forge::ffmpeg
```
-如果你的电脑是ubuntu,执行下面的语句:
-
-```
-apt-get install libopus0 ffmpeg
-```
-
## 2.安装本项目依赖
你先要下载本项目源码,源码可以通过`git clone`命令下载,如果你不熟悉`git clone`命令。
@@ -43,9 +185,17 @@ pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/
pip install -r requirements.txt
```
-## 3.[跳转到下载语音识别模型文件](#模型文件)
+## 3.下载语音识别模型文件
-## 4.[跳转到配置项目文件](#配置项目)
+你需要下载语音识别的模型文件,因为本项目的默认语音识别用的是本地离线语音识别方案。可通过这个方式下载
+[跳转到下载语音识别模型文件](#模型文件)
+
+下载完后,回到本教程。
+
+## 4.配置项目文件
+
+接下里,程序还不能直接运行,你需要配置一下,你到底使用的是什么模型。你可以看这个教程:
+[跳转到配置项目文件](#配置项目)
## 5.运行项目
@@ -55,25 +205,20 @@ conda activate xiaozhi-esp32-server
python app.py
```
-## 6.[跳转到运行状态确认](#运行状态确认)
+这时,你就要留意日志信息,可以根据这个教程,判断是否成功了。[跳转到运行状态确认](#运行状态确认)
# 汇总
## 配置项目
-修改`config.yaml`文件,配置本项目所需的各种参数。默认的LLM使用的是`ChatGLMLLM`
+如果你的`项目目录`目录没有`data`,你需要创建`data`目录。
+如果你的`data`下面没有`.config.yaml`文件,你可以把源码目录下的`config.yaml`文件复制一份,重命名为`.config.yaml`
+
+修改`项目目录`下`data`目录下的`.config.yaml`文件,配置本项目所需的各种参数。默认的LLM使用的是`ChatGLMLLM`
,你需要配置密钥,因为他们的模型,虽然有免费的,但是仍要去[官网](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)注册密钥,才能启动。
默认的TTS使用的是`EdgeTTS`,这个无需配置,如果你需要更换成`豆包TTS`,则需要配置密钥。
-```
-# 如果您是一名开发者,建议阅读以下内容。如果不是开发者,可以忽略这部分内容。
-# 在开发中,在项目根目录创建data目录,将【config.yaml】复制一份,改成【.config.yaml】,放进data目录中
-# 系统会优先读取【data/.config.yaml】文件的配置。
-# 这样做,可以避免在提交代码的时候,错误地提交密钥信息,保护您的密钥安全。
-```
-
配置说明:这里是各个功能使用的默认组件,例如LLM默认使用`ChatGLMLLM`模型。如果需要切换模型,就是改对应的名称。
-
本项目的默认配置仅是成本最低配置(`glm-4-flash`和`EdgeTTS`都是免费的),如果需要更优的更快的搭配,需要自己结合部署环境切换各组件的使用。
```
@@ -104,7 +249,7 @@ LLM:
## 模型文件
-下载源码后,需要下载模型文件。 默认使用`SenseVoiceSmall`模型,进行语音转文字。因为模型较大,需要独立下载,下载后把`model.pt`
+本项目语音识别模型,默认使用`SenseVoiceSmall`模型,进行语音转文字。因为模型较大,需要独立下载,下载后把`model.pt`
文件放在`model/SenseVoiceSmall`
目录下。下面两个下载路线任选一个。
@@ -112,27 +257,21 @@ LLM:
- 线路二:百度网盘下载[SenseVoiceSmall](https://pan.baidu.com/share/init?surl=QlgM58FHhYv1tFnUT_A8Sg&pwd=qvna) 提取码:
`qvna`
-huggingface地址:
-
-```
-https://huggingface.co/jinaai/jina-embeddings-v2-base-zh?clone=true
-https://huggingface.co/FunAudioLLM/SenseVoiceSmall/tree/main?clone=true
-```
-
-镜像地址`https://hf-mirror.com/`
-
-
## 运行状态确认
如果你能看到,类似以下日志,则是本项目服务启动成功的标志。
```
-2025-xx-xx xx:51:59,492 - core.server - INFO - Server is running at ws://xx.xx.xx.xxx:8000
-2025-xx-xx xx:51:59,516 - websockets.server - INFO - server listening on 0.0.0.0:8000
+25-02-23 12:01:09[core.websocket_server] - INFO - Server is running at ws://xxx.xx.xx.xx:8000
+25-02-23 12:01:09[core.websocket_server] - INFO - =======上面的地址是websocket协议地址,请勿用浏览器访问=======
```
-接下来,你就可以开始 `编译esp32固件`了,请往下翻,翻到编译`esp32固件`相关章节。
-正常来说,假设你的ip是`192.168.1.25`,那么你的接口地址就是:`ws://192.168.1.25:8000`。这个信息很有用的,后面`编译esp32固件`
-需要用到。
+正常来说,如果您是通过源码运行本项目,日志会有你的接口地址信息。
+但是如果你用docker部署,那么你的日志里给出的接口地址信息就不是真实的接口地址。
-请注意,你的接口地址是`websocket`协议的地址,你可以使用`apifox`等工具调试。但是不能直接用浏览器打开访问,如果用浏览器打开,日志会显示错误,会让你怀疑是否部署成功了。
+最正确的方法,是根据电脑的局域网IP来确定你的接口地址。
+如果你的电脑的局域网IP比如是`192.168.1.25`,那么你的接口地址就是:`ws://192.168.1.25:8000`。
+
+这个信息很有用的,后面`编译esp32固件`需要用到。
+
+接下来,你就可以开始 [编译esp32固件](firmware-build.md)了。
\ No newline at end of file
diff --git a/docs/Deployment_old.md b/docs/Deployment_old.md
deleted file mode 100644
index 44a54a43..00000000
--- a/docs/Deployment_old.md
+++ /dev/null
@@ -1,250 +0,0 @@
-# 方式一:docker快速部署
-
-docker镜像已支持x86架构、arm64架构的CPU,支持在国产操作系统上运行。
-
-## 1. 安装docker
-
-如果您的电脑还没安装docker,可以按照这里的教程安装:[docker安装](https://www.runoob.com/docker/ubuntu-docker-install.html)
-
-## 2. 创建目录
-
-安装完后,你需要为这个项目找一个安放配置文件的目录,我们暂且称它为`项目目录`,这个目录最好是一个新建的空的目录。
-
-## 3. 下载配置文件
-
-用浏览器打开[这个链接](https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/config.yaml)。
-
-在页面的右侧找到名称为`RAW`按钮,在`RAW`按钮的旁边,找到下载的图标,点击下载按钮,下载`config.yaml`文件。 把文件下载到你的
-`项目目录`。
-
-## 4.[跳转到配置项目文件](#配置项目)
-
-## 5. 执行docker命令
-
-打开命令行工具,`cd` 进入到你的`项目目录`,执行以下命令
-
-```
-#如果你是linux,执行
-ls
-#如果你是windows,执行
-dir
-```
-
-如果你能看到`config.yaml`文件,确确实实进入到了`项目目录`,接着执行以下命令:
-
-```
-docker run -d --name xiaozhi-esp32-server --restart always --security-opt seccomp:unconfined -p 8000:8000 -v $(pwd)/config.yaml:/opt/xiaozhi-esp32-server/config.yaml ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest
-```
-
-## 6.[跳转到运行状态确认](#运行状态确认)
-
-## [跳转到版本升级操作](#版本升级操作)
-
-# 方式二:借助docker环境运行部署(仅限开发人员/小白勿用)
-
-## 1.克隆项目
-
-## 2.[跳转到下载语音识别模型文件](#模型文件)
-
-## 3.[跳转到配置项目文件](#配置项目)
-
-## 4.运行docker
-
-修改完配置后,打开命令行工具,`cd`进入到你的项目目录下,执行以下命令
-
-```sh
-docker run -it --name xiaozhi-env --restart always --security-opt seccomp:unconfined \
- -p 8000:8000 \
- -p 8002:8002 \
- -v ./:/app \
- ccr.ccs.tencentyun.com/kalicyh/poetry:v3.10_latest
-```
-
-然后就和正常开发一样了
-
-## 5.安装依赖
-
-在刚刚的打开的终端运行
-
-```sh
-poetry install --no-root
-```
-
-```sh
-apt-get update
-apt-get install -y --no-install-recommends libopus0 ffmpeg
-```
-
-速度慢可以尝试使用清华镜像
-
-```sh
-echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm main contrib non-free non-free-firmware" > /etc/apt/sources.list
-echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm-updates main contrib non-free non-free-firmware" >> /etc/apt/sources.list
-echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm-backports main contrib non-free non-free-firmware" >> /etc/apt/sources.list
-echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian-security bookworm-security main contrib non-free non-free-firmware" >> /etc/apt/sources.list
-apt-get update
-apt-get install -y --no-install-recommends libopus0 ffmpeg
-```
-
-## 6.运行项目
-
-```sh
-poetry run python app.py
-```
-
-# 方式三:本地源码运行
-
-## 1.安装基础环境
-
-本项目使用`conda`管理依赖环境,安装好后,开始执行以下命令。
-
-```
-conda remove -n xiaozhi-esp32-server --all -y
-conda create -n xiaozhi-esp32-server python=3.10 -y
-conda activate xiaozhi-esp32-server
-```
-
-执行以上命令后, 如果你的电脑是Windows或Mac,执行下面的语句:
-
-```
-conda activate xiaozhi-esp32-server
-conda install conda-forge::libopus
-conda install conda-forge::ffmpeg
-```
-
-如果你的电脑是ubuntu,执行下面的语句:
-
-```
-apt-get install libopus0 ffmpeg
-```
-
-## 2.安装本项目依赖
-
-你先要下载本项目源码,源码可以通过`git clone`命令下载,如果你不熟悉`git clone`命令。
-
-你可以用浏览器打开这个地址`https://github.com/xinnan-tech/xiaozhi-esp32-server.git`
-
-打开完,找到页面中一个绿色的按钮,写着`Code`的按钮,点开它,然后你就看到`Download ZIP`的按钮。
-
-点击它,下载本项目源码压缩包。下载到你电脑后,解压它,此时它的名字可能叫`xiaozhi-esp32-server-main`
-你需要把它重命名成`xiaozhi-esp32-server`,好了请记住这个目录,我们暂且称它为`项目目录`。
-
-```
-# 使用dos或者终端,进入到你的项目目录,执行以下命令
-conda activate xiaozhi-esp32-server
-pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/
-pip install -r requirements.txt
-```
-
-## 3.[跳转到下载语音识别模型文件](#模型文件)
-
-## 4.[跳转到配置项目文件](#配置项目)
-
-## 5.运行项目
-
-```
-# 确保在本项目的根目录下执行
-conda activate xiaozhi-esp32-server
-python app.py
-```
-
-## 6.[跳转到运行状态确认](#运行状态确认)
-
-# 汇总
-
-## 配置项目
-
-修改`config.yaml`文件,配置本项目所需的各种参数。默认的LLM使用的是`ChatGLMLLM`
-,你需要配置密钥,因为他们的模型,虽然有免费的,但是仍要去[官网](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)注册密钥,才能启动。
-默认的TTS使用的是`EdgeTTS`,这个无需配置,如果你需要更换成`豆包TTS`,则需要配置密钥。
-
-```
-# 如果您是一名开发者,建议阅读以下内容。如果不是开发者,可以忽略这部分内容。
-# 在开发中,在项目根目录创建data目录,将【config.yaml】复制一份,改成【.config.yaml】,放进data目录中
-# 系统会优先读取【data/.config.yaml】文件的配置。
-# 这样做,可以避免在提交代码的时候,错误地提交密钥信息,保护您的密钥安全。
-```
-
-配置说明:这里是各个功能使用的默认组件,例如LLM默认使用`ChatGLMLLM`模型。如果需要切换模型,就是改对应的名称。
-
-本项目的默认配置仅是成本最低配置(`glm-4-flash`和`EdgeTTS`都是免费的),如果需要更优的更快的搭配,需要自己结合部署环境切换各组件的使用。
-
-```
-selected_module:
- ASR: FunASR
- VAD: SileroVAD
- LLM: ChatGLMLLM
- TTS: EdgeTTS
-```
-
-比如修改`LLM`使用的组件,就看本项目支持哪些`LLM` API接口,当前支持的是`openai`、`dify`。欢迎验证和支持更多LLM平台的接口。
-使用时,在`selected_module`修改成对应的如下LLM配置的名称:
-
-```
-LLM:
- DeepSeekLLM:
- type: openai
- ...
- ChatGLMLLM:
- type: openai
- ...
- DifyLLM:
- type: dify
- ...
-```
-
-有些服务,比如如果你使用`Dify`、`豆包的TTS`,是需要密钥的,记得在配置文件加上哦!
-
-## 模型文件
-
-下载源码后,需要下载模型文件。 默认使用`SenseVoiceSmall`模型,进行语音转文字。因为模型较大,需要独立下载,下载后把`model.pt`
-文件放在`model/SenseVoiceSmall`
-目录下。下面两个下载路线任选一个。
-
-- 线路一:阿里魔塔下载[SenseVoiceSmall](https://modelscope.cn/models/iic/SenseVoiceSmall/resolve/master/model.pt)
-- 线路二:百度网盘下载[SenseVoiceSmall](https://pan.baidu.com/share/init?surl=QlgM58FHhYv1tFnUT_A8Sg&pwd=qvna) 提取码:
- `qvna`
-
-
-## 运行状态确认
-
-如果首次执行,可能需要几分钟时间,你要耐心等待他完成拉取。正常拉取完成后,你可以在命令行执行以下命令查看服务是否启动成功
-
-```
-docker ps
-```
-
-如果你能看到`xiaozhi-server`,说明服务启动成功。那你还可以进一步执行以下命令,查看服务的日志
-
-```
-docker logs -f xiaozhi-esp32-server
-```
-
-如果你能看到,类似以下日志,则是本项目服务启动成功的标志。
-
-```
-2025-xx-xx xx:51:59,492 - core.server - INFO - Server is running at ws://xx.xx.xx.xxx:8000
-2025-xx-xx xx:51:59,516 - websockets.server - INFO - server listening on 0.0.0.0:8000
-```
-
-接下来,你就可以开始 `编译esp32固件`了,请往下翻,翻到编译`esp32固件`相关章节。那么由于你是用docker部署,你要自己查看自己本机电脑的ip是多少。
-正常来说,假设你的ip是`192.168.1.25`,那么你的接口地址就是:`ws://192.168.1.25:8000`。这个信息很有用的,后面`编译esp32固件`
-需要用到。
-
-请注意,你的接口地址是`websocket`协议的地址,你可以使用`apifox`等工具调试。但是不能直接用浏览器打开访问,如果用浏览器打开,日志会显示错误,会让你怀疑是否部署成功了。
-
-## 版本升级操作
-
-如果想升级版本,可以这么操作
-
-1、备份好`config.yaml`文件,一些关键的配置到时复制到新的`config.yaml`文件里。
-
-2、执行以下命令
-
-```
-docker stop xiaozhi-esp32-server
-docker rm xiaozhi-esp32-server
-docker rmi ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest
-```
-
-3、重新开始安装
\ No newline at end of file
diff --git a/docs/docker-build.md b/docs/docker-build.md
index a4ed16db..ac14dab2 100644
--- a/docs/docker-build.md
+++ b/docs/docker-build.md
@@ -1,4 +1,7 @@
-# 编译docker镜像
+# 本地编译docker镜像方法
+
+现在本项目已经使用github自动编译docker功能,本文档是提供给有本地编译docker镜像需求的朋友准备的。
+
1、安装docker
```
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
@@ -6,7 +9,7 @@ sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin
2、编译docker镜像
```
# 普通编译
-docker build -t xiaozhi-esp32-server:local -f ./Dockerfile .
+docker build -t xiaozhi-esp32-server:local -f ./Dockerfile-pip .
```
3、测试本地镜像
```
diff --git a/docs/images/demo.png b/docs/images/demo.png
deleted file mode 100644
index 0fe4ebce..00000000
Binary files a/docs/images/demo.png and /dev/null differ
diff --git a/docs/images/demo1.png b/docs/images/demo1.png
new file mode 100644
index 00000000..9878827f
Binary files /dev/null and b/docs/images/demo1.png differ
diff --git a/docs/images/demo2.png b/docs/images/demo2.png
new file mode 100644
index 00000000..6b30494f
Binary files /dev/null and b/docs/images/demo2.png differ
diff --git a/docs/images/demo3.png b/docs/images/demo3.png
new file mode 100644
index 00000000..e1bc859a
Binary files /dev/null and b/docs/images/demo3.png differ
diff --git a/docs/images/demo4.png b/docs/images/demo4.png
new file mode 100644
index 00000000..7b57ab25
Binary files /dev/null and b/docs/images/demo4.png differ
diff --git a/docs/images/demo5.png b/docs/images/demo5.png
new file mode 100644
index 00000000..8e03685b
Binary files /dev/null and b/docs/images/demo5.png differ