fix(#462): 修复直通模式下水印静默失效 - 配置格式不兼容 (#739)
CI/CD Pipeline / Validate - Migration (alembic) (push) Failing after 57s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m29s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 2m31s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 2m41s
CI/CD Pipeline / Integration Tests (push) Failing after 1m10s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m59s
CI/CD Pipeline / Unit Tests (push) Successful in 3m30s
CI/CD Pipeline / Build Staging API Image (push) Successful in 16m17s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 36s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 41s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 15m51s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 15m51s
CI/CD Pipeline / Frontend Lint (push) Successful in 42m37s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 39m39s
CI/CD Pipeline / Production Browser E2E (push) Failing after 1336h29m56s
CI/CD Pipeline / Deploy Production (push) Failing after 1336h30m4s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 1336h30m34s
CI/CD Pipeline / Build Production Web Image (push) Failing after 1336h30m36s
CI/CD Pipeline / Build Production API Image (push) Failing after 1336h30m38s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 1336h33m45s
CI/CD Pipeline / PR Build Web Image (push) Failing after 1336h33m45s
CI/CD Pipeline / PR Build API Image (push) Failing after 1336h33m45s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 1336h33m51s
CI/CD Pipeline / Validate - Migration (alembic) (push) Failing after 57s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m29s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 2m31s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 2m41s
CI/CD Pipeline / Integration Tests (push) Failing after 1m10s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m59s
CI/CD Pipeline / Unit Tests (push) Successful in 3m30s
CI/CD Pipeline / Build Staging API Image (push) Successful in 16m17s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 36s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 41s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 15m51s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 15m51s
CI/CD Pipeline / Frontend Lint (push) Successful in 42m37s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 39m39s
CI/CD Pipeline / Production Browser E2E (push) Failing after 1336h29m56s
CI/CD Pipeline / Deploy Production (push) Failing after 1336h30m4s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 1336h30m34s
CI/CD Pipeline / Build Production Web Image (push) Failing after 1336h30m36s
CI/CD Pipeline / Build Production API Image (push) Failing after 1336h30m38s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 1336h33m45s
CI/CD Pipeline / PR Build Web Image (push) Failing after 1336h33m45s
CI/CD Pipeline / PR Build API Image (push) Failing after 1336h33m45s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 1336h33m51s
This commit was merged in pull request #739.
This commit is contained in:
@@ -843,6 +843,44 @@ class UnifiedRenderService:
|
||||
logger.warning("TTS 配音异常,跳过: %s", e)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _resolve_watermark_config(plan_config: dict[str, Any] | None) -> WatermarkConfig | None:
|
||||
"""从 plan config 中解析水印配置,兼容两种存储格式.
|
||||
|
||||
支持格式:
|
||||
1. 嵌套格式:config.watermark = {enabled, mode, text, image_path, ...}
|
||||
2. 扁平格式(导出配置):config.export.watermark_enabled + config.export.watermark_text
|
||||
|
||||
Returns:
|
||||
WatermarkConfig 或 None(未启用水印时)
|
||||
"""
|
||||
if not plan_config or not isinstance(plan_config, dict):
|
||||
return None
|
||||
|
||||
# 格式1: 嵌套 watermark 对象(优先)
|
||||
wm_data = plan_config.get("watermark")
|
||||
if isinstance(wm_data, dict) and wm_data:
|
||||
config = WatermarkConfig.from_dict(wm_data)
|
||||
if config is not None:
|
||||
return config
|
||||
|
||||
# 格式2: 扁平 export.watermark_enabled + export.watermark_text
|
||||
export_cfg = plan_config.get("export")
|
||||
if isinstance(export_cfg, dict) and export_cfg:
|
||||
enabled = export_cfg.get("watermark_enabled", False)
|
||||
text = export_cfg.get("watermark_text", "") or ""
|
||||
if enabled and text:
|
||||
return WatermarkConfig(
|
||||
mode="text",
|
||||
text=str(text),
|
||||
position=export_cfg.get("watermark_position", "bottom_right"),
|
||||
opacity=float(export_cfg.get("watermark_opacity", 0.6)),
|
||||
font_size=int(export_cfg.get("watermark_font_size", 24)),
|
||||
font_color=str(export_cfg.get("watermark_font_color", "white")),
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
def _can_use_pass_through(self, layers: list[RenderLayer]) -> bool:
|
||||
"""判断是否可以走直通优化路径。
|
||||
|
||||
@@ -864,15 +902,11 @@ class UnifiedRenderService:
|
||||
if isinstance(plan_config, dict) and plan_config.get("stickers"):
|
||||
return False
|
||||
|
||||
# 有水印时禁用直通(图片水印需要额外输入,统一走 filter_complex)
|
||||
try:
|
||||
from video_processing.watermark_engine import WatermarkConfig
|
||||
|
||||
wm_config = WatermarkConfig.from_dict(plan_config.get("watermark"))
|
||||
if wm_config is not None and wm_config.validate()[0]:
|
||||
return False
|
||||
except Exception:
|
||||
pass
|
||||
# 有水印时禁用直通(图片水印需要额外输入,统一走 filter_complex;
|
||||
# 文字水印虽然可以 -vf 叠加,但为了保持路径统一也走 filter_complex)
|
||||
wm_config = UnifiedRenderService._resolve_watermark_config(plan_config)
|
||||
if wm_config is not None and wm_config.validate()[0]:
|
||||
return False
|
||||
|
||||
# 有调速时仍然可以走直通(视频调速通过 setpts 实现,单输入即可)
|
||||
|
||||
@@ -1117,11 +1151,11 @@ class UnifiedRenderService:
|
||||
filters.append(f"scale={pip_w}:{pip_h}")
|
||||
elif role == "background":
|
||||
# background: 铺满裁剪(作为底图,覆盖全屏)
|
||||
filters.append(f"scale={self.output_width}:{self.output_height}" ":force_original_aspect_ratio=increase")
|
||||
filters.append(f"scale={self.output_width}:{self.output_height}:force_original_aspect_ratio=increase")
|
||||
filters.append(f"crop={self.output_width}:{self.output_height}")
|
||||
else:
|
||||
# main / broll: 等比缩放 + 居中留黑边(保持原始比例,不裁剪内容)
|
||||
filters.append(f"scale={self.output_width}:{self.output_height}" ":force_original_aspect_ratio=decrease")
|
||||
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")
|
||||
|
||||
# 调色滤镜
|
||||
@@ -1466,16 +1500,12 @@ class UnifiedRenderService:
|
||||
pip_h = int(self.output_height * _PIP_SCALE)
|
||||
filters.append(f"scale={pip_w}:{pip_h}")
|
||||
elif role == "background":
|
||||
filters.append(
|
||||
f"scale={self.output_width}:{self.output_height}" ":force_original_aspect_ratio=increase"
|
||||
)
|
||||
filters.append(f"scale={self.output_width}:{self.output_height}:force_original_aspect_ratio=increase")
|
||||
filters.append(f"crop={self.output_width}:{self.output_height}")
|
||||
else:
|
||||
# main / broll: 等比缩放 + 居中留黑边(保持原始比例,不裁剪内容)
|
||||
# concat 要求所有输入分辨率完全一致,pad 模式确保不同宽高比的素材都能正常拼接
|
||||
filters.append(
|
||||
f"scale={self.output_width}:{self.output_height}" ":force_original_aspect_ratio=decrease"
|
||||
)
|
||||
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")
|
||||
|
||||
# 调色滤镜(每个 clip 独立的 color grade 配置)
|
||||
@@ -1563,9 +1593,7 @@ class UnifiedRenderService:
|
||||
if role in layer_output_labels:
|
||||
base_label = layer_output_labels[role]
|
||||
combined_label = f"combined_{role}"
|
||||
filter_parts.append(
|
||||
f"[{final_video_label}][{base_label}]" f"overlay=(W-w)/2:(H-h)/2[{combined_label}]"
|
||||
)
|
||||
filter_parts.append(f"[{final_video_label}][{base_label}]overlay=(W-w)/2:(H-h)/2[{combined_label}]")
|
||||
final_video_label = combined_label
|
||||
else:
|
||||
# 无 background 时,取 broll 或 main 作为基础
|
||||
@@ -1589,11 +1617,11 @@ class UnifiedRenderService:
|
||||
20,
|
||||
)
|
||||
combined_label = f"combined_{layer.role}"
|
||||
filter_parts.append(f"[{final_video_label}][{overlay_label}]" f"overlay={x}:{y}[{combined_label}]")
|
||||
filter_parts.append(f"[{final_video_label}][{overlay_label}]overlay={x}:{y}[{combined_label}]")
|
||||
final_video_label = combined_label
|
||||
|
||||
# 叠加水印(在字幕之前)
|
||||
watermark_config = WatermarkConfig.from_dict((self.plan.config or {}).get("watermark"))
|
||||
watermark_config = UnifiedRenderService._resolve_watermark_config(self.plan.config)
|
||||
if watermark_config is not None:
|
||||
wm_valid, wm_err = watermark_config.validate()
|
||||
if wm_valid:
|
||||
|
||||
Reference in New Issue
Block a user