Files
xiaoxia-saas/tests/unit/test_1754_visual_perturbation.py
xiaoxia 4b3feb2d88
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 8s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 12s
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 / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (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 / Integration Tests (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 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 / PR Build Web Image (pull_request) Successful in 37s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 38s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 40s
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 / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m35s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m6s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 6m35s
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 (push) Successful in 1s
CI/CD Pipeline / Frontend Lint (push) 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 / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 15s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 15s
CI/CD Pipeline / Build Staging API Image (push) Successful in 18s
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 49s
CI/CD Pipeline / Integration Tests (push) Successful in 1m20s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m34s
CI/CD Pipeline / Validate - Style (push) Successful in 2m7s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m37s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m55s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m58s
CI/CD Pipeline / Validate - Security (push) Successful in 6m9s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 4m55s
CI/CD Pipeline / Unit Tests (push) Successful in 6m53s
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
feat(#1754): 渲染期画面扰动——让批量视频本身更不同 (#1755)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-07 13:55:47 +08:00

253 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""#1754 渲染期画面扰动——让批量视频本身更不同。
测试覆盖:
1. generate_visual_perturbation 生成合法参数
2. _build_filter_complex 中视觉扰动滤镜正确应用
3. _render_pass_through 中视觉扰动滤镜正确应用
4. ensure_variant_plans 为每个变体生成并存储扰动参数
"""
from __future__ import annotations
import random
import sys
import types
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
# ── 1. generate_visual_perturbation 单元测试 ──────────────────────────────────
class TestGenerateVisualPerturbation:
"""测试 generate_visual_perturbation 函数。"""
def test_returns_dict_with_expected_keys(self):
"""返回 dict 包含 hflip/zoom_ratio/speed_factor/brightness_shift。"""
from packages.domain.variant_plan_selector import generate_visual_perturbation
result = generate_visual_perturbation(random.Random(42))
assert set(result.keys()) == {"hflip", "zoom_ratio", "speed_factor", "brightness_shift"}
def test_hflip_is_bool(self):
"""hflip 是 bool 类型。"""
from packages.domain.variant_plan_selector import generate_visual_perturbation
result = generate_visual_perturbation(random.Random(42))
assert isinstance(result["hflip"], bool)
def test_zoom_ratio_range(self):
"""zoom_ratio 在 1.0~1.08 范围内。"""
from packages.domain.variant_plan_selector import generate_visual_perturbation
for seed in range(100):
result = generate_visual_perturbation(random.Random(seed))
assert 1.0 <= result["zoom_ratio"] <= 1.08, f"seed={seed}: {result['zoom_ratio']}"
def test_speed_factor_range(self):
"""speed_factor 在 0.95~1.05 范围内。"""
from packages.domain.variant_plan_selector import generate_visual_perturbation
for seed in range(100):
result = generate_visual_perturbation(random.Random(seed))
assert 0.95 <= result["speed_factor"] <= 1.05, f"seed={seed}: {result['speed_factor']}"
def test_brightness_shift_values(self):
"""brightness_shift 只能取特定值。"""
from packages.domain.variant_plan_selector import generate_visual_perturbation
allowed = {-10, -5, 0, 5, 10}
for seed in range(100):
result = generate_visual_perturbation(random.Random(seed))
assert result["brightness_shift"] in allowed, f"seed={seed}: {result['brightness_shift']}"
def test_different_seeds_produce_different_results(self):
"""不同种子产生不同扰动参数。"""
from packages.domain.variant_plan_selector import generate_visual_perturbation
results = [generate_visual_perturbation(random.Random(seed)) for seed in range(20)]
# 至少有一些不同的 zoom_ratio
zooms = {r["zoom_ratio"] for r in results}
assert len(zooms) > 1, "所有种子产生了相同的 zoom_ratio"
def test_hflip_probability_approximately_30_percent(self):
"""hflip 概率约 30%(大样本验证)。"""
from packages.domain.variant_plan_selector import generate_visual_perturbation
n = 1000
flips = sum(1 for seed in range(n) if generate_visual_perturbation(random.Random(seed))["hflip"])
ratio = flips / n
assert 0.20 <= ratio <= 0.40, f"hflip 比例 {ratio:.2%} 偏离 30%"
# ── 2. 渲染侧滤镜应用测试 ────────────────────────────────────────────────────
class TestVisualPerturbationFilters:
"""测试 _get_visual_perturbation 和滤镜应用方法。"""
def _make_render_service(self, perturbation: dict | None = None):
"""构造一个带 mock plan 的 UnifiedRenderService 实例。"""
# 避免实际初始化
from video_processing.unified_render_service import UnifiedRenderService
svc = object.__new__(UnifiedRenderService)
svc.output_width = 1920
svc.output_height = 1080
svc.plan = MagicMock()
svc.plan.config = {"visual_perturbation": perturbation} if perturbation else {}
return svc
def test_get_visual_perturbation_empty_when_no_config(self):
"""plan.config 无 visual_perturbation 时返回空 dict。"""
svc = self._make_render_service(None)
assert svc._get_visual_perturbation() == {}
def test_get_visual_perturbation_parses_values(self):
"""正确解析 perturbation dict 中的各字段。"""
perturbation = {
"hflip": True,
"zoom_ratio": 1.05,
"speed_factor": 0.97,
"brightness_shift": 5,
}
svc = self._make_render_service(perturbation)
result = svc._get_visual_perturbation()
assert result["hflip"] is True
assert result["zoom_ratio"] == 1.05
assert result["speed_factor"] == 0.97
assert result["brightness_shift"] == 5
def test_get_visual_perturbation_clamps_zoom_ratio(self):
"""zoom_ratio 被限制在 1.0~1.2 范围内。"""
svc = self._make_render_service({"zoom_ratio": 2.0})
assert svc._get_visual_perturbation()["zoom_ratio"] == 1.2
svc2 = self._make_render_service({"zoom_ratio": 0.5})
assert svc2._get_visual_perturbation()["zoom_ratio"] == 1.0
def test_get_visual_perturbation_clamps_brightness(self):
"""brightness_shift 被限制在 -30~30 范围内。"""
svc = self._make_render_service({"brightness_shift": 50})
assert svc._get_visual_perturbation()["brightness_shift"] == 30
svc2 = self._make_render_service({"brightness_shift": -50})
assert svc2._get_visual_perturbation()["brightness_shift"] == -30
def test_pre_scale_hflip(self):
"""hflip=True 时 filters 列表追加 hflip。"""
svc = self._make_render_service({"hflip": True})
filters: list[str] = []
svc._apply_visual_perturbation_pre_scale(filters, svc._get_visual_perturbation())
assert "hflip" in filters
def test_pre_scale_no_hflip(self):
"""hflip=False 时 filters 列表不变。"""
svc = self._make_render_service({"hflip": False})
filters: list[str] = ["existing_filter"]
svc._apply_visual_perturbation_pre_scale(filters, svc._get_visual_perturbation())
assert filters == ["existing_filter"]
def test_post_scale_zoom_and_brightness(self):
"""zoom_ratio>1 时追加 scale+cropbrightness!=0 时追加 eq。"""
svc = self._make_render_service({"zoom_ratio": 1.05, "brightness_shift": 10})
perturbation = svc._get_visual_perturbation()
filters: list[str] = []
svc._apply_visual_perturbation_post_scale(filters, perturbation)
# zoom: scale=2016:1134 (1920*1.05=2016, 1080*1.05=1134)
assert "scale=2016:1134" in filters
assert "crop=1920:1080" in filters
# brightness: 10/100 = 0.1
assert "eq=brightness=0.100" in filters
def test_post_scale_no_perturbation_when_defaults(self):
"""zoom_ratio=1.0 且 brightness_shift=0 时不追加滤镜。"""
svc = self._make_render_service({"zoom_ratio": 1.0, "brightness_shift": 0})
perturbation = svc._get_visual_perturbation()
filters: list[str] = []
svc._apply_visual_perturbation_post_scale(filters, perturbation)
assert filters == []
# ── 3. ensure_variant_plans 扰动注入测试 ──────────────────────────────────────
class TestEnsureVariantPlansPerturbation:
"""测试 ensure_variant_plans 为每个变体注入视觉扰动。"""
def test_variant_plans_get_perturbation_in_config(self):
"""每个变体 plan 的 config 中包含 visual_perturbation。"""
from app.services.edit_plan_service import EditPlanService
db = MagicMock()
svc = EditPlanService(db)
# Mock 所有依赖方法
mock_plan0 = MagicMock()
mock_plan0.id = "plan-0"
mock_variant = MagicMock()
mock_variant.id = "plan-1"
svc.clone_plan_for_variant = MagicMock(return_value=mock_plan0)
svc.reselect_plan_for_variant = MagicMock(return_value=mock_variant)
svc.apply_voice_duration_to_plan = MagicMock()
svc.mark_clips_ready = MagicMock()
svc.update_plan_config = MagicMock()
rng = random.Random(42)
plan_ids = svc.ensure_variant_plans(
"source-plan-id",
count=3,
candidate_asset_ids=["asset-1", "asset-2", "asset-3"],
rng=rng,
)
assert len(plan_ids) == 3
# update_plan_config 应该被调用 3 次(每个变体一次)
assert svc.update_plan_config.call_count == 3
# 检查每次调用都传入了 visual_perturbation
for call in svc.update_plan_config.call_args_list:
args, kwargs = call
plan_id = args[0]
config_updates = args[1]
assert "visual_perturbation" in config_updates
perturbation = config_updates["visual_perturbation"]
assert "hflip" in perturbation
assert "zoom_ratio" in perturbation
assert "speed_factor" in perturbation
assert "brightness_shift" in perturbation
def test_variant_0_no_hflip(self):
"""变体 0(预览 plan)不做 hflip。"""
from app.services.edit_plan_service import EditPlanService
db = MagicMock()
svc = EditPlanService(db)
mock_plan0 = MagicMock()
mock_plan0.id = "plan-0"
mock_variant = MagicMock()
mock_variant.id = "plan-1"
svc.clone_plan_for_variant = MagicMock(return_value=mock_plan0)
svc.reselect_plan_for_variant = MagicMock(return_value=mock_variant)
svc.apply_voice_duration_to_plan = MagicMock()
svc.mark_clips_ready = MagicMock()
svc.update_plan_config = MagicMock()
rng = random.Random(42)
svc.ensure_variant_plans(
"source-plan-id",
count=2,
candidate_asset_ids=["asset-1", "asset-2"],
rng=rng,
)
# 第一次调用(变体 0)的 hflip 应该是 False
first_call_args = svc.update_plan_config.call_args_list[0]
perturbation_0 = first_call_args[0][1]["visual_perturbation"]
assert perturbation_0["hflip"] is False