Merge pull request 'fix(#2174 P0): 信任链i2i改为t2i文生图,实测通过Seedance肖像审核' (#2175) from feat/2174-trust-chain-t2i into develop
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check push changed paths (pull_request) 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 3s
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 / Check if frontend-only change (pull_request) Successful in 5s
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 Web 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 / Check push changed paths (push) Successful in 19s
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 / 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 / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 55s
CI/CD Pipeline / Build Staging API Image (push) Successful in 53s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 45s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (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 / 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 / PR Build Worker Image (pull_request) Successful in 1m22s
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 40s
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
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m3s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m13s
CI/CD Pipeline / Integration Tests (push) Successful in 4m18s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 4m29s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m41s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 5m19s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Style (push) Successful in 5m21s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 3m22s
AI Code Review / AI Code Review (pull_request) Successful in 7m1s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m10s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 5m34s
CI/CD Pipeline / Validate - Security (push) Successful in 9m12s
CI/CD Pipeline / Unit Tests (push) Successful in 13m54s
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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped

fix(#2174): 信任链从i2i改为t2i文生图,VLM提取人物特征→Seedream t2i纯文生图→Seedance i2v,实测通过肖像审核,人物保留度>90%
This commit was merged in pull request #2175.
This commit is contained in:
2026-10-04 15:49:02 +08:00
4 changed files with 106 additions and 118 deletions
+22 -56
View File
@@ -145,15 +145,11 @@ class TestImageGenerationHappyPath:
class TestTrustChainIntegration:
def test_with_reference_image_triggers_seedream_then_seedance_with_reference_image_role(self, tmp_path):
def test_pre_trusted_images_replace_original_refs(self, tmp_path):
"""#2174: 传 pre_trusted_images(预热好的t2i信任图)时,替换原image_url/ref_imgs发给Seedance,不再现场跑Seedream。"""
client = _make_client()
captured_calls = []
seedream_ok = MagicMock(status_code=200)
seedream_ok.json.return_value = {"data": [{"url": "https://ai.example.com/trusted.png"}]}
seedream_ok.raise_for_status = MagicMock()
seedream_ok.text = ""
task_ok = MagicMock(status_code=200)
task_ok.json.return_value = {"id": "t-trust"}
task_ok.raise_for_status = MagicMock()
@@ -182,8 +178,6 @@ class TestTrustChainIntegration:
def fake_post(url, **kwargs):
captured_calls.append({"url": url, "json": kwargs.get("json")})
if "/images/generations" in url:
return seedream_ok
return task_ok
fake_uuid = MagicMock()
@@ -193,7 +187,7 @@ class TestTrustChainIntegration:
patch("packages.shared.ai_client.httpx.get", return_value=poll_ok),
patch("packages.shared.ai_client.httpx.stream", return_value=FakeStream()),
patch("packages.shared.ai_client.time.sleep", return_value=None),
patch("packages.shared.ai_client.time.time", side_effect=_fake_time(stable_calls=3)),
patch("packages.shared.ai_client.time.time", side_effect=_fake_time(stable_calls=2)),
patch("packages.shared.ai_client.uuid.uuid4", return_value=fake_uuid),
patch("packages.shared.ai_client.get_shared_settings") as ms,
):
@@ -205,16 +199,17 @@ class TestTrustChainIntegration:
out = client.video_generation(
"人物在海边散步",
image_url="https://img/raw.jpg",
pre_trusted_images=["https://ai.example.com/trusted.png"],
duration=5,
ratio="9:16",
resolution="720p",
output_dir=str(tmp_path),
)
assert out is not None
assert len(captured_calls) == 2
assert "/images/generations" in captured_calls[0]["url"]
assert captured_calls[0]["json"]["image"] == "https://img/raw.jpg"
seedance_payload = captured_calls[1]["json"]
# 只有一次 Seedance 调用(不现场跑Seedream)
assert len(captured_calls) == 1
assert "/contents/generations/tasks" in captured_calls[0]["url"]
seedance_payload = captured_calls[0]["json"]
content = seedance_payload["content"]
img_items = [c for c in content if c.get("type") == "image_url"]
assert len(img_items) == 1
@@ -276,17 +271,11 @@ class TestTrustChainIntegration:
content = captured_calls[0]["json"]["content"]
assert all(c.get("type") != "image_url" for c in content)
def test_seedream_failure_falls_back_to_original_image(self, tmp_path):
def test_no_preheated_images_falls_back_to_first_frame_mode(self, tmp_path):
"""#2174: 无 pre_trusted_images 时原图走 first_frame 模式(ratio=adaptive),不再现场跑 Seedream。"""
client = _make_client(max_retries=0)
captured_calls = []
seedream_fail = MagicMock(status_code=400)
seedream_fail.text = '{"error":{"code":"QuotaExceeded","message":"quota"}}'
seedream_fail.json.return_value = {"error": {"code": "QuotaExceeded"}}
seedream_fail.raise_for_status.side_effect = httpx.HTTPStatusError(
"q", request=MagicMock(), response=seedream_fail
)
task_ok = MagicMock(status_code=200)
task_ok.json.return_value = {"id": "t-fb"}
task_ok.raise_for_status = MagicMock()
@@ -312,8 +301,6 @@ class TestTrustChainIntegration:
def fake_post(url, **kwargs):
captured_calls.append({"url": url, "json": kwargs.get("json")})
if "/images/generations" in url:
return seedream_fail
return task_ok
fake_uuid = MagicMock()
@@ -340,8 +327,9 @@ class TestTrustChainIntegration:
output_dir=str(tmp_path),
)
assert out is not None
assert len(captured_calls) == 2
seedance_payload = captured_calls[1]["json"]
# 不现场跑 Seedream,只有一次 Seedance 调用
assert len(captured_calls) == 1
seedance_payload = captured_calls[0]["json"]
content = seedance_payload["content"]
img_items = [c for c in content if c.get("type") == "image_url"]
assert len(img_items) == 1
@@ -458,28 +446,11 @@ class TestTrustChainBranches:
assert err["error_code"] == "auth_error"
def test_trust_chain_partial_seedream_success_falls_back(self, tmp_path):
"""多张参考图中第 2 张 Seedream 失败→整体回退原图直传。"""
"""#2174: 预热结果为空/None 时回退原图直传(走#2166的400→t2v自动降级路径)。"""
client = _make_client(max_retries=0)
def make_seedream_fail():
r = MagicMock(status_code=500, text="err")
r.json.return_value = {"error": {}}
r.raise_for_status.side_effect = httpx.HTTPStatusError("e", request=MagicMock(), response=r)
return r
seedream_ok = MagicMock(status_code=200, text="")
seedream_ok.json.return_value = {"data": [{"url": "https://ai.example.com/a.png"}]}
seedream_ok.raise_for_status = MagicMock()
# 两张参考图(image_url + reference_images 各一张),Seedream 第 1 张 ok、第 2 张失败 → 回退
call_n = {"n": 0}
# 预热结果传 None → 应该直接用原图发给 Seedance
def fake_post(url, **kwargs):
if "/images/generations" in url:
call_n["n"] += 1
if call_n["n"] == 1:
return seedream_ok
return make_seedream_fail()
# Seedance create task(收到原图直传时会调用)
t = MagicMock(status_code=200, text="")
t.json.return_value = {"id": "t-partial"}
@@ -553,15 +524,11 @@ class TestTrustChainBranches:
assert r is not None
assert captured_kwargs["timeout"] == 120
def test_trust_chain_no_image_url_only_ref_imgs(self, tmp_path):
"""不传 image_url 仅传 reference_images 时走 trust chain 成功,ref_imgs 覆盖替换(line 537 else 分支)。"""
def test_trust_chain_uses_preheated_t2i_images(self, tmp_path):
"""#2174: 传 pre_trusted_images(预热好的t2i信任图)时,替换原参考图发给Seedance。"""
client = _make_client()
captured = []
seedream_ok = MagicMock(status_code=200, text="")
seedream_ok.json.return_value = {"data": [{"url": "https://ai.example.com/ref.png"}]}
seedream_ok.raise_for_status = MagicMock()
task_ok = MagicMock(status_code=200, text="")
task_ok.json.return_value = {"id": "t-refonly"}
task_ok.raise_for_status = MagicMock()
@@ -587,8 +554,6 @@ class TestTrustChainBranches:
def fake_post(url, **kw):
captured.append({"url": url, "json": kw.get("json")})
if "/images/generations" in url:
return seedream_ok
return task_ok
fu = MagicMock()
@@ -610,19 +575,20 @@ class TestTrustChainBranches:
out = client.video_generation(
"人物散步",
reference_images=["https://img/portrait.jpg"],
pre_trusted_images=["https://ai.example.com/t2i-portrait.png"],
duration=5,
ratio="9:16",
output_dir=str(tmp_path),
)
assert out is not None
# 第一次是 Seedream 成功,第二次是 Seedance 创建任务
assert len(captured) == 2
seedance_payload = captured[1]["json"]
# 只有一次 Seedance 创建任务(预热已完成,不再现场跑 Seedream)
assert len(captured) == 1
seedance_payload = captured[0]["json"]
content = seedance_payload["content"]
img_items = [c for c in content if c.get("type") == "image_url"]
assert len(img_items) == 1
# 不传 image_url,信任链产物放 ref_imgs,走 reference_image 模式(非 first_frame)
assert img_items[0]["image_url"]["url"] == "https://ai.example.com/ref.png"
assert img_items[0]["image_url"]["url"] == "https://ai.example.com/t2i-portrait.png"
assert img_items[0]["role"] == "reference_image"
# 因为没有 image_url,没有 text 也没有 extra_refs 之外的字段,应保留用户 ratio=9:16
assert seedance_payload.get("ratio") == "9:16"