Files
xiaoxia-saas/packages/domain/transition_randomizer.py
xiaoxia 44224cfaf6
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 4s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
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 / PR Build API Image (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 10s
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
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 / 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 / Build Staging API Image (push) Successful in 28s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (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 / Build Staging Web Image (push) Successful in 23s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 29s
CI/CD Pipeline / CI Gate (pull_request) 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 / Canary Release to Production (pull_request) Has been skipped
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
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
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 / 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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
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