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")
|
logger.warning(f"[task:{task.task_id}] Failed to download overlay, continuing without it")
|
||||||
overlay_file = None
|
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: 来自前一片段的出场转场
|
||||||
overlap_head_ms = task.get_overlap_head_ms()
|
overlap_head_ms = task.get_overlap_head_ms()
|
||||||
# 尾部 overlap: 当前片段的出场转场
|
# 尾部 overlap: 当前片段的出场转场
|
||||||
overlap_tail_ms = task.get_overlap_tail_ms_v2()
|
overlap_tail_ms = task.get_overlap_tail_ms_v2()
|
||||||
|
|
||||||
# 7. 构建 FFmpeg 命令
|
# 8. 构建 FFmpeg 命令
|
||||||
output_file = os.path.join(work_dir, 'output.mp4')
|
output_file = os.path.join(work_dir, 'output.mp4')
|
||||||
cmd = self._build_command(
|
cmd = self._build_command(
|
||||||
input_file=input_file,
|
input_file=input_file,
|
||||||
@@ -152,28 +175,29 @@ class RenderSegmentVideoHandler(BaseHandler):
|
|||||||
lut_file=lut_file,
|
lut_file=lut_file,
|
||||||
overlay_file=overlay_file,
|
overlay_file=overlay_file,
|
||||||
overlap_head_ms=overlap_head_ms,
|
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):
|
if not self.run_ffmpeg(cmd, task.task_id):
|
||||||
return TaskResult.fail(
|
return TaskResult.fail(
|
||||||
ErrorCode.E_FFMPEG_FAILED,
|
ErrorCode.E_FFMPEG_FAILED,
|
||||||
"FFmpeg rendering failed"
|
"FFmpeg rendering failed"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 9. 验证输出文件
|
# 10. 验证输出文件
|
||||||
if not self.ensure_file_exists(output_file, min_size=4096):
|
if not self.ensure_file_exists(output_file, min_size=4096):
|
||||||
return TaskResult.fail(
|
return TaskResult.fail(
|
||||||
ErrorCode.E_FFMPEG_FAILED,
|
ErrorCode.E_FFMPEG_FAILED,
|
||||||
"Output file is missing or too small"
|
"Output file is missing or too small"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 10. 获取实际时长
|
# 11. 获取实际时长
|
||||||
actual_duration = self.probe_duration(output_file)
|
actual_duration = self.probe_duration(output_file)
|
||||||
actual_duration_ms = int(actual_duration * 1000) if actual_duration else duration_ms
|
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)
|
video_url = self.upload_file(task.task_id, 'video', output_file)
|
||||||
if not video_url:
|
if not video_url:
|
||||||
return TaskResult.fail(
|
return TaskResult.fail(
|
||||||
@@ -181,7 +205,7 @@ class RenderSegmentVideoHandler(BaseHandler):
|
|||||||
"Failed to upload video"
|
"Failed to upload video"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 12. 构建结果(包含 overlap 信息)
|
# 13. 构建结果(包含 overlap 信息)
|
||||||
result_data = {
|
result_data = {
|
||||||
'videoUrl': video_url,
|
'videoUrl': video_url,
|
||||||
'actualDurationMs': actual_duration_ms,
|
'actualDurationMs': actual_duration_ms,
|
||||||
@@ -310,7 +334,8 @@ class RenderSegmentVideoHandler(BaseHandler):
|
|||||||
lut_file: Optional[str] = None,
|
lut_file: Optional[str] = None,
|
||||||
overlay_file: Optional[str] = None,
|
overlay_file: Optional[str] = None,
|
||||||
overlap_head_ms: int = 0,
|
overlap_head_ms: int = 0,
|
||||||
overlap_tail_ms: int = 0
|
overlap_tail_ms: int = 0,
|
||||||
|
source_duration_sec: Optional[float] = None
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
"""
|
"""
|
||||||
构建 FFmpeg 渲染命令
|
构建 FFmpeg 渲染命令
|
||||||
@@ -325,6 +350,7 @@ class RenderSegmentVideoHandler(BaseHandler):
|
|||||||
overlay_file: 叠加层文件路径(可选)
|
overlay_file: 叠加层文件路径(可选)
|
||||||
overlap_head_ms: 头部 overlap 时长(毫秒)
|
overlap_head_ms: 头部 overlap 时长(毫秒)
|
||||||
overlap_tail_ms: 尾部 overlap 时长(毫秒)
|
overlap_tail_ms: 尾部 overlap 时长(毫秒)
|
||||||
|
source_duration_sec: 源视频实际时长(秒),用于检测时长不足
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
FFmpeg 命令参数列表
|
FFmpeg 命令参数列表
|
||||||
@@ -347,10 +373,12 @@ class RenderSegmentVideoHandler(BaseHandler):
|
|||||||
filters = self._build_video_filters(
|
filters = self._build_video_filters(
|
||||||
render_spec=render_spec,
|
render_spec=render_spec,
|
||||||
output_spec=output_spec,
|
output_spec=output_spec,
|
||||||
|
duration_ms=duration_ms,
|
||||||
lut_file=lut_file,
|
lut_file=lut_file,
|
||||||
overlay_file=overlay_file,
|
overlay_file=overlay_file,
|
||||||
overlap_head_ms=overlap_head_ms,
|
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,
|
self,
|
||||||
render_spec: RenderSpec,
|
render_spec: RenderSpec,
|
||||||
output_spec: OutputSpec,
|
output_spec: OutputSpec,
|
||||||
|
duration_ms: int,
|
||||||
lut_file: Optional[str] = None,
|
lut_file: Optional[str] = None,
|
||||||
overlay_file: Optional[str] = None,
|
overlay_file: Optional[str] = None,
|
||||||
overlap_head_ms: int = 0,
|
overlap_head_ms: int = 0,
|
||||||
overlap_tail_ms: int = 0
|
overlap_tail_ms: int = 0,
|
||||||
|
source_duration_sec: Optional[float] = None
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
构建视频滤镜链
|
构建视频滤镜链
|
||||||
@@ -414,10 +444,12 @@ class RenderSegmentVideoHandler(BaseHandler):
|
|||||||
Args:
|
Args:
|
||||||
render_spec: 渲染规格
|
render_spec: 渲染规格
|
||||||
output_spec: 输出规格
|
output_spec: 输出规格
|
||||||
|
duration_ms: 目标时长(毫秒)
|
||||||
lut_file: LUT 文件路径
|
lut_file: LUT 文件路径
|
||||||
overlay_file: 叠加层文件路径(支持图片 png/jpg 和视频 mov)
|
overlay_file: 叠加层文件路径(支持图片 png/jpg 和视频 mov)
|
||||||
overlap_head_ms: 头部 overlap 时长(毫秒)
|
overlap_head_ms: 头部 overlap 时长(毫秒)
|
||||||
overlap_tail_ms: 尾部 overlap 时长(毫秒)
|
overlap_tail_ms: 尾部 overlap 时长(毫秒)
|
||||||
|
source_duration_sec: 源视频实际时长(秒),用于检测时长不足
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
滤镜字符串
|
滤镜字符串
|
||||||
@@ -496,20 +528,39 @@ class RenderSegmentVideoHandler(BaseHandler):
|
|||||||
is_video_overlay=is_video_overlay,
|
is_video_overlay=is_video_overlay,
|
||||||
overlap_head_ms=overlap_head_ms,
|
overlap_head_ms=overlap_head_ms,
|
||||||
overlap_tail_ms=overlap_tail_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 必须在缩放之后应用
|
||||||
tpad_parts = []
|
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:
|
if overlap_head_ms > 0:
|
||||||
# 头部冻结:将第一帧冻结指定时长
|
# 头部冻结:将第一帧冻结指定时长
|
||||||
head_duration_sec = overlap_head_ms / 1000.0
|
head_duration_sec = overlap_head_ms / 1000.0
|
||||||
tpad_parts.append(f"start_mode=clone:start_duration={head_duration_sec}")
|
tpad_parts.append(f"start_mode=clone:start_duration={head_duration_sec}")
|
||||||
if overlap_tail_ms > 0:
|
|
||||||
# 尾部冻结:将最后一帧冻结指定时长
|
# 尾部冻结:合并 overlap 和时长不足的冻结
|
||||||
tail_duration_sec = overlap_tail_ms / 1000.0
|
total_tail_freeze_sec = (overlap_tail_ms / 1000.0) + extra_tail_freeze_sec
|
||||||
tpad_parts.append(f"stop_mode=clone:stop_duration={tail_duration_sec}")
|
if total_tail_freeze_sec > 0:
|
||||||
|
# 将最后一帧冻结指定时长
|
||||||
|
tpad_parts.append(f"stop_mode=clone:stop_duration={total_tail_freeze_sec}")
|
||||||
|
|
||||||
if tpad_parts:
|
if tpad_parts:
|
||||||
filters.append(f"tpad={':'.join(tpad_parts)}")
|
filters.append(f"tpad={':'.join(tpad_parts)}")
|
||||||
@@ -539,7 +590,10 @@ class RenderSegmentVideoHandler(BaseHandler):
|
|||||||
is_video_overlay: bool = False,
|
is_video_overlay: bool = False,
|
||||||
overlap_head_ms: int = 0,
|
overlap_head_ms: int = 0,
|
||||||
overlap_tail_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:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
构建包含特效的 filter_complex 滤镜图
|
构建包含特效的 filter_complex 滤镜图
|
||||||
@@ -557,6 +611,9 @@ class RenderSegmentVideoHandler(BaseHandler):
|
|||||||
overlap_head_ms: 头部 overlap 时长
|
overlap_head_ms: 头部 overlap 时长
|
||||||
overlap_tail_ms: 尾部 overlap 时长
|
overlap_tail_ms: 尾部 overlap 时长
|
||||||
use_hwdownload: 是否使用了硬件加速解码(已在 base_filters 中包含 hwdownload)
|
use_hwdownload: 是否使用了硬件加速解码(已在 base_filters 中包含 hwdownload)
|
||||||
|
duration_ms: 目标时长(毫秒)
|
||||||
|
render_spec: 渲染规格(用于获取变速参数)
|
||||||
|
source_duration_sec: 源视频实际时长(秒),用于检测时长不足
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
filter_complex 格式的滤镜字符串
|
filter_complex 格式的滤镜字符串
|
||||||
@@ -615,14 +672,30 @@ class RenderSegmentVideoHandler(BaseHandler):
|
|||||||
current_output = effect_output
|
current_output = effect_output
|
||||||
effect_idx += 1
|
effect_idx += 1
|
||||||
|
|
||||||
# 帧冻结(tpad)- 用于转场 overlap 区域
|
# 帧冻结(tpad)- 用于转场 overlap 区域和时长不足补足
|
||||||
tpad_parts = []
|
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:
|
if overlap_head_ms > 0:
|
||||||
head_duration_sec = overlap_head_ms / 1000.0
|
head_duration_sec = overlap_head_ms / 1000.0
|
||||||
tpad_parts.append(f"start_mode=clone:start_duration={head_duration_sec}")
|
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
|
# 尾部冻结:合并 overlap 和时长不足的冻结
|
||||||
tpad_parts.append(f"stop_mode=clone:stop_duration={tail_duration_sec}")
|
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:
|
if tpad_parts:
|
||||||
tpad_output = '[v_tpad]'
|
tpad_output = '[v_tpad]'
|
||||||
|
|||||||
54
index.py
54
index.py
@@ -25,6 +25,8 @@ import sys
|
|||||||
import time
|
import time
|
||||||
import signal
|
import signal
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
from logging.handlers import RotatingFileHandler
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
@@ -34,11 +36,55 @@ from services.task_executor import TaskExecutor
|
|||||||
from constant import SOFTWARE_VERSION
|
from constant import SOFTWARE_VERSION
|
||||||
|
|
||||||
# 日志配置
|
# 日志配置
|
||||||
logging.basicConfig(
|
def setup_logging():
|
||||||
level=logging.INFO,
|
"""配置日志系统,输出到控制台和文件"""
|
||||||
format='[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s',
|
# 日志格式
|
||||||
datefmt='%Y-%m-%d %H:%M:%S'
|
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')
|
logger = logging.getLogger('worker')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user