"""转场位置与类型随机化(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