style: auto-format with black + isort + prettier [skip ci-format-check]
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 4s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 18s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 20s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m23s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Style (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m23s
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 4s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 18s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 20s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m23s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Style (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m23s
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
This commit is contained in:
@@ -1872,24 +1872,24 @@ class UnifiedRenderService:
|
||||
# 有转场效果:用 TransitionEngine 构建 xfade 链
|
||||
# #1766: 提取逐转场时长(每个 clip 的 transition_duration,跳过第一个)
|
||||
# layer_transition_durations[i] 对应 clip i 的转场,第 0 个忽略
|
||||
per_transition_durations = [
|
||||
d for idx, d in enumerate(layer_transition_durations) if idx > 0
|
||||
]
|
||||
per_transition_durations = [d for idx, d in enumerate(layer_transition_durations) if idx > 0]
|
||||
# #1766: 提取每个 clip 的 jitter(存在 config 中),跳过第一个
|
||||
layer_jitters = [
|
||||
(all_clips[layer_clip_indices[idx]].config.get("transition_jitter", 0.0)
|
||||
if isinstance(all_clips[layer_clip_indices[idx]].config, dict)
|
||||
else 0.0)
|
||||
(
|
||||
all_clips[layer_clip_indices[idx]].config.get("transition_jitter", 0.0)
|
||||
if isinstance(all_clips[layer_clip_indices[idx]].config, dict)
|
||||
else 0.0
|
||||
)
|
||||
for idx in range(len(layer_clip_indices))
|
||||
]
|
||||
per_transition_jitters = [
|
||||
j for idx, j in enumerate(layer_jitters) if idx > 0
|
||||
]
|
||||
per_transition_jitters = [j for idx, j in enumerate(layer_jitters) if idx > 0]
|
||||
xfade_filter, xfade_estimated_dur = self._transition_engine.build_xfade_chain(
|
||||
clip_durations=layer_durations,
|
||||
clip_video_labels=layer_labels,
|
||||
transitions=layer_transitions,
|
||||
transition_durations=per_transition_durations if any(d > 0 for d in per_transition_durations) else None,
|
||||
transition_durations=(
|
||||
per_transition_durations if any(d > 0 for d in per_transition_durations) else None
|
||||
),
|
||||
jitters=per_transition_jitters if any(j != 0.0 for j in per_transition_jitters) else None,
|
||||
output_label=out_label,
|
||||
)
|
||||
|
||||
@@ -24,11 +24,11 @@ logger = logging.getLogger(__name__)
|
||||
#: 非硬切转场类型池(5 种效果,均为 FFmpeg xfade 支持的 transition 名称)
|
||||
#: 选择标准:视觉效果差异大、FFmpeg 渲染稳定、肉眼可区分
|
||||
TRANSITION_POOL: list[str] = [
|
||||
"dissolve", # 溶解
|
||||
"zoomin", # 缩放(放大进入)
|
||||
"slideleft", # 左滑入
|
||||
"wipeleft", # 左擦除
|
||||
"fade", # 淡入淡出
|
||||
"dissolve", # 溶解
|
||||
"zoomin", # 缩放(放大进入)
|
||||
"slideleft", # 左滑入
|
||||
"wipeleft", # 左擦除
|
||||
"fade", # 淡入淡出
|
||||
]
|
||||
|
||||
#: 硬切(无转场效果),由 build_xfade_filter_chain 特殊处理(concat filter)
|
||||
@@ -164,16 +164,14 @@ def generate_transition_plan(
|
||||
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
|
||||
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),
|
||||
}
|
||||
)
|
||||
|
||||
plan.append({
|
||||
"effect": effect,
|
||||
"duration": round(duration, 3),
|
||||
"jitter": round(jitter_val, 3),
|
||||
})
|
||||
|
||||
return plan
|
||||
|
||||
@@ -396,10 +396,7 @@ def _apply_transition_randomization(
|
||||
硬切比例保证在 30%-50%。intro/outro 等非 main 片段的转场保持源值不变。
|
||||
"""
|
||||
# 收集 main 片段的索引(按 order 排序)
|
||||
main_indices = [
|
||||
i for i, c in enumerate(result)
|
||||
if c is not None and c.get("clip_type", "main") == "main"
|
||||
]
|
||||
main_indices = [i for i, c in enumerate(result) if c is not None and c.get("clip_type", "main") == "main"]
|
||||
|
||||
if len(main_indices) < 2:
|
||||
# 不足 2 个 main 片段,无转场点可随机化
|
||||
|
||||
@@ -135,18 +135,16 @@ class TestGenerateTransitionPlan:
|
||||
# 统计波动允许 ±10% 的宽松范围
|
||||
assert 0.20 <= ratio <= 0.60, f"硬切比例 {ratio:.2%} 超出宽松范围"
|
||||
# 更严格的范围检查(±5%)
|
||||
assert CUT_RATIO_MIN - 0.05 <= ratio <= CUT_RATIO_MAX + 0.05, (
|
||||
f"硬切比例 {ratio:.2%} 超出 [25%, 55%] 范围"
|
||||
)
|
||||
assert CUT_RATIO_MIN - 0.05 <= ratio <= CUT_RATIO_MAX + 0.05, f"硬切比例 {ratio:.2%} 超出 [25%, 55%] 范围"
|
||||
|
||||
def test_transition_duration_in_range(self):
|
||||
"""非硬切转场的时长在 [0.3, 0.8] 范围内。"""
|
||||
plan = generate_transition_plan(30, rng=random.Random(42))
|
||||
for item in plan:
|
||||
if item["effect"] != HARD_CUT:
|
||||
assert TRANSITION_DURATION_MIN <= item["duration"] <= TRANSITION_DURATION_MAX, (
|
||||
f"转场时长 {item['duration']} 超出 [{TRANSITION_DURATION_MIN}, {TRANSITION_DURATION_MAX}]"
|
||||
)
|
||||
assert (
|
||||
TRANSITION_DURATION_MIN <= item["duration"] <= TRANSITION_DURATION_MAX
|
||||
), f"转场时长 {item['duration']} 超出 [{TRANSITION_DURATION_MIN}, {TRANSITION_DURATION_MAX}]"
|
||||
|
||||
def test_cut_duration_is_zero(self):
|
||||
"""硬切转场的时长必须为 0。"""
|
||||
@@ -159,9 +157,7 @@ class TestGenerateTransitionPlan:
|
||||
"""jitter 在 [-0.5, +0.5] 范围内。"""
|
||||
plan = generate_transition_plan(30, rng=random.Random(42))
|
||||
for item in plan:
|
||||
assert -TIMING_JITTER_MAX <= item["jitter"] <= TIMING_JITTER_MAX, (
|
||||
f"jitter {item['jitter']} 超出范围"
|
||||
)
|
||||
assert -TIMING_JITTER_MAX <= item["jitter"] <= TIMING_JITTER_MAX, f"jitter {item['jitter']} 超出范围"
|
||||
|
||||
def test_cut_jitter_is_zero(self):
|
||||
"""硬切转场的 jitter 必须为 0。"""
|
||||
@@ -175,10 +171,7 @@ class TestGenerateTransitionPlan:
|
||||
plans_seen = set()
|
||||
for seed in range(20):
|
||||
plan = generate_transition_plan(5, rng=random.Random(seed))
|
||||
plan_sig = tuple(
|
||||
(item["effect"], item["duration"])
|
||||
for item in plan
|
||||
)
|
||||
plan_sig = tuple((item["effect"], item["duration"]) for item in plan)
|
||||
plans_seen.add(plan_sig)
|
||||
assert len(plans_seen) >= 2, "20 个 seed 只产生 1 种转场序列"
|
||||
|
||||
@@ -210,9 +203,7 @@ class TestGenerateTransitionPlan:
|
||||
avg_long = sum(long_durs) / len(long_durs)
|
||||
avg_short = sum(short_durs) / len(short_durs)
|
||||
# 长片段平均转场时长 >= 短片段(协同节奏)
|
||||
assert avg_long >= avg_short * 0.95, (
|
||||
f"长片段转场 {avg_long:.3f}s 不应显著短于短片段 {avg_short:.3f}s"
|
||||
)
|
||||
assert avg_long >= avg_short * 0.95, f"长片段转场 {avg_long:.3f}s 不应显著短于短片段 {avg_short:.3f}s"
|
||||
|
||||
def test_clip_durations_none_uses_default(self):
|
||||
"""clip_durations=None 时使用默认值 5.0。"""
|
||||
@@ -269,9 +260,7 @@ class TestTransitionRandomizationIntegration:
|
||||
)
|
||||
transition_seqs.add(seq)
|
||||
|
||||
assert len(transition_seqs) >= 2, (
|
||||
f"5 个 seed 只产生 {len(transition_seqs)} 种转场序列"
|
||||
)
|
||||
assert len(transition_seqs) >= 2, f"5 个 seed 只产生 {len(transition_seqs)} 种转场序列"
|
||||
|
||||
def test_reselect_preserves_non_main_transitions(self):
|
||||
"""非 main 片段(intro/outro)的转场不被随机化。"""
|
||||
|
||||
@@ -150,6 +150,7 @@ class TestJitters:
|
||||
)
|
||||
# 提取 offset 值
|
||||
import re
|
||||
|
||||
offset_no = float(re.search(r"offset=([\d.]+)", result_no_jitter).group(1))
|
||||
offset_yes = float(re.search(r"offset=([\d.]+)", result_with_jitter).group(1))
|
||||
assert offset_yes > offset_no
|
||||
@@ -170,6 +171,7 @@ class TestJitters:
|
||||
jitters=[-0.3], # 负 jitter:提前转场
|
||||
)
|
||||
import re
|
||||
|
||||
offset_no = float(re.search(r"offset=([\d.]+)", result_no_jitter).group(1))
|
||||
offset_yes = float(re.search(r"offset=([\d.]+)", result_with_jitter).group(1))
|
||||
assert offset_yes < offset_no
|
||||
@@ -184,6 +186,7 @@ class TestJitters:
|
||||
jitters=[-100.0], # 极大负 jitter
|
||||
)
|
||||
import re
|
||||
|
||||
offset = float(re.search(r"offset=([\d.]+)", result).group(1))
|
||||
assert offset >= 0.0
|
||||
|
||||
@@ -197,6 +200,7 @@ class TestJitters:
|
||||
jitters=[0.2, -0.1],
|
||||
)
|
||||
import re
|
||||
|
||||
offsets = [float(m.group(1)) for m in re.finditer(r"offset=([\d.]+)", result)]
|
||||
assert len(offsets) == 2
|
||||
|
||||
|
||||
Reference in New Issue
Block a user