Compare commits

...

2 Commits

Author SHA1 Message Date
xiaoxia cfc584ad11 fix(ditto-emotion): GPU表情公式配套——全兜底置空+强度保底0.35+对齐固化 (#2292)
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 1s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
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 / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 4s
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4s
CI/CD Pipeline / Check push changed paths (push) Successful in 7s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) 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 / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Unit 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 / Build Staging Web Image (push) 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 / 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 Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
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 / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 52s
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 57s
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 53s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m29s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m27s
CI/CD Pipeline / Integration Tests (push) Successful in 3m27s
CI/CD Pipeline / Validate - Style (push) Successful in 4m17s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 4m38s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m46s
CI/CD Pipeline / Build Staging API Image (push) Successful in 4m54s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Successful in 17s
AI Code Review / AI Code Review (pull_request) Successful in 6m55s
CI/CD Pipeline / Validate - Security (push) Successful in 7m45s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 2m57s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m45s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m25s
CI/CD Pipeline / Unit Tests (push) Successful in 12m3s
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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Failing after 4m23s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-10-14 07:26:10 +08:00
xiaoxia afc89142c1 fix(ai-avatar): finalize 接口超时延长到120s (#2291)
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 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 5s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m2s
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
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 / 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 / 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 / Frontend Unit Tests (push) Successful in 3m5s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 27s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m25s
CI/CD Pipeline / Build Staging API Image (push) Successful in 44s
PR Automation / Auto Merge on CI Green + Approved (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 / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
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 / Validate - Python (mypy + alembic) (push) Successful in 6m1s
CI/CD Pipeline / Validate - Style (push) Successful in 6m42s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m51s
AI Code Review / AI Code Review (pull_request) Successful in 6m51s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 3m22s
CI/CD Pipeline / Integration Tests (push) Successful in 6m39s
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 2m40s
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 Staging (Watchtower auto-deploy) (push) Successful in 1m16s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m13s
CI/CD Pipeline / Validate - Security (push) Successful in 12m47s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m45s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 4m53s
CI/CD Pipeline / Unit Tests (push) Successful in 14m58s
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 / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
2026-10-14 06:29:41 +08:00
3 changed files with 134 additions and 2 deletions
@@ -126,4 +126,5 @@ 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 }, // 后端兜底抽帧/写入成片库可能耗时较长
)
+21 -2
View File
@@ -4,7 +4,8 @@
1. 分句:先按 。!?; 句末标点切分;整段无句末标点且超长时按逗号等语义停顿二次切分
2. 调 DoubaoClient.chat_completion 分析每句表情(emo: 0-7 八种全部开放, intensity: 0-1)
3. 结果 LRU 缓存(文案 hash → 情绪列表)
4. LLM 失败/超时/格式错 → 返回空列表(降级中性表情,不阻塞生成)
4. LLM 失败/超时/格式错 → analyze 层预分句全中性兜底;build_timeline 识别全兜底特征后
返回空串不传 emo_timeline(避免压制 GPU 端自身表情逻辑),不阻塞生成
5. TTS 完成后按字数比例或 sentence_timings 对齐成秒级 timeline
"""
@@ -31,6 +32,11 @@ 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 保留标点在前句末尾)
@@ -82,7 +88,8 @@ class EmotionTimelineEntry:
self.start = round(start, 2)
self.end = round(end, 2)
self.emo = emo
self.intensity = round(intensity, 2)
# 强度保底:低于 MIN_TIMELINE_INTENSITY 的信号在 GPU 混合公式中压不过中性
self.intensity = round(max(intensity, MIN_TIMELINE_INTENSITY), 2)
def to_dict(self) -> dict[str, Any]:
return {
@@ -361,6 +368,18 @@ 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,8 +8,11 @@ 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,
@@ -362,3 +365,112 @@ 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]