feat(#1754): 渲染期画面扰动——让批量视频本身更不同 (#1755)
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 / PR Build Web Image (pull_request) Successful in 37s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 38s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 40s
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
AI Code Review / AI Code Review (pull_request) Successful in 6m35s
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 / 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 / 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 / Frontend Unit Tests (push) Successful in 3m55s
CI/CD Pipeline / Validate - Security (push) Successful in 6m9s
CI/CD Pipeline / Unit Tests (push) Successful in 6m53s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 217h5m50s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 217h5m52s
CI/CD Pipeline / Deploy Production (push) Failing after 217h5m50s
CI/CD Pipeline / Build Production API Image (push) Failing after 217h5m52s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 217h12m23s
CI/CD Pipeline / PR Build API Image (push) Failing after 217h12m45s
CI/CD Pipeline / Frontend Lint (push) Failing after 217h12m45s
CI/CD Pipeline / ACR Image Cleanup (push) Failing after 217h11m27s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 217h11m27s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 217h25m2s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 217h27m26s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 217h27m28s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 217h27m29s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 217h27m29s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 217h28m5s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 217h27m30s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 217h28m6s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 217h28m7s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 217h28m8s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 217h28m8s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 217h28m9s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 217h28m9s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 217h28m9s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 217h28m9s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 217h28m10s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 217h28m10s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 217h28m17s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 217h28m10s
CI/CD Pipeline / CI Gate (push) Failing after 217h40m30s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 217h47m1s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 217h47m23s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 217h47m26s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 217h46m5s
CI/CD Pipeline / Build Production Web Image (push) Failing after 217h40m30s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 217h47m1s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 218h2m6s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 218h2m44s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 218h2m46s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 218h2m47s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 218h2m47s
CI/CD Pipeline / PR Build Web Image (push) Failing after 217h47m23s

Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
This commit was merged in pull request #1755.
This commit is contained in:
2026-09-07 13:55:47 +08:00
committed by auto-approve-bot
parent d8da69970b
commit 4b3feb2d88
4 changed files with 347 additions and 6 deletions
@@ -1308,10 +1308,15 @@ class UnifiedRenderService:
if freeze_seconds > 0:
filters.append(f"tpad=stop_mode=clone:stop_duration={freeze_seconds:.3f}")
# 调速 — 与 filter_complex 路径一致
# 视觉扰动(plan 级别,直通模式同样适用)
vp = self._get_visual_perturbation()
# 调速 — 与 filter_complex 路径一致(叠加视觉扰动 speed_factor)
speed = UnifiedRenderService._clip_speed(clip)
if abs(speed - 1.0) >= 1e-6:
filters.append(f"setpts=PTS/{speed:.4f}")
vp_speed = vp.get("speed_factor", 1.0) if vp else 1.0
effective_speed = speed * vp_speed
if abs(effective_speed - 1.0) >= 1e-6:
filters.append(f"setpts=PTS/{effective_speed:.4f}")
# 倒放滤镜
reverse_config = ReverseConfig.from_dict(clip.config.get("reverse"))
@@ -1320,6 +1325,10 @@ class UnifiedRenderService:
if reverse_filter:
filters.append(reverse_filter)
# 视觉扰动:hflip(在 scale 之前)
if vp:
self._apply_visual_perturbation_pre_scale(filters, vp)
# scale + pad(等比缩放+留黑边)
if role in ("overlay", "corner_voice"):
pip_w = int(self.output_width * _PIP_SCALE)
@@ -1334,6 +1343,10 @@ class UnifiedRenderService:
filters.append(f"scale={self.output_width}:{self.output_height}:force_original_aspect_ratio=decrease")
filters.append(f"pad={self.output_width}:{self.output_height}:trunc((ow-iw)/2):trunc((oh-ih)/2):black")
# 视觉扰动:zoom + brightness(在 scale+pad 之后、调色之前)
if vp:
self._apply_visual_perturbation_post_scale(filters, vp)
# 调色滤镜
color_grade = ColorGradeConfig.from_dict(clip.config.get("color_grade"))
if color_grade.enabled and color_grade.has_effect():
@@ -1729,6 +1742,8 @@ class UnifiedRenderService:
# Step 1: 预处理每个 clip — trim + scale + setpts
# 为每个 clip 生成预处理后的标签 [v0], [v1], ...
preprocessed_labels: list[str] = []
# 视觉扰动(plan 级别,所有 clip 共享同一套扰动参数)
vp = self._get_visual_perturbation()
for i, clip in enumerate(all_clips):
label = f"v{i}"
role = _resolve_layer_role(clip.clip_type, clip.config)
@@ -1749,10 +1764,12 @@ class UnifiedRenderService:
filters.append(f"trim=duration={trim_dur:.3f}")
filters.append("setpts=PTS-STARTPTS")
# 调速 — 基于 setpts 改变播放速度
# 调速 — 基于 setpts 改变播放速度(叠加视觉扰动 speed_factor)
speed = UnifiedRenderService._clip_speed(clip)
if abs(speed - 1.0) >= 1e-6:
filters.append(f"setpts=PTS/{speed:.4f}")
vp_speed = vp.get("speed_factor", 1.0) if vp else 1.0
effective_speed = speed * vp_speed
if abs(effective_speed - 1.0) >= 1e-6:
filters.append(f"setpts=PTS/{effective_speed:.4f}")
# 倒放滤镜(在 trim 之后、scale 之前应用)
reverse_config = ReverseConfig.from_dict(clip.config.get("reverse"))
@@ -1761,6 +1778,10 @@ class UnifiedRenderService:
if reverse_filter:
filters.append(reverse_filter)
# 视觉扰动:hflip(在 scale 之前,翻转原始画面)
if vp:
self._apply_visual_perturbation_pre_scale(filters, vp)
# scale
if role in ("overlay", "corner_voice"):
pip_w = int(self.output_width * _PIP_SCALE)
@@ -1775,6 +1796,10 @@ class UnifiedRenderService:
filters.append(f"scale={self.output_width}:{self.output_height}:force_original_aspect_ratio=decrease")
filters.append(f"pad={self.output_width}:{self.output_height}:trunc((ow-iw)/2):trunc((oh-ih)/2):black")
# 视觉扰动:zoom + brightness(在 scale+pad 之后、调色之前)
if vp:
self._apply_visual_perturbation_post_scale(filters, vp)
# 调色滤镜(每个 clip 独立的 color grade 配置)
color_grade = ColorGradeConfig.from_dict(clip.config.get("color_grade"))
if color_grade.enabled and color_grade.has_effect():
@@ -2190,6 +2215,35 @@ class UnifiedRenderService:
"""
return _clip_playback_speed_pure(getattr(clip, "playback_speed", 1.0))
def _get_visual_perturbation(self) -> dict:
# 读取当前 plan 的视觉扰动参数(plan.config.visual_perturbation)
perturbation = (self.plan.config or {}).get("visual_perturbation") or {}
if not perturbation:
return {}
return {
"hflip": bool(perturbation.get("hflip", False)),
"zoom_ratio": max(1.0, min(1.2, float(perturbation.get("zoom_ratio", 1.0) or 1.0))),
"speed_factor": max(0.8, min(1.2, float(perturbation.get("speed_factor", 1.0) or 1.0))),
"brightness_shift": max(-30, min(30, int(perturbation.get("brightness_shift", 0) or 0))),
}
def _apply_visual_perturbation_pre_scale(self, filters: list[str], perturbation: dict) -> None:
# scale+pad 之前的扰动(hflip),就地修改 filters
if perturbation.get("hflip"):
filters.append("hflip")
def _apply_visual_perturbation_post_scale(self, filters: list[str], perturbation: dict) -> None:
# scale+pad 之后的扰动(zoom/brightness),就地修改 filters
zoom = perturbation.get("zoom_ratio", 1.0)
if zoom > 1.0 + 1e-6:
zoomed_w = int(self.output_width * zoom)
zoomed_h = int(self.output_height * zoom)
filters.append(f"scale={zoomed_w}:{zoomed_h}")
filters.append(f"crop={self.output_width}:{self.output_height}")
brightness = perturbation.get("brightness_shift", 0)
if brightness != 0:
filters.append(f"eq=brightness={brightness / 100.0:.3f}")
@staticmethod
def _clip_volume(clip: ResolvedClip) -> float:
"""获取 clip 的音量(config.volume)。缺省 1.0 原声,0.0 静音。"""