update:旧非流失兼容改造

This commit is contained in:
hrz
2025-05-26 02:20:38 +08:00
parent 40632019ac
commit 24526ad206
10 changed files with 355 additions and 273 deletions
+183 -110
View File
@@ -2,12 +2,12 @@ import asyncio
from config.logger import setup_logging
import queue
import os
import json
import uuid
import threading
from enum import Enum
from core.utils import p3
from core.utils import textUtils
from core.handle.sendAudioHandle import sendAudioMessage
from core.handle.reportHandle import enqueue_tts_report
from core.providers.tts.dto.dto import TTSMessageDTO, SentenceType, ContentType
from abc import ABC, abstractmethod
from core.utils.tts import MarkdownCleaner
from core.utils.util import audio_to_data
@@ -16,30 +16,39 @@ TAG = __name__
logger = setup_logging()
class TTSImplementationType(Enum):
"""TTS实现类型枚举"""
NON_STREAMING = "non_streaming" # 非流式实现
SINGLE_STREAMING = "single_streaming" # 单流式实现
DOUBLE_STREAMING = "double_streaming" # 双流式实现
class TTSProviderBase(ABC):
def __init__(self, config, delete_audio_file):
self.conn = None
self.tts_timeout = 10
self.delete_audio_file = delete_audio_file
self.output_file = config.get("output_dir")
self.tts_queue = queue.Queue()
self.audio_play_queue = queue.Queue()
# 添加实现类型属性,默认为非流式
self.interface_type = TTSImplementationType.NON_STREAMING
self.tts_text_queue = queue.Queue()
self.tts_audio_queue = queue.Queue()
self.stop_event = threading.Event()
self.loop = asyncio.get_event_loop()
self.tts_text_buff = []
self.punctuations = (
"",
".",
"",
"?",
"",
"!",
"",
";",
"",
)
self.first_sentence_punctuations = ("", ",")
self.tts_stop_request = False
self.processed_chars = 0
self.is_first_sentence = True
@abstractmethod
def generate_filename(self):
pass
def to_tts(self, text, index):
def to_tts(self, text):
tmp_file = self.generate_filename()
try:
max_repeat_time = 5
@@ -82,12 +91,51 @@ class TTSProviderBase(ABC):
"""音频文件转换为Opus编码"""
return audio_to_data(audio_file_path, is_opus=True)
def tts_one_sentence(
self,
conn,
content_type,
content_detail=None,
content_file=None,
sentence_id=None,
):
"""发送一句话"""
if not sentence_id:
if conn.sentence_id:
sentence_id = conn.sentence_id
else:
sentence_id = str(uuid.uuid4()).replace("-", "")
conn.sentence_id = sentence_id
self.tts_text_queue.put(
TTSMessageDTO(
sentence_id=sentence_id,
sentence_type=SentenceType.FIRST,
content_type=ContentType.ACTION,
)
)
self.tts_text_queue.put(
TTSMessageDTO(
sentence_id=sentence_id,
sentence_type=SentenceType.MIDDLE,
content_type=content_type,
content_detail=content_detail,
content_file=content_file,
)
)
self.tts_text_queue.put(
TTSMessageDTO(
sentence_id=sentence_id,
sentence_type=SentenceType.LAST,
content_type=ContentType.ACTION,
)
)
async def open_audio_channels(self, conn):
self.conn = conn
self.tts_timeout = conn.config.get("tts_timeout", 10)
# tts 消化线程
self.tts_priority_thread = threading.Thread(
target=self._tts_priority_thread, daemon=True
target=self._tts_text_priority_thread, daemon=True
)
self.tts_priority_thread.start()
@@ -97,113 +145,60 @@ class TTSProviderBase(ABC):
)
self.audio_play_priority_thread.start()
def _tts_priority_thread(self):
while not self.conn.stop_event.is_set():
text = None
# 这里默认是非流式的处理方式
# 流式处理方式请在子类中重写
def _tts_text_priority_thread(self):
while not self.stop_event.is_set():
try:
try:
item = self.tts_queue.get(timeout=1)
if item is None:
continue
future, text_index = item # 解包获取 Future 和 text_index
except queue.Empty:
if self.conn.stop_event.is_set():
break
continue
if future is None:
continue
text = None
audio_datas, tts_file = [], None
try:
logger.bind(tag=TAG).debug("正在处理TTS任务...")
tts_file, text, _ = future.result(timeout=self.tts_timeout)
# 如果tts_file返回流式标识,则不继续处理
if tts_file is None:
logger.bind(tag=TAG).error(
f"TTS出错: file is empty: {text_index}: {text}"
message = self.tts_text_queue.get()
if message.sentence_type == SentenceType.FIRST:
# 初始化参数
self.tts_stop_request = False
self.processed_chars = 0
self.tts_text_buff = []
self.is_first_sentence = True
elif ContentType.TEXT == message.content_type:
self.tts_text_buff.append(message.content_detail)
segment_text = self._get_segment_text()
if segment_text:
tts_file = self.to_tts(segment_text)
audio_datas = self._process_audio_file(tts_file)
self.tts_audio_queue.put(
(message.sentence_type, audio_datas, segment_text)
)
elif (
tts_file == TTSImplementationType.SINGLE_STREAMING.value
or tts_file == TTSImplementationType.DOUBLE_STREAMING.value
):
logger.bind(tag=TAG).debug(f"TTS生成:流式标识: {tts_file}")
continue
else:
logger.bind(tag=TAG).debug(f"TTS生成:文件路径: {tts_file}")
if os.path.exists(tts_file):
if tts_file.endswith(".p3"):
audio_datas, _ = p3.decode_opus_from_file(tts_file)
elif self.conn.audio_format == "pcm":
audio_datas, _ = self.audio_to_pcm_data(tts_file)
else:
audio_datas, _ = self.audio_to_opus_data(tts_file)
# 在这里上报TTS数据
enqueue_tts_report(
self.conn,
tts_file if text is None else text,
audio_datas,
)
else:
logger.bind(tag=TAG).error(f"TTS出错:文件不存在{tts_file}")
except TimeoutError:
logger.bind(tag=TAG).error("TTS超时")
except Exception as e:
import traceback
elif ContentType.FILE == message.content_type:
self._process_remaining_text()
error_info = {
"error_type": type(e).__name__,
"error_message": str(e),
"stack_trace": traceback.format_exc(),
"text_index": text_index,
"text": text,
"tts_file": tts_file,
"audio_format": getattr(self.conn, "audio_format", None),
"interface_type": self.interface_type.value,
}
logger.bind(tag=TAG).error(
f"TTS处理出错: {json.dumps(error_info, ensure_ascii=False)}"
tts_file = message.content_file
audio_datas = self._process_audio_file(tts_file)
self.tts_audio_queue.put(
(message.sentence_type, audio_datas, message.content_detail)
)
if not self.conn.client_abort:
# 如果没有中途打断就发送语音
self.audio_play_queue.put((audio_datas, text, text_index))
if (
self.delete_audio_file
and tts_file is not None
and os.path.exists(tts_file)
and tts_file.startswith(self.output_file)
):
os.remove(tts_file)
if message.sentence_type == SentenceType.LAST:
self._process_remaining_text()
self.tts_audio_queue.put(
(message.sentence_type, [], message.content_detail)
)
except Exception as e:
logger.bind(tag=TAG).error(f"TTS任务处理错误: {e}")
self.conn.clearSpeakStatus()
asyncio.run_coroutine_threadsafe(
self.conn.websocket.send(
json.dumps(
{
"type": "tts",
"state": "stop",
"session_id": self.conn.session_id,
}
)
),
self.conn.loop,
)
logger.bind(tag=TAG).error(f"tts_priority priority_thread: {text} {e}")
logger.bind(tag=TAG).error(f"Failed to process TTS text: {e}")
def _audio_play_priority_thread(self):
while not self.conn.stop_event.is_set():
while not self.stop_event.is_set():
text = None
try:
try:
audio_datas, text, text_index = self.audio_play_queue.get(timeout=1)
sentence_type, audio_datas, text = self.tts_audio_queue.get(
timeout=1
)
except queue.Empty:
if self.conn.stop_event.is_set():
if self.stop_event.is_set():
break
continue
future = asyncio.run_coroutine_threadsafe(
sendAudioMessage(self.conn, audio_datas, text, text_index),
self.conn.loop,
sendAudioMessage(self.conn, sentence_type, audio_datas, text),
self.loop,
)
future.result()
except Exception as e:
@@ -221,3 +216,81 @@ class TTSProviderBase(ABC):
"""资源清理方法"""
if hasattr(self, "ws") and self.ws:
await self.ws.close()
def _get_segment_text(self):
# 合并当前全部文本并处理未分割部分
full_text = "".join(self.tts_text_buff)
current_text = full_text[self.processed_chars :] # 从未处理的位置开始
last_punct_pos = -1
# 根据是否是第一句话选择不同的标点符号集合
punctuations_to_use = (
self.first_sentence_punctuations
if self.is_first_sentence
else self.punctuations
)
for punct in punctuations_to_use:
pos = current_text.rfind(punct)
if (pos != -1 and last_punct_pos == -1) or (
pos != -1 and pos < last_punct_pos
):
last_punct_pos = pos
if last_punct_pos != -1:
segment_text_raw = current_text[: last_punct_pos + 1]
segment_text = textUtils.get_string_no_punctuation_or_emoji(
segment_text_raw
)
self.processed_chars += len(segment_text_raw) # 更新已处理字符位置
# 如果是第一句话,在找到第一个逗号后,将标志设置为False
if self.is_first_sentence:
self.is_first_sentence = False
return segment_text
elif self.tts_stop_request and current_text:
segment_text = current_text
self.is_first_sentence = True # 重置标志
return segment_text
else:
return None
def _process_audio_file(self, tts_file):
"""处理音频文件并转换为指定格式
Args:
tts_file: 音频文件路径
content_detail: 内容详情
Returns:
tuple: (sentence_type, audio_datas, content_detail)
"""
audio_datas = []
if tts_file.endswith(".p3"):
audio_datas, _ = p3.decode_opus_from_file(tts_file)
elif self.conn.audio_format == "pcm":
audio_datas, _ = self.audio_to_pcm_data(tts_file)
else:
audio_datas, _ = self.audio_to_opus_data(tts_file)
return audio_datas
def _process_remaining_text(self):
"""处理剩余的文本并生成语音
Returns:
bool: 是否成功处理了文本
"""
full_text = "".join(self.tts_text_buff)
remaining_text = full_text[self.processed_chars :]
if remaining_text:
segment_text = textUtils.get_string_no_punctuation_or_emoji(remaining_text)
if segment_text:
tts_file = self.to_tts(segment_text)
audio_datas = self._process_audio_file(tts_file)
self.tts_audio_queue.put(
(SentenceType.MIDDLE, audio_datas, segment_text)
)
self.processed_chars += len(full_text)
return True
return False