You've already forked FrameTour-RenderWorker
Compare commits
2 Commits
eeb21cada3
...
dd2d40c55b
| Author | SHA1 | Date | |
|---|---|---|---|
| dd2d40c55b | |||
| c57524f174 |
@@ -135,13 +135,36 @@ class RenderSegmentVideoHandler(BaseHandler):
|
||||
logger.warning(f"[task:{task.task_id}] Failed to download overlay, continuing without it")
|
||||
overlay_file = None
|
||||
|
||||
# 6. 计算 overlap 时长(用于转场帧冻结)
|
||||
# 6. 探测源视频时长(仅对视频素材)
|
||||
# 用于检测时长不足并通过冻结最后一帧补足
|
||||
source_duration_sec = None
|
||||
if not is_image:
|
||||
source_duration = self.probe_duration(input_file)
|
||||
if source_duration:
|
||||
source_duration_sec = source_duration
|
||||
speed = float(render_spec.speed) if render_spec.speed else 1.0
|
||||
if speed > 0:
|
||||
# 计算变速后的有效时长
|
||||
effective_duration_sec = source_duration_sec / speed
|
||||
required_duration_sec = duration_ms / 1000.0
|
||||
|
||||
# 如果源视频时长不足,记录日志
|
||||
if effective_duration_sec < required_duration_sec:
|
||||
shortage_sec = required_duration_sec - effective_duration_sec
|
||||
logger.warning(
|
||||
f"[task:{task.task_id}] Source video duration insufficient: "
|
||||
f"effective={effective_duration_sec:.2f}s (speed={speed}), "
|
||||
f"required={required_duration_sec:.2f}s, "
|
||||
f"will freeze last frame for {shortage_sec:.2f}s"
|
||||
)
|
||||
|
||||
# 7. 计算 overlap 时长(用于转场帧冻结)
|
||||
# 头部 overlap: 来自前一片段的出场转场
|
||||
overlap_head_ms = task.get_overlap_head_ms()
|
||||
# 尾部 overlap: 当前片段的出场转场
|
||||
overlap_tail_ms = task.get_overlap_tail_ms_v2()
|
||||
|
||||
# 7. 构建 FFmpeg 命令
|
||||
# 8. 构建 FFmpeg 命令
|
||||
output_file = os.path.join(work_dir, 'output.mp4')
|
||||
cmd = self._build_command(
|
||||
input_file=input_file,
|
||||
@@ -152,28 +175,29 @@ class RenderSegmentVideoHandler(BaseHandler):
|
||||
lut_file=lut_file,
|
||||
overlay_file=overlay_file,
|
||||
overlap_head_ms=overlap_head_ms,
|
||||
overlap_tail_ms=overlap_tail_ms
|
||||
overlap_tail_ms=overlap_tail_ms,
|
||||
source_duration_sec=source_duration_sec
|
||||
)
|
||||
|
||||
# 8. 执行 FFmpeg
|
||||
# 9. 执行 FFmpeg
|
||||
if not self.run_ffmpeg(cmd, task.task_id):
|
||||
return TaskResult.fail(
|
||||
ErrorCode.E_FFMPEG_FAILED,
|
||||
"FFmpeg rendering failed"
|
||||
)
|
||||
|
||||
# 9. 验证输出文件
|
||||
# 10. 验证输出文件
|
||||
if not self.ensure_file_exists(output_file, min_size=4096):
|
||||
return TaskResult.fail(
|
||||
ErrorCode.E_FFMPEG_FAILED,
|
||||
"Output file is missing or too small"
|
||||
)
|
||||
|
||||
# 10. 获取实际时长
|
||||
# 11. 获取实际时长
|
||||
actual_duration = self.probe_duration(output_file)
|
||||
actual_duration_ms = int(actual_duration * 1000) if actual_duration else duration_ms
|
||||
|
||||
# 11. 上传产物
|
||||
# 12. 上传产物
|
||||
video_url = self.upload_file(task.task_id, 'video', output_file)
|
||||
if not video_url:
|
||||
return TaskResult.fail(
|
||||
@@ -181,7 +205,7 @@ class RenderSegmentVideoHandler(BaseHandler):
|
||||
"Failed to upload video"
|
||||
)
|
||||
|
||||
# 12. 构建结果(包含 overlap 信息)
|
||||
# 13. 构建结果(包含 overlap 信息)
|
||||
result_data = {
|
||||
'videoUrl': video_url,
|
||||
'actualDurationMs': actual_duration_ms,
|
||||
@@ -310,7 +334,8 @@ class RenderSegmentVideoHandler(BaseHandler):
|
||||
lut_file: Optional[str] = None,
|
||||
overlay_file: Optional[str] = None,
|
||||
overlap_head_ms: int = 0,
|
||||
overlap_tail_ms: int = 0
|
||||
overlap_tail_ms: int = 0,
|
||||
source_duration_sec: Optional[float] = None
|
||||
) -> List[str]:
|
||||
"""
|
||||
构建 FFmpeg 渲染命令
|
||||
@@ -325,6 +350,7 @@ class RenderSegmentVideoHandler(BaseHandler):
|
||||
overlay_file: 叠加层文件路径(可选)
|
||||
overlap_head_ms: 头部 overlap 时长(毫秒)
|
||||
overlap_tail_ms: 尾部 overlap 时长(毫秒)
|
||||
source_duration_sec: 源视频实际时长(秒),用于检测时长不足
|
||||
|
||||
Returns:
|
||||
FFmpeg 命令参数列表
|
||||
@@ -347,10 +373,12 @@ class RenderSegmentVideoHandler(BaseHandler):
|
||||
filters = self._build_video_filters(
|
||||
render_spec=render_spec,
|
||||
output_spec=output_spec,
|
||||
duration_ms=duration_ms,
|
||||
lut_file=lut_file,
|
||||
overlay_file=overlay_file,
|
||||
overlap_head_ms=overlap_head_ms,
|
||||
overlap_tail_ms=overlap_tail_ms
|
||||
overlap_tail_ms=overlap_tail_ms,
|
||||
source_duration_sec=source_duration_sec
|
||||
)
|
||||
|
||||
# 应用滤镜
|
||||
@@ -403,10 +431,12 @@ class RenderSegmentVideoHandler(BaseHandler):
|
||||
self,
|
||||
render_spec: RenderSpec,
|
||||
output_spec: OutputSpec,
|
||||
duration_ms: int,
|
||||
lut_file: Optional[str] = None,
|
||||
overlay_file: Optional[str] = None,
|
||||
overlap_head_ms: int = 0,
|
||||
overlap_tail_ms: int = 0
|
||||
overlap_tail_ms: int = 0,
|
||||
source_duration_sec: Optional[float] = None
|
||||
) -> str:
|
||||
"""
|
||||
构建视频滤镜链
|
||||
@@ -414,10 +444,12 @@ class RenderSegmentVideoHandler(BaseHandler):
|
||||
Args:
|
||||
render_spec: 渲染规格
|
||||
output_spec: 输出规格
|
||||
duration_ms: 目标时长(毫秒)
|
||||
lut_file: LUT 文件路径
|
||||
overlay_file: 叠加层文件路径(支持图片 png/jpg 和视频 mov)
|
||||
overlap_head_ms: 头部 overlap 时长(毫秒)
|
||||
overlap_tail_ms: 尾部 overlap 时长(毫秒)
|
||||
source_duration_sec: 源视频实际时长(秒),用于检测时长不足
|
||||
|
||||
Returns:
|
||||
滤镜字符串
|
||||
@@ -496,20 +528,39 @@ class RenderSegmentVideoHandler(BaseHandler):
|
||||
is_video_overlay=is_video_overlay,
|
||||
overlap_head_ms=overlap_head_ms,
|
||||
overlap_tail_ms=overlap_tail_ms,
|
||||
use_hwdownload=bool(hwaccel_prefix)
|
||||
use_hwdownload=bool(hwaccel_prefix),
|
||||
duration_ms=duration_ms,
|
||||
render_spec=render_spec,
|
||||
source_duration_sec=source_duration_sec
|
||||
)
|
||||
|
||||
# 6. 帧冻结(tpad)- 用于转场 overlap 区域
|
||||
# 6. 帧冻结(tpad)- 用于转场 overlap 区域和时长不足补足
|
||||
# 注意:tpad 必须在缩放之后应用
|
||||
tpad_parts = []
|
||||
|
||||
# 计算是否需要额外的尾部冻结(源视频时长不足)
|
||||
extra_tail_freeze_sec = 0.0
|
||||
if source_duration_sec is not None:
|
||||
speed = float(render_spec.speed) if render_spec.speed else 1.0
|
||||
if speed > 0:
|
||||
# 计算变速后的有效时长
|
||||
effective_duration_sec = source_duration_sec / speed
|
||||
required_duration_sec = duration_ms / 1000.0
|
||||
|
||||
# 如果源视频时长不足,需要冻结最后一帧来补足
|
||||
if effective_duration_sec < required_duration_sec:
|
||||
extra_tail_freeze_sec = required_duration_sec - effective_duration_sec
|
||||
|
||||
if overlap_head_ms > 0:
|
||||
# 头部冻结:将第一帧冻结指定时长
|
||||
head_duration_sec = overlap_head_ms / 1000.0
|
||||
tpad_parts.append(f"start_mode=clone:start_duration={head_duration_sec}")
|
||||
if overlap_tail_ms > 0:
|
||||
# 尾部冻结:将最后一帧冻结指定时长
|
||||
tail_duration_sec = overlap_tail_ms / 1000.0
|
||||
tpad_parts.append(f"stop_mode=clone:stop_duration={tail_duration_sec}")
|
||||
|
||||
# 尾部冻结:合并 overlap 和时长不足的冻结
|
||||
total_tail_freeze_sec = (overlap_tail_ms / 1000.0) + extra_tail_freeze_sec
|
||||
if total_tail_freeze_sec > 0:
|
||||
# 将最后一帧冻结指定时长
|
||||
tpad_parts.append(f"stop_mode=clone:stop_duration={total_tail_freeze_sec}")
|
||||
|
||||
if tpad_parts:
|
||||
filters.append(f"tpad={':'.join(tpad_parts)}")
|
||||
@@ -539,7 +590,10 @@ class RenderSegmentVideoHandler(BaseHandler):
|
||||
is_video_overlay: bool = False,
|
||||
overlap_head_ms: int = 0,
|
||||
overlap_tail_ms: int = 0,
|
||||
use_hwdownload: bool = False
|
||||
use_hwdownload: bool = False,
|
||||
duration_ms: int = 0,
|
||||
render_spec: Optional[RenderSpec] = None,
|
||||
source_duration_sec: Optional[float] = None
|
||||
) -> str:
|
||||
"""
|
||||
构建包含特效的 filter_complex 滤镜图
|
||||
@@ -557,6 +611,9 @@ class RenderSegmentVideoHandler(BaseHandler):
|
||||
overlap_head_ms: 头部 overlap 时长
|
||||
overlap_tail_ms: 尾部 overlap 时长
|
||||
use_hwdownload: 是否使用了硬件加速解码(已在 base_filters 中包含 hwdownload)
|
||||
duration_ms: 目标时长(毫秒)
|
||||
render_spec: 渲染规格(用于获取变速参数)
|
||||
source_duration_sec: 源视频实际时长(秒),用于检测时长不足
|
||||
|
||||
Returns:
|
||||
filter_complex 格式的滤镜字符串
|
||||
@@ -615,14 +672,30 @@ class RenderSegmentVideoHandler(BaseHandler):
|
||||
current_output = effect_output
|
||||
effect_idx += 1
|
||||
|
||||
# 帧冻结(tpad)- 用于转场 overlap 区域
|
||||
# 帧冻结(tpad)- 用于转场 overlap 区域和时长不足补足
|
||||
tpad_parts = []
|
||||
|
||||
# 计算是否需要额外的尾部冻结(源视频时长不足)
|
||||
extra_tail_freeze_sec = 0.0
|
||||
if source_duration_sec is not None and render_spec is not None and duration_ms > 0:
|
||||
speed = float(render_spec.speed) if render_spec.speed else 1.0
|
||||
if speed > 0:
|
||||
# 计算变速后的有效时长
|
||||
effective_duration_sec = source_duration_sec / speed
|
||||
required_duration_sec = duration_ms / 1000.0
|
||||
|
||||
# 如果源视频时长不足,需要冻结最后一帧来补足
|
||||
if effective_duration_sec < required_duration_sec:
|
||||
extra_tail_freeze_sec = required_duration_sec - effective_duration_sec
|
||||
|
||||
if overlap_head_ms > 0:
|
||||
head_duration_sec = overlap_head_ms / 1000.0
|
||||
tpad_parts.append(f"start_mode=clone:start_duration={head_duration_sec}")
|
||||
if overlap_tail_ms > 0:
|
||||
tail_duration_sec = overlap_tail_ms / 1000.0
|
||||
tpad_parts.append(f"stop_mode=clone:stop_duration={tail_duration_sec}")
|
||||
|
||||
# 尾部冻结:合并 overlap 和时长不足的冻结
|
||||
total_tail_freeze_sec = (overlap_tail_ms / 1000.0) + extra_tail_freeze_sec
|
||||
if total_tail_freeze_sec > 0:
|
||||
tpad_parts.append(f"stop_mode=clone:stop_duration={total_tail_freeze_sec}")
|
||||
|
||||
if tpad_parts:
|
||||
tpad_output = '[v_tpad]'
|
||||
|
||||
56
index.py
56
index.py
@@ -25,6 +25,8 @@ import sys
|
||||
import time
|
||||
import signal
|
||||
import logging
|
||||
import os
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
@@ -34,11 +36,55 @@ from services.task_executor import TaskExecutor
|
||||
from constant import SOFTWARE_VERSION
|
||||
|
||||
# 日志配置
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
def setup_logging():
|
||||
"""配置日志系统,输出到控制台和文件"""
|
||||
# 日志格式
|
||||
log_format = '[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s'
|
||||
date_format = '%Y-%m-%d %H:%M:%S'
|
||||
formatter = logging.Formatter(log_format, date_format)
|
||||
|
||||
# 获取根logger
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(logging.INFO)
|
||||
|
||||
# 清除已有的handlers(避免重复)
|
||||
root_logger.handlers.clear()
|
||||
|
||||
# 1. 控制台handler(只输出WARNING及以上级别)
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(logging.WARNING)
|
||||
console_handler.setFormatter(formatter)
|
||||
root_logger.addHandler(console_handler)
|
||||
|
||||
# 确保日志文件所在目录存在
|
||||
log_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# 2. 所有日志文件handler(all_log.log)
|
||||
all_log_path = os.path.join(log_dir, 'all_log.log')
|
||||
all_log_handler = RotatingFileHandler(
|
||||
all_log_path,
|
||||
maxBytes=10*1024*1024, # 10MB
|
||||
backupCount=5,
|
||||
encoding='utf-8'
|
||||
)
|
||||
all_log_handler.setLevel(logging.DEBUG) # 记录所有级别
|
||||
all_log_handler.setFormatter(formatter)
|
||||
root_logger.addHandler(all_log_handler)
|
||||
|
||||
# 3. 错误日志文件handler(error.log)
|
||||
error_log_path = os.path.join(log_dir, 'error.log')
|
||||
error_log_handler = RotatingFileHandler(
|
||||
error_log_path,
|
||||
maxBytes=10*1024*1024, # 10MB
|
||||
backupCount=5,
|
||||
encoding='utf-8'
|
||||
)
|
||||
error_log_handler.setLevel(logging.ERROR) # 只记录ERROR及以上
|
||||
error_log_handler.setFormatter(formatter)
|
||||
root_logger.addHandler(error_log_handler)
|
||||
|
||||
# 初始化日志系统
|
||||
setup_logging()
|
||||
logger = logging.getLogger('worker')
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user