mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-27 01:23:55 +08:00
Merge pull request #455 from CaixyPromise/feature/clean-markdown-for-tts
feature/clean markdown for tts 实现清理大模型AI的Markdown语法功能,
This commit is contained in:
@@ -5,6 +5,7 @@ import numpy as np
|
|||||||
import opuslib_next
|
import opuslib_next
|
||||||
from pydub import AudioSegment
|
from pydub import AudioSegment
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from core.utils.tts import MarkdownCleaner
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
@@ -23,6 +24,7 @@ class TTSProviderBase(ABC):
|
|||||||
tmp_file = self.generate_filename()
|
tmp_file = self.generate_filename()
|
||||||
try:
|
try:
|
||||||
max_repeat_time = 5
|
max_repeat_time = 5
|
||||||
|
text = MarkdownCleaner.clean_markdown(text)
|
||||||
while not os.path.exists(tmp_file) and max_repeat_time > 0:
|
while not os.path.exists(tmp_file) and max_repeat_time > 0:
|
||||||
asyncio.run(self.text_to_speak(text, tmp_file))
|
asyncio.run(self.text_to_speak(text, tmp_file))
|
||||||
if not os.path.exists(tmp_file):
|
if not os.path.exists(tmp_file):
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
import importlib
|
import importlib
|
||||||
@@ -15,3 +16,97 @@ def create_instance(class_name, *args, **kwargs):
|
|||||||
return sys.modules[lib_name].TTSProvider(*args, **kwargs)
|
return sys.modules[lib_name].TTSProvider(*args, **kwargs)
|
||||||
|
|
||||||
raise ValueError(f"不支持的TTS类型: {class_name},请检查该配置的type是否设置正确")
|
raise ValueError(f"不支持的TTS类型: {class_name},请检查该配置的type是否设置正确")
|
||||||
|
|
||||||
|
|
||||||
|
class MarkdownCleaner:
|
||||||
|
"""
|
||||||
|
封装 Markdown 清理逻辑:直接用 MarkdownCleaner.clean_markdown(text) 即可
|
||||||
|
"""
|
||||||
|
# 公式字符
|
||||||
|
NORMAL_FORMULA_CHARS = re.compile(r'[a-zA-Z\\^_{}\+\-\(\)\[\]=]')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _replace_inline_dollar(m: re.Match) -> str:
|
||||||
|
"""
|
||||||
|
只要捕获到完整的 "$...$":
|
||||||
|
- 如果内部有典型公式字符 => 去掉两侧 $
|
||||||
|
- 否则 (纯数字/货币等) => 保留 "$...$"
|
||||||
|
"""
|
||||||
|
content = m.group(1)
|
||||||
|
if MarkdownCleaner.NORMAL_FORMULA_CHARS.search(content):
|
||||||
|
return content
|
||||||
|
else:
|
||||||
|
return m.group(0)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _replace_table_block(match: re.Match) -> str:
|
||||||
|
"""
|
||||||
|
当匹配到一个整段表格块时,回调该函数。
|
||||||
|
"""
|
||||||
|
block_text = match.group('table_block')
|
||||||
|
lines = block_text.strip('\n').split('\n')
|
||||||
|
|
||||||
|
parsed_table = []
|
||||||
|
for line in lines:
|
||||||
|
line_stripped = line.strip()
|
||||||
|
if re.match(r'^\|\s*[-:]+\s*(\|\s*[-:]+\s*)+\|?$', line_stripped):
|
||||||
|
continue
|
||||||
|
columns = [col.strip() for col in line_stripped.split('|') if col.strip() != '']
|
||||||
|
if columns:
|
||||||
|
parsed_table.append(columns)
|
||||||
|
|
||||||
|
if not parsed_table:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
headers = parsed_table[0]
|
||||||
|
data_rows = parsed_table[1:] if len(parsed_table) > 1 else []
|
||||||
|
|
||||||
|
lines_for_tts = []
|
||||||
|
if len(parsed_table) == 1:
|
||||||
|
# 只有一行
|
||||||
|
only_line_str = ", ".join(parsed_table[0])
|
||||||
|
lines_for_tts.append(f"单行表格:{only_line_str}")
|
||||||
|
else:
|
||||||
|
lines_for_tts.append(f"表头是:{', '.join(headers)}")
|
||||||
|
for i, row in enumerate(data_rows, start=1):
|
||||||
|
row_str_list = []
|
||||||
|
for col_index, cell_val in enumerate(row):
|
||||||
|
if col_index < len(headers):
|
||||||
|
row_str_list.append(f"{headers[col_index]} = {cell_val}")
|
||||||
|
else:
|
||||||
|
row_str_list.append(cell_val)
|
||||||
|
lines_for_tts.append(f"第 {i} 行:{', '.join(row_str_list)}")
|
||||||
|
|
||||||
|
return "\n".join(lines_for_tts) + "\n"
|
||||||
|
|
||||||
|
# 预编译所有正则表达式(按执行频率排序)
|
||||||
|
# 这里要把 replace_xxx 的静态方法放在最前定义,以便在列表里能正确引用它们。
|
||||||
|
REGEXES = [
|
||||||
|
(re.compile(r'```.*?```', re.DOTALL), ''), # 代码块
|
||||||
|
(re.compile(r'^#+\s*', re.MULTILINE), ''), # 标题
|
||||||
|
(re.compile(r'(\*\*|__)(.*?)\1'), r'\2'), # 粗体
|
||||||
|
(re.compile(r'(\*|_)(?=\S)(.*?)(?<=\S)\1'), r'\2'), # 斜体
|
||||||
|
(re.compile(r'!\[.*?\]\(.*?\)'), ''), # 图片
|
||||||
|
(re.compile(r'\[(.*?)\]\(.*?\)'), r'\1'), # 链接
|
||||||
|
(re.compile(r'^\s*>+\s*', re.MULTILINE), ''), # 引用
|
||||||
|
(
|
||||||
|
re.compile(r'(?P<table_block>(?:^[^\n]*\|[^\n]*\n)+)', re.MULTILINE),
|
||||||
|
_replace_table_block
|
||||||
|
),
|
||||||
|
(re.compile(r'^\s*[*+-]\s*', re.MULTILINE), '- '), # 列表
|
||||||
|
(re.compile(r'\$\$.*?\$\$', re.DOTALL), ''), # 块级公式
|
||||||
|
(
|
||||||
|
re.compile(r'(?<![A-Za-z0-9])\$([^\n$]+)\$(?![A-Za-z0-9])'),
|
||||||
|
_replace_inline_dollar
|
||||||
|
),
|
||||||
|
(re.compile(r'\n{2,}'), '\n'), # 多余空行
|
||||||
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def clean_markdown(text: str) -> str:
|
||||||
|
"""
|
||||||
|
主入口方法:依序执行所有正则,移除或替换 Markdown 元素
|
||||||
|
"""
|
||||||
|
for regex, replacement in MarkdownCleaner.REGEXES:
|
||||||
|
text = regex.sub(replacement, text)
|
||||||
|
return text.strip()
|
||||||
Reference in New Issue
Block a user