242 lines
7.2 KiB
Python
Executable File
242 lines
7.2 KiB
Python
Executable File
"""渲染图层工具函数 — 纯函数集合.
|
||
|
||
从 unified_render_service.py 抽离的纯逻辑,负责:
|
||
- clip 时长计算(有效时长、调速后时长)
|
||
- clip_type → layer_role 映射
|
||
- 总时长估算
|
||
- 图层默认属性(z_index 等)
|
||
|
||
所有函数均为纯函数,不依赖 FFmpeg、数据库或外部 IO。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import Any
|
||
|
||
# ── 图层角色定义 ─────────────────────────────────────────────────────────────
|
||
|
||
# 图层默认 z_index 映射
|
||
LAYER_Z_INDEX: dict[str, int] = {
|
||
"background": -1,
|
||
"broll": 0,
|
||
"main": 0,
|
||
"overlay": 1,
|
||
"corner_voice": 1,
|
||
"audio": 2,
|
||
}
|
||
|
||
# 图层默认 PiP 缩放比例(相对于主画面)
|
||
PIP_DEFAULT_SCALE = 0.25
|
||
|
||
# 主视频图层角色(用于总时长计算、直通判断等)
|
||
MAIN_LAYER_ROLES = frozenset({"main", "broll", "background"})
|
||
|
||
|
||
# ── clip_type → layer_role 映射 ──────────────────────────────────────────────
|
||
|
||
|
||
def resolve_layer_role(clip_type: str, config: dict[str, Any] | None = None) -> str:
|
||
"""根据 clip_type 和 config.role 确定图层角色。
|
||
|
||
映射规则:
|
||
intro / outro → "main"(按 order 排在首/尾)
|
||
overlay → "overlay"(画中画叠加,z=1)
|
||
corner_voice → "corner_voice"(右上角小窗,z=1)
|
||
background → "background"(全屏底图,z=0)
|
||
b_roll → "broll"(z=0)
|
||
main + config.role=b_roll → "broll"
|
||
main + config.role=audio → "audio"
|
||
main (default) → "main"
|
||
|
||
Args:
|
||
clip_type: 片段类型字符串
|
||
config: 片段配置字典(可选)
|
||
|
||
Returns:
|
||
图层角色字符串
|
||
"""
|
||
role = (config or {}).get("role", "") if config else ""
|
||
|
||
if clip_type in ("intro", "outro"):
|
||
return "main"
|
||
if clip_type == "overlay":
|
||
return "overlay"
|
||
if clip_type == "corner_voice":
|
||
return "corner_voice"
|
||
if clip_type == "background":
|
||
return "background"
|
||
if clip_type == "b_roll":
|
||
return "broll"
|
||
# main type
|
||
if role == "b_roll":
|
||
return "broll"
|
||
if role == "audio":
|
||
return "audio"
|
||
return "main"
|
||
|
||
|
||
def get_layer_z_index(role: str) -> int:
|
||
"""获取图层角色的默认 z_index。
|
||
|
||
Args:
|
||
role: 图层角色
|
||
|
||
Returns:
|
||
z_index 值,未知角色返回 0
|
||
"""
|
||
return LAYER_Z_INDEX.get(role, 0)
|
||
|
||
|
||
# ── clip 时长计算 ──────────────────────────────────────────────────────────
|
||
|
||
|
||
def clip_effective_duration(
|
||
duration: float,
|
||
actual_duration: float = 0.0,
|
||
) -> float:
|
||
"""计算 clip 的有效时长(原速 trim 后时长)。
|
||
|
||
规则:
|
||
- duration > 0: min(duration, actual_duration),actual=0 时用 duration
|
||
- duration <= 0: actual_duration,actual=0 时返回 0
|
||
|
||
Args:
|
||
duration: 配置的时长(0 表示使用完整素材)
|
||
actual_duration: 素材实际时长(probe 后的结果)
|
||
|
||
Returns:
|
||
有效时长(秒)
|
||
"""
|
||
if duration > 0:
|
||
return min(duration, actual_duration) if actual_duration > 0 else duration
|
||
return actual_duration if actual_duration > 0 else 0.0
|
||
|
||
|
||
def clip_playback_speed(playback_speed: Any) -> float:
|
||
"""获取 clip 的播放速度,无效值回退到 1.0。
|
||
|
||
Args:
|
||
playback_speed: 播放速度(可为任意类型
|
||
|
||
Returns:
|
||
有效的播放速度(正数)
|
||
"""
|
||
if not isinstance(playback_speed, (int, float)):
|
||
return 1.0
|
||
if playback_speed <= 0:
|
||
return 1.0
|
||
return float(playback_speed)
|
||
|
||
|
||
def clip_adjusted_duration(
|
||
duration: float,
|
||
actual_duration: float = 0.0,
|
||
playback_speed: Any = 1.0,
|
||
) -> float:
|
||
"""计算调速后的 clip 实际时长(用于拼接计算)。
|
||
|
||
Args:
|
||
duration: 配置的时长
|
||
actual_duration: 素材实际时长
|
||
playback_speed: 播放速度
|
||
|
||
Returns:
|
||
调速后的时长
|
||
"""
|
||
base = clip_effective_duration(duration, actual_duration)
|
||
speed = clip_playback_speed(playback_speed)
|
||
if abs(speed - 1.0) < 1e-6:
|
||
return base
|
||
return base / speed
|
||
|
||
|
||
# ── 总时长估算 ────────────────────────────────────────────────────────────
|
||
|
||
|
||
def estimate_total_duration(
|
||
layers: list[Any],
|
||
transition_duration: float = 0.0,
|
||
) -> float:
|
||
"""估算视频总时长。
|
||
|
||
取主图层(main/broll/background)的总调整后时长,减去转场重叠时间。
|
||
|
||
Args:
|
||
layers: 图层列表(每个元素需有 role 和 clips 属性,
|
||
clips 中元素需有 duration/actual_duration/playback_speed 属性)
|
||
transition_duration: 转场时长(秒),用于估算重叠时间
|
||
|
||
Returns:
|
||
估算的总时长(秒),最小 0.1
|
||
"""
|
||
# 找主图层(第一个有视频内容的图层)
|
||
main_layer = None
|
||
for role in ("main", "broll", "background"):
|
||
for layer in layers:
|
||
if getattr(layer, "role", None) == role and getattr(layer, "clips", None):
|
||
main_layer = layer
|
||
break
|
||
if main_layer:
|
||
break
|
||
|
||
if not main_layer or not getattr(main_layer, "clips", None):
|
||
return 0.0
|
||
|
||
clips = getattr(main_layer, "clips", [])
|
||
total = sum(
|
||
clip_adjusted_duration(
|
||
duration=getattr(c, "duration", 0),
|
||
actual_duration=getattr(c, "actual_duration", 0.0),
|
||
playback_speed=getattr(c, "playback_speed", 1.0),
|
||
)
|
||
for c in clips
|
||
)
|
||
|
||
# 减去转场重叠时间(粗略估算)
|
||
n_clips = len(clips)
|
||
if n_clips > 1 and transition_duration > 0:
|
||
total -= (n_clips - 1) * transition_duration
|
||
|
||
return max(0.1, total)
|
||
|
||
|
||
# ── 直通 / Stream Copy 判断辅助 ──────────────────────────────────────────────
|
||
|
||
|
||
def can_pass_through(
|
||
layers: list[Any],
|
||
has_stickers: bool = False,
|
||
has_watermark: bool = False,
|
||
) -> bool:
|
||
"""判断是否可以走直通优化路径(单 clip 简单场景)。
|
||
|
||
条件:
|
||
1. 只有 1 个图层
|
||
2. 该图层是视频图层(main/broll/background)
|
||
3. 该图层只有 1 个 clip(无转场需求)
|
||
4. 没有贴纸
|
||
5. 没有水印
|
||
|
||
Args:
|
||
layers: 图层列表
|
||
has_stickers: 是否有贴纸
|
||
has_watermark: 是否有水印
|
||
|
||
Returns:
|
||
是否可以走直通
|
||
"""
|
||
if len(layers) != 1:
|
||
return False
|
||
layer = layers[0]
|
||
role = getattr(layer, "role", "")
|
||
if role not in MAIN_LAYER_ROLES:
|
||
return False
|
||
clips = getattr(layer, "clips", [])
|
||
if len(clips) != 1:
|
||
return False
|
||
if has_stickers:
|
||
return False
|
||
if has_watermark:
|
||
return False
|
||
return True
|