新增了Alexa渠道的所有核心功能模块,包括配置管理、请求校验、会话管理、安全验证、去重、响应格式化、渐进式响应、主动推送、提醒功能、配对绑定以及Webhook端点,并完成了插件注册和配置项定义。
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>" |