|
|
@@ -2,7 +2,7 @@
|
|
|
from __future__ import annotations
|
|
|
|
|
|
import re
|
|
|
-from typing import Optional
|
|
|
+from typing import Optional, Tuple
|
|
|
|
|
|
VALID_EMOTIONS = frozenset(
|
|
|
{
|
|
|
@@ -24,10 +24,7 @@ VALID_EMOTIONS = frozenset(
|
|
|
|
|
|
EMOTION_TAG_PATTERN = re.compile(r"\[EMOTION:\s*(\w+)\]", re.IGNORECASE)
|
|
|
_EMOTION_WORDS_ALT = "|".join(re.escape(w) for w in sorted(VALID_EMOTIONS, key=len, reverse=True))
|
|
|
-_LEADING_PLAIN_EMOTION = re.compile(
|
|
|
- rf"^\s*(?P<word>{_EMOTION_WORDS_ALT})(?:\s*[\r\n]+|\s+(?=\S)|\s*$)",
|
|
|
- re.IGNORECASE,
|
|
|
-)
|
|
|
+_EMOTION_WORD_HEAD = re.compile(rf"^\s*(?P<word>{_EMOTION_WORDS_ALT})", re.IGNORECASE)
|
|
|
|
|
|
|
|
|
def normalize_emotion(raw: str) -> str:
|
|
|
@@ -35,6 +32,33 @@ def normalize_emotion(raw: str) -> str:
|
|
|
return word if word in VALID_EMOTIONS else "neutral"
|
|
|
|
|
|
|
|
|
+def _split_plain_emotion_metadata(text: str) -> Tuple[Optional[str], Optional[str]]:
|
|
|
+ """
|
|
|
+ 若开头为裸 emotion 元数据,返回 (emotion, 正文);否则 (None, None)。
|
|
|
+ 元数据形态:单独一行 / 空格+大写句首 / 粘连大写句首(如 friendlyHi)。
|
|
|
+ 不将「happy to see you」等正常英文句首当作元数据。
|
|
|
+ """
|
|
|
+ m = _EMOTION_WORD_HEAD.match(text)
|
|
|
+ if not m:
|
|
|
+ return None, None
|
|
|
+ word = m.group("word")
|
|
|
+ if word.lower() not in VALID_EMOTIONS:
|
|
|
+ return None, None
|
|
|
+ rest = text[m.end() :]
|
|
|
+ if not rest:
|
|
|
+ return normalize_emotion(word), ""
|
|
|
+ if rest[0] in "\r\n":
|
|
|
+ return normalize_emotion(word), rest.lstrip()
|
|
|
+ if rest[0] == " ":
|
|
|
+ body = rest.lstrip()
|
|
|
+ if body and body[0].isascii() and body[0].isupper():
|
|
|
+ return normalize_emotion(word), body
|
|
|
+ return None, None
|
|
|
+ if rest[0].isascii() and rest[0].isupper():
|
|
|
+ return normalize_emotion(word), rest
|
|
|
+ return None, None
|
|
|
+
|
|
|
+
|
|
|
def parse_emotion_from_text(text: str) -> Optional[str]:
|
|
|
"""从 LLM 输出中解析情感(支持 [EMOTION:xxx] 及首行裸词)。"""
|
|
|
if not text:
|
|
|
@@ -42,16 +66,16 @@ def parse_emotion_from_text(text: str) -> Optional[str]:
|
|
|
tag_match = EMOTION_TAG_PATTERN.search(text)
|
|
|
if tag_match:
|
|
|
return normalize_emotion(tag_match.group(1))
|
|
|
- plain_match = _LEADING_PLAIN_EMOTION.match(text)
|
|
|
- if plain_match:
|
|
|
- return normalize_emotion(plain_match.group("word"))
|
|
|
- return None
|
|
|
+ emotion, _ = _split_plain_emotion_metadata(text)
|
|
|
+ return emotion
|
|
|
|
|
|
|
|
|
def strip_emotion_from_text(text: str) -> str:
|
|
|
"""移除情感标签/裸词,供字幕与 TTS 使用。"""
|
|
|
cleaned = EMOTION_TAG_PATTERN.sub("", text)
|
|
|
- cleaned = _LEADING_PLAIN_EMOTION.sub("", cleaned, count=1)
|
|
|
+ _, body = _split_plain_emotion_metadata(cleaned)
|
|
|
+ if body is not None:
|
|
|
+ cleaned = body
|
|
|
cleaned = cleaned.strip()
|
|
|
if re.fullmatch(rf"(?:{_EMOTION_WORDS_ALT})", cleaned, re.IGNORECASE):
|
|
|
return ""
|