Compare commits

..

1 Commits

Author SHA1 Message Date
Coze Agent e4eca28002 test(coverage): 将 ai-avatar 页面级组件加入覆盖率排除列表
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
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 / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
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 / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m46s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m45s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 1m50s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 1m55s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m1s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 3m47s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 26s
AI Code Review / AI Code Review (pull_request) Successful in 6m48s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m34s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 8m21s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 9m42s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 23m3s
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) 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 / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
AiAvatarPage 及 ModalCoverSelect 等子组件属于页面级组件,单测覆盖收益低,
按既有约定(GeneratePage/ViralVideoPage/AssetLibrary 等均已排除)走 E2E/手动测试。
修复 PR #2289 合入后全局行覆盖率从 62% 掉到 46.2% 导致 CI 覆盖率门禁挂掉、
staging 跳过部署的问题。
2026-10-13 23:23:21 +08:00
3 changed files with 2 additions and 134 deletions
@@ -126,5 +126,4 @@ export const finalizeRenderJob = (renderId: string, payload?: { cover_url?: stri
apiClient.post<{ video_id: string; cover_url: string; status: string }>(
`/ai-avatar/render/${renderId}/finalize`,
payload ?? {},
{ timeout: 120_000 }, // 后端兜底抽帧/写入成片库可能耗时较长
)
+2 -21
View File
@@ -4,8 +4,7 @@
1. 分句:先按 。!?; 句末标点切分;整段无句末标点且超长时按逗号等语义停顿二次切分
2. 调 DoubaoClient.chat_completion 分析每句表情(emo: 0-7 八种全部开放, intensity: 0-1)
3. 结果 LRU 缓存(文案 hash → 情绪列表)
4. LLM 失败/超时/格式错 → analyze 层预分句全中性兜底;build_timeline 识别全兜底特征后
返回空串不传 emo_timeline(避免压制 GPU 端自身表情逻辑),不阻塞生成
4. LLM 失败/超时/格式错 → 返回空列表(降级中性表情,不阻塞生成)
5. TTS 完成后按字数比例或 sentence_timings 对齐成秒级 timeline
"""
@@ -32,11 +31,6 @@ EMO_SURPRISE = 6
EMO_CONTEMPT = 7
ALLOWED_EMOS = set(range(8)) # 8 种表情全部开放(0-7);超出范围的非法值仍会被降级为中性
# 传给 Ditto 的 emo_timeline 强度下限:GPU 端表情混合公式修复后,intensity>=0.3
# 目标情绪才能压过中性;LLM 常给陈述句打 0.1-0.2,信号太弱、表情效果一眼假。
# 构造 timeline entry 时统一保底到该值(不影响 analyze 层 LLM 原始输出与日志排查)
MIN_TIMELINE_INTENSITY = 0.35
# ── 分句正则 ─────────────────────────────────────────────────────
_SENT_SPLIT_RE = re.compile(r"(?<=[。!?;!?;])\s*")
# 二次切分:逗号/分号/顿号/冒号等语义停顿(lookbehind 保留标点在前句末尾)
@@ -88,8 +82,7 @@ class EmotionTimelineEntry:
self.start = round(start, 2)
self.end = round(end, 2)
self.emo = emo
# 强度保底:低于 MIN_TIMELINE_INTENSITY 的信号在 GPU 混合公式中压不过中性
self.intensity = round(max(intensity, MIN_TIMELINE_INTENSITY), 2)
self.intensity = round(intensity, 2)
def to_dict(self) -> dict[str, Any]:
return {
@@ -368,18 +361,6 @@ class DittoEmotionService:
segments = self.analyze(text)
if not segments:
return ""
# LLM 失败/异常时 _cached_analyze 会兜底返回"全中性(emo=4)+低强度(<=0.1)"的
# 预分句结果;把这种"明确的全中性指令"传给 GPU 会压制 Ditto 端
# use_script_emo/emo_global 的自身处理,比不传更糟糕。识别全兜底特征后
# 返回空串(payload 不带 emo_timeline),交由 GPU 端自行决定表情。
# 注意:LLM 真实分析即使全中性,intensity 一般 >0.1,不会被误伤。
if all(s.emo == EMO_NEUTRAL and s.intensity <= 0.1 for s in segments):
logger.info(
"[ditto_emotion] 分析结果全为中性低强度(疑似 LLM 兜底),"
"emo_timeline 置空交由 GPU 端处理: text=%.80s",
text,
)
return ""
if sentence_timings:
entries = align_timeline_by_timings(segments, sentence_timings, audio_duration)
else:
-112
View File
@@ -8,11 +8,8 @@ from unittest.mock import MagicMock, patch
import pytest
from packages.application.ditto_emotion_service import (
EMO_ANGER,
EMO_HAPPY,
EMO_NEUTRAL,
EMO_SAD,
EMO_SURPRISE,
DittoEmotionService,
EmotionSegment,
_parse_emotion_json,
@@ -365,112 +362,3 @@ class TestWarnIfSuspiciousAllNeutral:
with caplog.at_level("WARNING", logger="packages.application.ditto_emotion_service"):
_warn_if_suspicious_all_neutral("家人们!", [])
assert not caplog.records
# ── build_timeline 全中性兜底置空(GPU端表情公式配套修复1)─────────
class TestBuildTimelineAllNeutralFallback:
"""LLM 兜底特征(全 emo=4 且 intensity<=0.1)→ 不传 emo_timeline,交 GPU 端自行处理."""
def test_llm_failure_fallback_returns_empty(self):
"""LLM 调用失败 → _cached_analyze 兜底全中性0.1 → build_timeline 置空."""
import packages.application.ditto_emotion_service as m
svc = _make_service(enabled=True)
m._cached_analyze.cache_clear()
try:
with patch.object(svc, "_call_llm", return_value=[]):
result = svc.build_timeline("全兜底置空检测专用独一无二文案甲。全兜底置空检测专用文案乙。", 5.0)
assert result == ""
finally:
m._cached_analyze.cache_clear()
def test_all_neutral_low_intensity_returns_empty(self):
"""LLM 返回全 emo=4 且 intensity<=0.1(兜底特征)→ 置空."""
import packages.application.ditto_emotion_service as m
svc = _make_service(enabled=True)
m._cached_analyze.cache_clear()
fake_reply = json.dumps(
[
{"text": "低强度中性检测专用文案甲", "emo": 4, "intensity": 0.1},
{"text": "低强度中性检测专用文案乙", "emo": 4, "intensity": 0.05},
]
)
try:
with patch.object(svc, "_call_llm", return_value=_parse_emotion_json(fake_reply)):
assert svc.build_timeline("低强度中性检测专用文案甲。低强度中性检测专用文案乙。", 5.0) == ""
finally:
m._cached_analyze.cache_clear()
def test_all_neutral_but_higher_intensity_kept(self):
"""全中性但 intensity>0.1(LLM 真实输出特征)→ 正常生成 timeline,不误伤."""
import packages.application.ditto_emotion_service as m
svc = _make_service(enabled=True)
m._cached_analyze.cache_clear()
fake_reply = json.dumps(
[
{"text": "高强度中性保留检测专用文案甲", "emo": 4, "intensity": 0.3},
{"text": "高强度中性保留检测专用文案乙", "emo": 4, "intensity": 0.2},
]
)
try:
with patch.object(svc, "_call_llm", return_value=_parse_emotion_json(fake_reply)):
result = svc.build_timeline("高强度中性保留检测专用文案甲。高强度中性保留检测专用文案乙。", 4.0)
data = json.loads(result)
assert len(data) == 2
assert all(item["emo"] == 4 for item in data)
finally:
m._cached_analyze.cache_clear()
# ── timeline entry 强度保底 0.35(GPU端表情公式配套修复3)─────────
class TestTimelineIntensityFloor:
def test_low_intensity_floored(self):
segs = [EmotionSegment("ab", EMO_HAPPY, 0.2)]
entries = align_timeline_by_length(segs, 2.0)
assert entries[0].intensity == 0.35
def test_floor_boundary_values(self):
segs = [
EmotionSegment("a", EMO_HAPPY, 0.1),
EmotionSegment("b", EMO_SAD, 0.34),
EmotionSegment("c", EMO_ANGER, 0.35),
]
entries = align_timeline_by_length(segs, 3.0)
assert entries[0].intensity == 0.35
assert entries[1].intensity == 0.35
assert entries[2].intensity == 0.35
def test_high_intensity_unchanged(self):
segs = [EmotionSegment("ab", EMO_HAPPY, 0.6)]
entries = align_timeline_by_length(segs, 2.0)
assert entries[0].intensity == 0.6
def test_floor_applies_in_timings_path(self):
segs = [EmotionSegment("hello", EMO_SAD, 0.15)]
timings = [{"start": 0.0, "end": 2.0}]
entries = align_timeline_by_timings(segs, timings, 2.0)
assert entries[0].intensity == 0.35
# ── timings 数量不匹配回退字数占比(GPU端表情公式配套修复2,固化行为)───
class TestAlignTimingsMismatchCharRatio:
def test_mismatch_falls_back_to_char_ratio_with_emo_mapping(self):
"""LLM 情绪分句(按逗号)与 TTS 分句(句末标点)数量不一致时,
必须回退到按情绪分句字数占比分配,保证 emo 与时间段一一对应."""
segs = [
EmotionSegment("a" * 6, EMO_HAPPY, 0.5),
EmotionSegment("b" * 2, EMO_SAD, 0.5),
EmotionSegment("c" * 2, EMO_SURPRISE, 0.5),
]
timings = [{"start": 0.0, "end": 10.0}] # len 1 != 3
entries = align_timeline_by_timings(segs, timings, 10.0)
assert len(entries) == 3
assert entries[0].start == 0.0
assert entries[0].end == 6.0
assert entries[1].start == 6.0
assert entries[1].end == 8.0
assert entries[2].start == 8.0
assert entries[2].end == 10.0
assert [e.emo for e in entries] == [EMO_HAPPY, EMO_SAD, EMO_SURPRISE]