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
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
178 lines
6.9 KiB
Python
178 lines
6.9 KiB
Python
"""转场位置与类型随机化(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
|