Files
xiaoxia-saas/packages/domain/transition_randomizer.py
xiaoxia 44224cfaf6
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 4s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 7s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 5s
CI/CD Pipeline / Check push changed paths (push) Successful in 10s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 36s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 38s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 44s
CI/CD Pipeline / Build Staging API Image (push) Successful in 28s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 23s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 29s
CI/CD Pipeline / CI Gate (pull_request) Successful in 18s
CI/CD Pipeline / Validate - Style (push) Successful in 1m43s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m57s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m0s
CI/CD Pipeline / Integration Tests (push) Successful in 2m3s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m5s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m7s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 2m10s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m56s
CI/CD Pipeline / Validate - Security (push) Successful in 5m13s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m24s
AI Code Review / AI Code Review (pull_request) Successful in 6m35s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m33s
CI/CD Pipeline / Unit Tests (push) Failing after 9m24s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 196h56m39s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 196h56m42s
CI/CD Pipeline / Deploy Production (push) Failing after 196h56m39s
CI/CD Pipeline / Build Production Web Image (push) Failing after 196h56m42s
CI/CD Pipeline / Build Production API Image (push) Failing after 196h56m42s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 197h1m8s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 197h4m37s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 197h4m37s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 197h4m38s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 197h4m53s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 197h4m56s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 197h5m11s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 197h5m10s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 197h5m13s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 197h5m16s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 197h5m14s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 197h5m16s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 197h5m16s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 197h6m1s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 197h5m17s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 197h6m3s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 197h6m3s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 197h6m4s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 197h6m5s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 197h6m7s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 197h6m7s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 197h6m7s
CI/CD Pipeline / PR Build API Image (push) Failing after 197h6m7s
CI/CD Pipeline / Frontend Lint (push) Failing after 197h6m7s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 197h6m13s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 197h6m17s
CI/CD Pipeline / CI Gate (push) Failing after 197h31m25s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 197h39m53s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 197h39m58s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 197h40m45s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 197h40m47s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 197h40m51s
CI/CD Pipeline / PR Build Web Image (push) Failing after 197h40m51s
feat: 转场位置与类型随机化 (#1766) (#1786)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-08 10:17:35 +08:00

178 lines
6.9 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.
"""转场位置与类型随机化(Issue #1766).
为不同变体生成不同的转场序列(类型 + 时长 + 位置微调),
打破"所有变体转场节奏完全一致"的结构相似性,
降低平台查重识别为结构相似视频的风险。
设计要点:
1. 转场类型池:5 种效果(dissolve / zoom / slideleft / wipeleft / fade)
2. 硬切概率:保证 30%-50% 的转场是硬切(保持节奏感)
3. 转场时长随机:0.3s ~ 0.8s
4. 转场位置微调:±0.5s 偏移(通过 xfade jitter 实现)
5. 与 #1764 节奏模板协同:长片段之间的转场倾向于更长时长
"""
from __future__ import annotations
import logging
import random
logger = logging.getLogger(__name__)
# ── 转场类型池 ──────────────────────────────────────────────────────────────
#: 非硬切转场类型池(5 种效果,均为 FFmpeg xfade 支持的 transition 名称)
#: 选择标准:视觉效果差异大、FFmpeg 渲染稳定、肉眼可区分
TRANSITION_POOL: list[str] = [
"dissolve", # 溶解
"zoomin", # 缩放(放大进入)
"slideleft", # 左滑入
"wipeleft", # 左擦除
"fade", # 淡入淡出
]
#: 硬切(无转场效果),由 build_xfade_filter_chain 特殊处理(concat filter)
HARD_CUT = "cut"
# ── 时长约束 ────────────────────────────────────────────────────────────────
#: 随机转场时长下限(秒)
TRANSITION_DURATION_MIN = 0.3
#: 随机转场时长上限(秒)
TRANSITION_DURATION_MAX = 0.8
# ── 硬切比例 ────────────────────────────────────────────────────────────────
#: 硬切概率下限(至少 30% 硬切,保持节奏感)
CUT_RATIO_MIN = 0.3
#: 硬切概率上限(最多 50% 硬切,保证足够视觉变化)
CUT_RATIO_MAX = 0.5
# ── 位置微调 ────────────────────────────────────────────────────────────────
#: 转场位置最大偏移(秒),实际偏移在 [-MAX, +MAX] 均匀分布
#: 正 = 转场推迟(多留一点前一片段),负 = 转场提前
TIMING_JITTER_MAX = 0.5
# ── 内部辅助 ────────────────────────────────────────────────────────────────
def _cut_probability_for_pair(
prev_duration: float,
next_duration: float,
) -> float:
"""根据相邻片段时长计算硬切概率。
与 #1764 节奏模板协同:
- 两片段都较短(<3s,快节奏)→ 硬切概率更高(节奏更紧凑)
- 两片段都较长(>6s,慢节奏)→ 硬切概率稍低(留出过渡空间)
- 混合场景 → 基准概率(CUT_RATIO_MIN + CUT_RATIO_MAX)/ 2
返回概率始终在 [CUT_RATIO_MIN, CUT_RATIO_MAX] 范围内。
"""
base = (CUT_RATIO_MIN + CUT_RATIO_MAX) / 2 # 0.4
avg_dur = (prev_duration + next_duration) / 2
if avg_dur < 3.0:
# 快节奏:硬切概率偏高
return min(CUT_RATIO_MAX, base + 0.1)
elif avg_dur > 6.0:
# 慢节奏:硬切概率偏低(更多视觉过渡)
return max(CUT_RATIO_MIN, base - 0.1)
return base
def _apply_jitter(
base_duration: float,
jitter: float,
clip_duration: float,
) -> float:
"""给转场时长应用微调偏移,钳制到安全范围。
Args:
base_duration: 基础转场时长
jitter: 偏移量(可正可负)
clip_duration: 较短的相邻片段时长(转场不能超过此值)
Returns:
钳制后的实际转场时长
"""
effective = base_duration + jitter
# 上界:不超过相邻片段时长的 40%(留足内容时间),也不超过 MAX
upper = min(TRANSITION_DURATION_MAX, clip_duration * 0.4)
lower = TRANSITION_DURATION_MIN if jitter < 0 else max(TRANSITION_DURATION_MIN, base_duration)
return max(lower, min(upper, effective))
# ── 核心函数 ────────────────────────────────────────────────────────────────
def generate_transition_plan(
num_transitions: int,
*,
clip_durations: list[float] | None = None,
rng: random.Random | None = None,
) -> list[dict]:
"""为变体生成一组随机化的转场计划。
每个转场点独立随机选择类型和时长,硬切比例保持在 30%-50%。
Args:
num_transitions: 转场点数量(= 主片段数 - 1)
clip_durations: 各片段时长(用于协同节奏:长片段间转场更长),
长度应 >= num_transitions + 1;不足时用默认值
rng: 可选随机数生成器(测试可注入固定种子)
Returns:
转场计划列表,每项:
- effect: str — "cut" 或 TRANSITION_POOL 中某一效果
- duration: float — 转场时长(cut 为 0.0)
- jitter: float — 位置偏移量(秒,-0.5 ~ +0.5)
"""
if num_transitions <= 0:
return []
if rng is None:
rng = random.Random()
if clip_durations is None:
clip_durations = [5.0] * (num_transitions + 1)
plan: list[dict] = []
for i in range(num_transitions):
prev_dur = clip_durations[i] if i < len(clip_durations) else 5.0
next_dur = clip_durations[i + 1] if (i + 1) < len(clip_durations) else 5.0
# 计算硬切概率(协同节奏)
cut_prob = _cut_probability_for_pair(prev_dur, next_dur)
# 随机决定是否硬切
if rng.random() < cut_prob:
effect = HARD_CUT
duration = 0.0
else:
effect = rng.choice(TRANSITION_POOL)
base_dur = rng.uniform(TRANSITION_DURATION_MIN, TRANSITION_DURATION_MAX)
# 协同节奏:长片段间转场基础时长更长
avg_dur = (prev_dur + next_dur) / 2
if avg_dur > 6.0:
base_dur = min(TRANSITION_DURATION_MAX, base_dur * 1.15)
# 应用位置微调
jitter = rng.uniform(-TIMING_JITTER_MAX, TIMING_JITTER_MAX)
shorter_clip = min(prev_dur, next_dur)
duration = _apply_jitter(base_dur, jitter, shorter_clip)
jitter_val = rng.uniform(-TIMING_JITTER_MAX, TIMING_JITTER_MAX) if effect != HARD_CUT else 0.0
plan.append(
{
"effect": effect,
"duration": round(duration, 3),
"jitter": round(jitter_val, 3),
}
)
return plan