Merge pull request 'fix(ditto): 情绪分析彻底修复——智能分句+8表情开放+prompt few-shot' (#2276) from fix/ditto-emotion-thorough-fix into develop
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 4s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m30s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m56s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 27s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 57s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m7s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m46s
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m38s
AI Code Review / AI Code Review (pull_request) Successful in 7m3s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 4m8s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Validate - Style (push) Successful in 10m22s
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 2m1s
CI/CD Pipeline / Integration Tests (push) Successful in 9m17s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 11m43s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 3m26s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m52s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 3m55s
CI/CD Pipeline / Unit Tests (push) Successful in 18m45s
CI/CD Pipeline / Validate - Security (push) Successful in 34m7s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 4s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m30s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m56s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 27s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 57s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m7s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m46s
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m38s
AI Code Review / AI Code Review (pull_request) Successful in 7m3s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 4m8s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Validate - Style (push) Successful in 10m22s
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 2m1s
CI/CD Pipeline / Integration Tests (push) Successful in 9m17s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 11m43s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 3m26s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m52s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 3m55s
CI/CD Pipeline / Unit Tests (push) Successful in 18m45s
CI/CD Pipeline / Validate - Security (push) Successful in 34m7s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
This commit was merged in pull request #2276.
This commit is contained in:
@@ -764,7 +764,8 @@ def _fallback_script(job: ViralVideoJob) -> dict:
|
||||
# 口播超长时截断(字数上限统一从 constants 取)
|
||||
_max_v = get_duration_spec(dur)["max_chars"]
|
||||
if len(voiceover) > _max_v:
|
||||
voiceover = voiceover[:_max_v].rstrip(",、。, ") + "。"
|
||||
# ruff B005:多字符 rstrip 有误导性,用正则等价实现(按字符集去除末尾标点/空格)
|
||||
voiceover = re.sub(r"[,、。, ]+$", "", voiceover[:_max_v]) + "。"
|
||||
base["shots"] = [
|
||||
{
|
||||
"time_range": f"0-{dur}秒",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""Ditto LLM 情绪分析服务 — #2076 后续:根据文案生成 emo_timeline.
|
||||
|
||||
职责:
|
||||
1. 正则按 。!?; 初步分句
|
||||
2. 调 DoubaoClient.chat_completion 分析每句表情(emo: 0-7, intensity: 0-1)
|
||||
1. 分句:先按 。!?; 句末标点切分;整段无句末标点且超长时按逗号等语义停顿二次切分
|
||||
2. 调 DoubaoClient.chat_completion 分析每句表情(emo: 0-7 八种全部开放, intensity: 0-1)
|
||||
3. 结果 LRU 缓存(文案 hash → 情绪列表)
|
||||
4. LLM 失败/超时/格式错 → 返回空列表(降级中性表情,不阻塞生成)
|
||||
5. TTS 完成后按字数比例或 sentence_timings 对齐成秒级 timeline
|
||||
@@ -29,10 +29,17 @@ EMO_NEUTRAL = 4
|
||||
EMO_SAD = 5
|
||||
EMO_SURPRISE = 6
|
||||
EMO_CONTEMPT = 7
|
||||
ALLOWED_EMOS = {EMO_HAPPY, EMO_NEUTRAL, EMO_SAD, EMO_SURPRISE} # 营销场景白名单
|
||||
ALLOWED_EMOS = set(range(8)) # 8 种表情全部开放(0-7);超出范围的非法值仍会被降级为中性
|
||||
|
||||
# ── 分句正则 ─────────────────────────────────────────────────────
|
||||
_SENT_SPLIT_RE = re.compile(r"(?<=[。!?;!?;])\s*")
|
||||
# 二次切分:逗号/分号/顿号/冒号等语义停顿(lookbehind 保留标点在前句末尾)
|
||||
_CLAUSE_SPLIT_RE = re.compile(r"(?<=[,,、::])")
|
||||
# 整段无句末标点且超过该长度时才做逗号二次切分
|
||||
_SINGLE_SENTENCE_THRESHOLD = 30
|
||||
# 二次切分后过短片段(<5 字,如"家人们,")并入相邻句避免切太碎;
|
||||
# 4 字以上语义完整的短句(如"心中有爱,")保留独立成段
|
||||
_MIN_CLAUSE_LEN = 5
|
||||
|
||||
# ── 默认 prompt 模板文件路径 ──────────────────────────────────────
|
||||
_DEFAULT_PROMPT_PATH = Path(__file__).parent / "prompts" / "ditto_emotion.txt"
|
||||
@@ -45,8 +52,8 @@ def _load_default_prompt() -> str:
|
||||
# 文件不存在时用极简兜底
|
||||
return (
|
||||
"分析文案每句话表情,输出JSON数组:"
|
||||
'[{"text":"句子","emo":4,"intensity":0.2}],emo:3开心4中性5伤心6惊讶,'
|
||||
"禁止0/1/2/7。\n【文案】\n{文案}"
|
||||
'[{"text":"句子","emo":4,"intensity":0.2}],emo:0-7八种表情全部开放,'
|
||||
"至少30%句子用非中性表情。\n【文案】\n{文案}"
|
||||
)
|
||||
|
||||
|
||||
@@ -86,12 +93,46 @@ class EmotionTimelineEntry:
|
||||
|
||||
|
||||
# ── 分句 ─────────────────────────────────────────────────────────
|
||||
def _split_by_clauses(sentence: str) -> list[str]:
|
||||
"""按逗号/分号等语义停顿二次切分;过短片段(<5 字)并入相邻句,避免切太碎.
|
||||
|
||||
每句尽量控制在 10-50 字左右(50 字为软上限,无更细停顿点时保持语义完整不硬切)。
|
||||
"""
|
||||
raw = [c for c in _CLAUSE_SPLIT_RE.split(sentence) if c and c.strip()]
|
||||
if len(raw) <= 1:
|
||||
return [sentence]
|
||||
merged: list[str] = []
|
||||
buf = ""
|
||||
for clause in raw:
|
||||
if not buf:
|
||||
buf = clause
|
||||
continue
|
||||
if len(buf) < _MIN_CLAUSE_LEN:
|
||||
buf += clause
|
||||
continue
|
||||
merged.append(buf)
|
||||
buf = clause
|
||||
if buf:
|
||||
if merged and len(buf) < _MIN_CLAUSE_LEN:
|
||||
merged[-1] += buf
|
||||
else:
|
||||
merged.append(buf)
|
||||
return [m.strip() for m in merged if m.strip()]
|
||||
|
||||
|
||||
def split_sentences(text: str) -> list[str]:
|
||||
"""按中文句末标点切分,过滤空串."""
|
||||
"""按中文句末标点切分,过滤空串.
|
||||
|
||||
Bug1 修复:若按句末标点切分后只有 1 段且文本超过 30 字(文案全是逗号、
|
||||
结尾无标点),再按逗号/分号等语义停顿做二次切分,避免整段只返回 1 个
|
||||
segment 导致 LLM 只能对整段打一个表情。
|
||||
"""
|
||||
if not text:
|
||||
return []
|
||||
parts = _SENT_SPLIT_RE.split(text.strip())
|
||||
return [p.strip() for p in parts if p and p.strip()]
|
||||
parts = [p.strip() for p in _SENT_SPLIT_RE.split(text.strip()) if p and p.strip()]
|
||||
if len(parts) <= 1 and parts and len(parts[0]) > _SINGLE_SENTENCE_THRESHOLD:
|
||||
parts = _split_by_clauses(parts[0])
|
||||
return parts
|
||||
|
||||
|
||||
# ── 解析 LLM 返回的 JSON ─────────────────────────────────────────
|
||||
@@ -202,6 +243,25 @@ def align_timeline_by_timings(
|
||||
return entries
|
||||
|
||||
|
||||
# ── LLM 结果合理性校验(提示词遵循度排查)─────────────────────────
|
||||
# 感叹号或明显情绪关键词出现时,LLM 不应返回全中性/低强度
|
||||
_EMO_SIGNAL_RE = re.compile(r"[!!]|是不是|有没有|居然|竟然|只要|太坑|千万别|免费|赶紧|推荐|惊艳")
|
||||
|
||||
|
||||
def _warn_if_suspicious_all_neutral(text: str, segments: list[EmotionSegment]) -> None:
|
||||
"""文案含明显情绪信号但 LLM 返回全部 emo=4 且 intensity<=0.1 时打 warning 日志."""
|
||||
if not segments:
|
||||
return
|
||||
all_flat = all(s.emo == EMO_NEUTRAL and s.intensity <= 0.1 for s in segments)
|
||||
if all_flat and _EMO_SIGNAL_RE.search(text):
|
||||
logger.warning(
|
||||
"[ditto_emotion] 文案含情绪信号(感叹号/关键词)但 LLM 返回全中性/低强度,"
|
||||
"疑似提示词遵循度差: text=%.80s, segments=%d",
|
||||
text,
|
||||
len(segments),
|
||||
)
|
||||
|
||||
|
||||
# ── LLM 情绪分析服务 ─────────────────────────────────────────────
|
||||
class DittoEmotionService:
|
||||
"""Ditto 情绪分析服务(带 LRU 缓存)."""
|
||||
@@ -275,6 +335,8 @@ class DittoEmotionService:
|
||||
segments = _parse_emotion_json(result)
|
||||
if not segments:
|
||||
logger.warning("[ditto_emotion] LLM 返回解析失败: %s", result[:200])
|
||||
else:
|
||||
_warn_if_suspicious_all_neutral(text, segments)
|
||||
return segments
|
||||
|
||||
def analyze(self, text: str) -> list[EmotionSegment]:
|
||||
|
||||
@@ -1,27 +1,42 @@
|
||||
你是一个数字人视频表情导演。给定一段口播文案,分析每句话应该用什么表情和强度,让数字人说话时表情自然有变化,不僵硬。
|
||||
【表情编号】
|
||||
0=愤怒(营销场景禁用)
|
||||
1=厌恶(禁用)
|
||||
2=害怕(禁用)
|
||||
3=开心:介绍优点、优惠、好消息、号召行动时用
|
||||
4=中性:默认表情,陈述事实、平铺直叙时用
|
||||
5=伤心:仅在共情用户痛点时低强度使用(如"是不是经常遇到…")
|
||||
6=惊讶:惊喜、意外、强调价值时用(如"居然""只要""竟然")
|
||||
7=轻蔑(禁用)
|
||||
【强度说明】
|
||||
0.1-0.2:几乎看不出变化,比中性多一点情绪色彩
|
||||
0.3-0.4:有明显但自然的情绪,正常说话的波动
|
||||
0.5-0.6:较强情绪,感叹句/重点强调
|
||||
0.7+:极强情绪,极少使用
|
||||
【规则】
|
||||
1. 按自然语义分句,以。!?;为主要分界,逗号不分
|
||||
2. 60-70%的句子应该用中性(4),不要每句都标情绪
|
||||
3. 情绪和内容匹配:卖点→开心(3),痛点共情→伤心(5)低强度,惊喜/划算→惊讶(6),陈述→中性(4)
|
||||
4. 相邻句子情绪不要剧烈跳变
|
||||
5. 感叹号结尾强度0.4-0.6,句号结尾一般0.1-0.3
|
||||
6. 开头结尾句用中性(4)或低强度开心(3)
|
||||
7. 禁止使用0/1/2/7
|
||||
【输出格式】严格JSON数组,不要输出其他内容
|
||||
[{"text":"句子原文","emo":3,"intensity":0.4}]
|
||||
你是一个数字人短视频表情导演。为每句话分配一个最贴切的表情,让数字人表情丰富、有情绪感染力,像真人说话一样有起伏变化。
|
||||
|
||||
【8种表情】
|
||||
0=愤怒:怒斥、骂坑、强烈不满、控诉
|
||||
1=厌恶:嫌弃、吐槽劣质、鄙视套路
|
||||
2=害怕:紧张、警告、危险提示、悬念
|
||||
3=开心:夸赞、推荐、好消息、福利、号召
|
||||
4=中性:陈述事实、客观描述、数据、过渡衔接
|
||||
5=伤心:共情痛点、遗憾、难过、踩坑经历
|
||||
6=惊讶:惊喜、超预期、居然、竟然、只要、数字冲击
|
||||
7=轻蔑:嘲讽、冷笑、拆穿套路、不屑
|
||||
|
||||
【分句说明】
|
||||
- 输入文案已按句末标点预分句;若某句内部仍有明显语义停顿(逗号、分号),你应根据语义进一步拆分成更细的句子分别打分
|
||||
- 若输入没有句末标点,要自己根据语义在逗号处分句
|
||||
|
||||
【强度】0.1几乎无变化 0.3轻微自然 0.5明显情绪 0.7强烈 0.9爆发
|
||||
感叹号结尾0.4-0.7,句号0.2-0.4,问号0.3-0.6。
|
||||
|
||||
【示例1】
|
||||
文案:"家人们!你们是不是经常买东西踩坑?花了大价钱买回来的东西根本没法用!但是今天这款产品真的惊艳到我了!"
|
||||
输出:
|
||||
[{"text":"家人们!","emo":6,"intensity":0.5},{"text":"你们是不是经常买东西踩坑?","emo":5,"intensity":0.3},{"text":"花了大价钱买回来的东西根本没法用!","emo":0,"intensity":0.5},{"text":"但是今天这款产品真的惊艳到我了!","emo":3,"intensity":0.6}]
|
||||
|
||||
【示例2】
|
||||
文案:"做公益不一定是一个人去做惊天动地的大事,而是一群人扎扎实实的干好身边的小事,心中有爱,眼里有光。"
|
||||
输出:
|
||||
[{"text":"做公益不一定是一个人去做惊天动地的大事,","emo":4,"intensity":0.2},{"text":"而是一群人扎扎实实的干好身边的小事,","emo":4,"intensity":0.2},{"text":"心中有爱,","emo":3,"intensity":0.4},{"text":"眼里有光。","emo":3,"intensity":0.5}]
|
||||
|
||||
【关键要求】
|
||||
- 每句话都必须独立判断情绪,至少有30%的句子使用非中性(4)表情!感叹号结尾的句子intensity不得低于0.4!
|
||||
- 严禁全部句子都返回中性(4)!严禁全部强度都是0.1!
|
||||
- 情绪要有变化:痛点段伤心或愤怒→转折→卖点开心→福利惊讶→号召开心
|
||||
- 语义优先:根据每句话的实际语义选择表情,不要固定开头结尾情绪
|
||||
- 8种表情全部开放,该愤怒就愤怒(0),该厌恶就厌恶(1),该害怕就害怕(2),该轻蔑就轻蔑(7)
|
||||
- 开头第一句就根据内容定情绪,不要强行用中性开场
|
||||
|
||||
【输出】严格JSON数组,只输出JSON,不要其他文字。
|
||||
[{"text":"原文","emo":3,"intensity":0.5}]
|
||||
|
||||
【文案】
|
||||
{文案}
|
||||
|
||||
@@ -219,7 +219,7 @@ class SharedSettings(BaseSettings):
|
||||
validation_alias=AliasChoices("DITTO_EMOTION_MODEL", "ditto_emotion_model"),
|
||||
)
|
||||
ditto_emotion_temperature: float = Field(
|
||||
default=0.1,
|
||||
default=0.8,
|
||||
validation_alias=AliasChoices("DITTO_EMOTION_TEMPERATURE", "ditto_emotion_temperature"),
|
||||
)
|
||||
ditto_emotion_timeout: int = Field(
|
||||
|
||||
@@ -13,6 +13,7 @@ from packages.application.ditto_emotion_service import (
|
||||
DittoEmotionService,
|
||||
EmotionSegment,
|
||||
_parse_emotion_json,
|
||||
_warn_if_suspicious_all_neutral,
|
||||
align_timeline_by_length,
|
||||
align_timeline_by_timings,
|
||||
split_sentences,
|
||||
@@ -36,6 +37,34 @@ class TestSplitSentences:
|
||||
sents = split_sentences("Hello! How are you? I'm fine.")
|
||||
assert len(sents) == 3
|
||||
|
||||
# ── Bug1 修复:无句末标点长文案按逗号二次切分 ──
|
||||
def test_no_ending_punct_long_text_split_by_comma(self):
|
||||
"""公益文案:全是逗号、结尾无标点,整段超过30字 → 逗号二次切分."""
|
||||
text = "做公益不一定是一个人去做惊天动地的大事,而是一群人扎扎实实的干好身边的小事,心中有爱,眼里有光"
|
||||
sents = split_sentences(text)
|
||||
assert len(sents) >= 3
|
||||
assert any("心中有爱" in s for s in sents)
|
||||
assert any("眼里有光" in s for s in sents)
|
||||
|
||||
def test_short_text_no_split(self):
|
||||
"""不超过30字的无句末标点文案不二次切分."""
|
||||
text = "大家好,欢迎收看"
|
||||
assert split_sentences(text) == [text]
|
||||
|
||||
def test_short_clauses_merged(self):
|
||||
"""过短片段(<10字)并入相邻句,避免切太碎."""
|
||||
text = "家人们,你们是不是经常买东西踩坑呢,花了大价钱买回来的东西根本没法用啊"
|
||||
sents = split_sentences(text)
|
||||
assert len(sents) == 2
|
||||
assert all(len(s) >= 10 for s in sents)
|
||||
|
||||
def test_normal_punct_no_clause_split(self):
|
||||
"""有句末标点正常切分的文案不做逗号二次切分."""
|
||||
text = "大家好!今天给大家推荐一款超棒的产品,它真的很好用。不信你试试?"
|
||||
sents = split_sentences(text)
|
||||
assert len(sents) == 3
|
||||
assert "今天给大家推荐一款超棒的产品,它真的很好用。" in sents
|
||||
|
||||
|
||||
# ── JSON 解析 ────────────────────────────────────────────────────
|
||||
class TestParseEmotionJson:
|
||||
@@ -52,8 +81,17 @@ class TestParseEmotionJson:
|
||||
assert len(segs) == 1
|
||||
assert segs[0].emo == 3
|
||||
|
||||
def test_forbidden_emo_becomes_neutral(self):
|
||||
raw = json.dumps([{"text": "怒", "emo": 0, "intensity": 0.8}])
|
||||
def test_all_eight_emos_allowed(self):
|
||||
"""8 种表情全部开放:0-7 原样保留,不再强制转中性."""
|
||||
for emo in range(8):
|
||||
raw = json.dumps([{"text": "句", "emo": emo, "intensity": 0.5}])
|
||||
segs = _parse_emotion_json(raw)
|
||||
assert len(segs) == 1
|
||||
assert segs[0].emo == emo
|
||||
|
||||
def test_out_of_range_emo_becomes_neutral(self):
|
||||
"""超出 0-7 范围的非法 emo 值仍降级为中性."""
|
||||
raw = json.dumps([{"text": "句", "emo": 9, "intensity": 0.5}])
|
||||
segs = _parse_emotion_json(raw)
|
||||
assert len(segs) == 1
|
||||
assert segs[0].emo == EMO_NEUTRAL
|
||||
@@ -280,3 +318,35 @@ class TestSingleSentenceFallback:
|
||||
assert segs[0].intensity == 0.1
|
||||
finally:
|
||||
m._cached_analyze.cache_clear()
|
||||
|
||||
|
||||
# ── LLM 结果合理性校验(Bug2)────────────────────────────────────
|
||||
class TestWarnIfSuspiciousAllNeutral:
|
||||
def test_warns_when_all_neutral_with_signals(self, caplog):
|
||||
"""文案含感叹号但全中性/低强度 → warning."""
|
||||
segs = [EmotionSegment(text="家人们!", emo=4, intensity=0.1)]
|
||||
with caplog.at_level("WARNING", logger="packages.application.ditto_emotion_service"):
|
||||
_warn_if_suspicious_all_neutral("家人们!你们是不是经常踩坑?", segs)
|
||||
assert any("全中性" in r.message or "全中性" in r.getMessage() for r in caplog.records)
|
||||
|
||||
def test_no_warn_when_has_emotion(self, caplog):
|
||||
"""有非中性表情 → 不 warning."""
|
||||
segs = [
|
||||
EmotionSegment(text="家人们!", emo=6, intensity=0.5),
|
||||
EmotionSegment(text="你们是不是经常踩坑?", emo=5, intensity=0.3),
|
||||
]
|
||||
with caplog.at_level("WARNING", logger="packages.application.ditto_emotion_service"):
|
||||
_warn_if_suspicious_all_neutral("家人们!你们是不是经常踩坑?", segs)
|
||||
assert not caplog.records
|
||||
|
||||
def test_no_warn_when_no_signal(self, caplog):
|
||||
"""文案无情绪信号,即使全中性也不 warning."""
|
||||
segs = [EmotionSegment(text="今天天气不错。", emo=4, intensity=0.1)]
|
||||
with caplog.at_level("WARNING", logger="packages.application.ditto_emotion_service"):
|
||||
_warn_if_suspicious_all_neutral("今天天气不错。", segs)
|
||||
assert not caplog.records
|
||||
|
||||
def test_no_warn_when_empty(self, caplog):
|
||||
with caplog.at_level("WARNING", logger="packages.application.ditto_emotion_service"):
|
||||
_warn_if_suspicious_all_neutral("家人们!", [])
|
||||
assert not caplog.records
|
||||
|
||||
Reference in New Issue
Block a user