79 lines
2.8 KiB
Python
79 lines
2.8 KiB
Python
|
|
import html
|
|||
|
|
import re
|
|||
|
|
|
|||
|
|
MAX_SSML_CHARS = 8000
|
|||
|
|
VOICE_FRIENDLY_MAX = 1000
|
|||
|
|
|
|||
|
|
|
|||
|
|
class MarkdownToSSML:
|
|||
|
|
|
|||
|
|
def __init__(self, max_chars: int = VOICE_FRIENDLY_MAX):
|
|||
|
|
self._max_chars = max_chars
|
|||
|
|
|
|||
|
|
def convert(self, markdown_text: str) -> str:
|
|||
|
|
if not markdown_text:
|
|||
|
|
return "<speak></speak>"
|
|||
|
|
|
|||
|
|
text = self._strip_markdown(markdown_text)
|
|||
|
|
text = html.escape(text, quote=False)
|
|||
|
|
text = self._restore_placeholders(text)
|
|||
|
|
text = self._truncate_if_needed(text)
|
|||
|
|
return f"<speak>{text}</speak>"
|
|||
|
|
|
|||
|
|
def _strip_markdown(self, md: str) -> str:
|
|||
|
|
self._placeholders = []
|
|||
|
|
p = self._ph
|
|||
|
|
|
|||
|
|
md = re.sub(r'```[\s\S]*?```', p('此处有代码,请在手机 App 中查看。'), md)
|
|||
|
|
md = re.sub(r'`([^`]+)`', r'\1', md)
|
|||
|
|
md = re.sub(r'!\[.*?\]\(.*?\)', p('此处有图片。'), md)
|
|||
|
|
md = re.sub(r'\*\*(.+?)\*\*', lambda m: p(f'<emphasis level="strong">{m.group(1)}</emphasis>'), md)
|
|||
|
|
md = re.sub(r'\*([^*]+)\*', lambda m: p(f'<emphasis level="moderate">{m.group(1)}</emphasis>'), md)
|
|||
|
|
md = re.sub(r'__([^_]+)__', r'\1', md)
|
|||
|
|
md = re.sub(r'_([^_]+)_', r'\1', md)
|
|||
|
|
md = re.sub(r'~~(.+?)~~', r'\1', md)
|
|||
|
|
md = re.sub(r'\[(.+?)\]\(.+?\)', r'\1', md)
|
|||
|
|
md = re.sub(r'^#{1,6}\s+', '', md, flags=re.MULTILINE)
|
|||
|
|
md = re.sub(r'^[\-\*\+]\s+', '', md, flags=re.MULTILINE)
|
|||
|
|
md = re.sub(r'^\d+\.\s+', '', md, flags=re.MULTILINE)
|
|||
|
|
md = re.sub(r'^>\s+', '', md, flags=re.MULTILINE)
|
|||
|
|
md = md.replace('---', p('<break time="500ms"/>'))
|
|||
|
|
md = md.replace('***', p('<break time="500ms"/>'))
|
|||
|
|
md = re.sub(r'\|.*\|', p('表格内容请在手机 App 中查看。'), md)
|
|||
|
|
md = re.sub(r'https?://\S+', '', md)
|
|||
|
|
return md.strip()
|
|||
|
|
|
|||
|
|
def _ph(self, s: str) -> str:
|
|||
|
|
placeholder = f"\x00SSML{len(self._placeholders)}\x00"
|
|||
|
|
self._placeholders.append(s)
|
|||
|
|
return placeholder
|
|||
|
|
|
|||
|
|
def _restore_placeholders(self, text: str) -> str:
|
|||
|
|
for i, val in enumerate(self._placeholders):
|
|||
|
|
text = text.replace(f"\x00SSML{i}\x00", val)
|
|||
|
|
return text
|
|||
|
|
|
|||
|
|
def _truncate_if_needed(self, text: str) -> str:
|
|||
|
|
if len(text) <= self._max_chars:
|
|||
|
|
return text
|
|||
|
|
|
|||
|
|
truncated = text[:self._max_chars]
|
|||
|
|
delimiters = ['。', '.', '\n', ';', ';', ',']
|
|||
|
|
best_pos = -1
|
|||
|
|
for delim in delimiters:
|
|||
|
|
pos = truncated.rfind(delim)
|
|||
|
|
if pos > best_pos:
|
|||
|
|
best_pos = pos
|
|||
|
|
|
|||
|
|
if best_pos > 0:
|
|||
|
|
truncated = truncated[:best_pos + 1]
|
|||
|
|
|
|||
|
|
truncated += '<break time="500ms"/>以上为部分回答,完整内容请在手机 App 中查看。'
|
|||
|
|
return truncated
|
|||
|
|
|
|||
|
|
|
|||
|
|
def ssml_safe(text: str) -> str:
|
|||
|
|
if not text:
|
|||
|
|
return "<speak></speak>"
|
|||
|
|
text = html.escape(text, quote=False)
|
|||
|
|
return f"<speak>{text}</speak>"
|