emotion_text_utils.py 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. """LLM 情感标签解析与展示文本清理(字幕 / TTS 共用)"""
  2. from __future__ import annotations
  3. import re
  4. from typing import Optional, Tuple
  5. VALID_EMOTIONS = frozenset(
  6. {
  7. "neutral",
  8. "happy",
  9. "sad",
  10. "excited",
  11. "angry",
  12. "surprised",
  13. "thoughtful",
  14. "encouraging",
  15. "confused",
  16. "worried",
  17. "satisfied",
  18. "friendly",
  19. "sorry",
  20. }
  21. )
  22. EMOTION_TAG_PATTERN = re.compile(r"\[EMOTION:\s*(\w+)\]", re.IGNORECASE)
  23. _EMOTION_WORDS_ALT = "|".join(re.escape(w) for w in sorted(VALID_EMOTIONS, key=len, reverse=True))
  24. _EMOTION_WORD_HEAD = re.compile(rf"^\s*(?P<word>{_EMOTION_WORDS_ALT})", re.IGNORECASE)
  25. def normalize_emotion(raw: str) -> str:
  26. word = (raw or "").strip().lower()
  27. return word if word in VALID_EMOTIONS else "neutral"
  28. def _split_plain_emotion_metadata(text: str) -> Tuple[Optional[str], Optional[str]]:
  29. """
  30. 若开头为裸 emotion 元数据,返回 (emotion, 正文);否则 (None, None)。
  31. 元数据形态:单独一行 / 空格+大写句首 / 粘连大写句首(如 friendlyHi)。
  32. 不将「happy to see you」等正常英文句首当作元数据。
  33. """
  34. m = _EMOTION_WORD_HEAD.match(text)
  35. if not m:
  36. return None, None
  37. word = m.group("word")
  38. if word.lower() not in VALID_EMOTIONS:
  39. return None, None
  40. rest = text[m.end() :]
  41. if not rest:
  42. return normalize_emotion(word), ""
  43. if rest[0] in "\r\n":
  44. return normalize_emotion(word), rest.lstrip()
  45. if rest[0] == " ":
  46. body = rest.lstrip()
  47. if body and body[0].isascii() and body[0].isupper():
  48. return normalize_emotion(word), body
  49. return None, None
  50. if rest[0].isascii() and rest[0].isupper():
  51. return normalize_emotion(word), rest
  52. return None, None
  53. def parse_emotion_from_text(text: str) -> Optional[str]:
  54. """从 LLM 输出中解析情感(支持 [EMOTION:xxx] 及首行裸词)。"""
  55. if not text:
  56. return None
  57. tag_match = EMOTION_TAG_PATTERN.search(text)
  58. if tag_match:
  59. return normalize_emotion(tag_match.group(1))
  60. emotion, _ = _split_plain_emotion_metadata(text)
  61. return emotion
  62. def strip_emotion_from_text(text: str) -> str:
  63. """移除情感标签/裸词,供字幕与 TTS 使用。"""
  64. cleaned = EMOTION_TAG_PATTERN.sub("", text)
  65. _, body = _split_plain_emotion_metadata(cleaned)
  66. if body is not None:
  67. cleaned = body
  68. cleaned = cleaned.strip()
  69. if re.fullmatch(rf"(?:{_EMOTION_WORDS_ALT})", cleaned, re.IGNORECASE):
  70. return ""
  71. return cleaned
  72. def clean_text_for_display(text: str) -> str:
  73. """字幕展示文本清理(emotion 已通过独立 WebSocket 消息下发)。"""
  74. return strip_emotion_from_text(text)