You've already forked FrameTour-RenderWorker
feat(重构): 实现新的渲染服务架构
- 新增 RenderTask
This commit is contained in:
289
services/task_service.py
Normal file
289
services/task_service.py
Normal file
@@ -0,0 +1,289 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from abc import ABC, abstractmethod
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from typing import Dict, Any, List, Optional
|
||||
|
||||
from opentelemetry.trace import Status, StatusCode
|
||||
|
||||
from entity.render_task import RenderTask
|
||||
from services.render_service import RenderService
|
||||
from services.template_service import TemplateService
|
||||
from util.exceptions import TaskError, TaskValidationError
|
||||
from util import api, oss
|
||||
from telemetry import get_tracer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class TaskService(ABC):
|
||||
"""任务服务抽象接口"""
|
||||
|
||||
@abstractmethod
|
||||
def process_task(self, task_info: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
处理任务
|
||||
|
||||
Args:
|
||||
task_info: 任务信息
|
||||
|
||||
Returns:
|
||||
bool: 处理是否成功
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def create_render_task(self, task_info: Dict[str, Any], template_info: Dict[str, Any]) -> RenderTask:
|
||||
"""
|
||||
创建渲染任务
|
||||
|
||||
Args:
|
||||
task_info: 任务信息
|
||||
template_info: 模板信息
|
||||
|
||||
Returns:
|
||||
RenderTask: 渲染任务对象
|
||||
"""
|
||||
pass
|
||||
|
||||
class DefaultTaskService(TaskService):
|
||||
"""默认任务服务实现"""
|
||||
|
||||
def __init__(self, render_service: RenderService, template_service: TemplateService):
|
||||
self.render_service = render_service
|
||||
self.template_service = template_service
|
||||
|
||||
def process_task(self, task_info: Dict[str, Any]) -> bool:
|
||||
"""处理任务"""
|
||||
tracer = get_tracer(__name__)
|
||||
with tracer.start_as_current_span("process_task") as span:
|
||||
try:
|
||||
# 标准化任务信息
|
||||
task_info = api.normalize_task(task_info)
|
||||
span.set_attribute("task.id", task_info.get("id", "unknown"))
|
||||
span.set_attribute("task.template_id", task_info.get("templateId", "unknown"))
|
||||
|
||||
# 获取模板信息
|
||||
template_id = task_info.get("templateId")
|
||||
template_info = self.template_service.get_template(template_id)
|
||||
if not template_info:
|
||||
raise TaskError(f"Template not found: {template_id}")
|
||||
|
||||
# 报告任务开始
|
||||
api.report_task_start(task_info)
|
||||
|
||||
# 创建渲染任务
|
||||
render_task = self.create_render_task(task_info, template_info)
|
||||
|
||||
# 执行渲染
|
||||
success = self.render_service.render(render_task)
|
||||
if not success:
|
||||
span.set_status(Status(StatusCode.ERROR))
|
||||
api.report_task_failed(task_info, "Render failed")
|
||||
return False
|
||||
|
||||
# 获取视频信息
|
||||
width, height, duration = self.render_service.get_video_info(render_task.output_file)
|
||||
span.set_attribute("video.width", width)
|
||||
span.set_attribute("video.height", height)
|
||||
span.set_attribute("video.duration", duration)
|
||||
|
||||
# 音频淡出
|
||||
new_file = self.render_service.fade_out_audio(render_task.output_file, duration)
|
||||
render_task.output_file = new_file
|
||||
|
||||
# 上传文件 - 创建一个兼容对象
|
||||
class TaskCompat:
|
||||
def __init__(self, output_file):
|
||||
self.output_file = output_file
|
||||
def get_output_file(self):
|
||||
return self.output_file
|
||||
|
||||
task_compat = TaskCompat(render_task.output_file)
|
||||
upload_success = api.upload_task_file(task_info, task_compat)
|
||||
if not upload_success:
|
||||
span.set_status(Status(StatusCode.ERROR))
|
||||
api.report_task_failed(task_info, "Upload failed")
|
||||
return False
|
||||
|
||||
# 清理临时文件
|
||||
self._cleanup_temp_files(render_task)
|
||||
|
||||
# 报告任务成功
|
||||
api.report_task_success(task_info, videoInfo={
|
||||
"width": width,
|
||||
"height": height,
|
||||
"duration": duration
|
||||
})
|
||||
|
||||
span.set_status(Status(StatusCode.OK))
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
span.set_status(Status(StatusCode.ERROR))
|
||||
logger.error(f"Task processing failed: {e}", exc_info=True)
|
||||
api.report_task_failed(task_info, str(e))
|
||||
return False
|
||||
|
||||
def create_render_task(self, task_info: Dict[str, Any], template_info: Dict[str, Any]) -> RenderTask:
|
||||
"""创建渲染任务"""
|
||||
tracer = get_tracer(__name__)
|
||||
with tracer.start_as_current_span("create_render_task") as span:
|
||||
# 解析任务参数
|
||||
task_params_str = task_info.get("taskParams", "{}")
|
||||
span.set_attribute("task_params", task_params_str)
|
||||
|
||||
try:
|
||||
task_params = json.loads(task_params_str)
|
||||
task_params_orig = json.loads(task_params_str)
|
||||
except json.JSONDecodeError as e:
|
||||
raise TaskValidationError(f"Invalid task params JSON: {e}")
|
||||
|
||||
# 并行下载资源
|
||||
self._download_resources(task_params)
|
||||
|
||||
# 创建子任务列表
|
||||
sub_tasks = []
|
||||
only_if_usage_count = {}
|
||||
|
||||
for part in template_info.get("video_parts", []):
|
||||
source, ext_data = self._parse_video_source(
|
||||
part.get('source'), task_params, template_info
|
||||
)
|
||||
if not source:
|
||||
logger.warning("No video found for part: %s", part)
|
||||
continue
|
||||
|
||||
# 检查only_if条件
|
||||
only_if = part.get('only_if', '')
|
||||
if only_if:
|
||||
only_if_usage_count[only_if] = only_if_usage_count.get(only_if, 0) + 1
|
||||
required_count = only_if_usage_count[only_if]
|
||||
if not self._check_placeholder_exist_with_count(only_if, task_params_orig, required_count):
|
||||
logger.info("Skipping part due to only_if condition: %s (need %d)", only_if, required_count)
|
||||
continue
|
||||
|
||||
# 创建子任务
|
||||
sub_task = self._create_sub_task(part, source, ext_data, template_info)
|
||||
sub_tasks.append(sub_task)
|
||||
|
||||
# 创建主任务
|
||||
output_file = f"out_{task_info.get('id', 'unknown')}.mp4"
|
||||
main_task = RenderTask(
|
||||
input_files=[task.output_file for task in sub_tasks],
|
||||
output_file=output_file,
|
||||
resolution=template_info.get("video_size", ""),
|
||||
frame_rate=template_info.get("frame_rate", 25),
|
||||
center_cut=template_info.get("crop_mode"),
|
||||
zoom_cut=template_info.get("zoom_cut")
|
||||
)
|
||||
|
||||
# 应用整体模板设置
|
||||
overall_template = template_info.get("overall_template", {})
|
||||
self._apply_template_settings(main_task, overall_template, template_info)
|
||||
|
||||
# 设置扩展数据
|
||||
main_task.ext_data = task_info
|
||||
|
||||
span.set_attribute("render_task.sub_tasks", len(sub_tasks))
|
||||
span.set_attribute("render_task.effects", len(main_task.effects))
|
||||
|
||||
return main_task
|
||||
|
||||
def _download_resources(self, task_params: Dict[str, Any]):
|
||||
"""并行下载资源"""
|
||||
with ThreadPoolExecutor(max_workers=8) as executor:
|
||||
for param_list in task_params.values():
|
||||
if isinstance(param_list, list):
|
||||
for param in param_list:
|
||||
url = param.get("url", "")
|
||||
if url.startswith("http"):
|
||||
_, filename = os.path.split(url)
|
||||
executor.submit(oss.download_from_oss, url, filename, True)
|
||||
|
||||
def _parse_video_source(self, source: str, task_params: Dict[str, Any],
|
||||
template_info: Dict[str, Any]) -> tuple[Optional[str], Dict[str, Any]]:
|
||||
"""解析视频源"""
|
||||
if source.startswith('PLACEHOLDER_'):
|
||||
placeholder_id = source.replace('PLACEHOLDER_', '')
|
||||
new_sources = task_params.get(placeholder_id, [])
|
||||
pick_source = {}
|
||||
|
||||
if isinstance(new_sources, list):
|
||||
if len(new_sources) == 0:
|
||||
logger.debug("No video found for placeholder: %s", placeholder_id)
|
||||
return None, pick_source
|
||||
else:
|
||||
pick_source = new_sources.pop(0)
|
||||
new_sources = pick_source.get("url", "")
|
||||
|
||||
if new_sources.startswith("http"):
|
||||
_, source_name = os.path.split(new_sources)
|
||||
oss.download_from_oss(new_sources, source_name, True)
|
||||
return source_name, pick_source
|
||||
return new_sources, pick_source
|
||||
|
||||
return os.path.join(template_info.get("local_path", ""), source), {}
|
||||
|
||||
def _check_placeholder_exist_with_count(self, placeholder_id: str, task_params: Dict[str, Any],
|
||||
required_count: int = 1) -> bool:
|
||||
"""检查占位符是否存在足够数量的片段"""
|
||||
if placeholder_id in task_params:
|
||||
new_sources = task_params.get(placeholder_id, [])
|
||||
if isinstance(new_sources, list):
|
||||
return len(new_sources) >= required_count
|
||||
return required_count <= 1
|
||||
return False
|
||||
|
||||
def _create_sub_task(self, part: Dict[str, Any], source: str, ext_data: Dict[str, Any],
|
||||
template_info: Dict[str, Any]) -> RenderTask:
|
||||
"""创建子任务"""
|
||||
sub_task = RenderTask(
|
||||
input_files=[source],
|
||||
resolution=template_info.get("video_size", ""),
|
||||
frame_rate=template_info.get("frame_rate", 25),
|
||||
annexb=True,
|
||||
center_cut=part.get("crop_mode"),
|
||||
zoom_cut=part.get("zoom_cut"),
|
||||
ext_data=ext_data
|
||||
)
|
||||
|
||||
# 应用部分模板设置
|
||||
self._apply_template_settings(sub_task, part, template_info)
|
||||
|
||||
return sub_task
|
||||
|
||||
def _apply_template_settings(self, task: RenderTask, template_part: Dict[str, Any],
|
||||
template_info: Dict[str, Any]):
|
||||
"""应用模板设置到任务"""
|
||||
# 添加效果
|
||||
for effect in template_part.get('effects', []):
|
||||
task.add_effect(effect)
|
||||
|
||||
# 添加LUT
|
||||
for lut in template_part.get('luts', []):
|
||||
full_path = os.path.join(template_info.get("local_path", ""), lut)
|
||||
task.add_lut(full_path.replace("\\", "/"))
|
||||
|
||||
# 添加音频
|
||||
for audio in template_part.get('audios', []):
|
||||
full_path = os.path.join(template_info.get("local_path", ""), audio)
|
||||
task.add_audios(full_path)
|
||||
|
||||
# 添加覆盖层
|
||||
for overlay in template_part.get('overlays', []):
|
||||
full_path = os.path.join(template_info.get("local_path", ""), overlay)
|
||||
task.add_overlay(full_path)
|
||||
|
||||
def _cleanup_temp_files(self, task: RenderTask):
|
||||
"""清理临时文件"""
|
||||
try:
|
||||
template_dir = os.getenv("TEMPLATE_DIR", "")
|
||||
if template_dir and template_dir not in task.output_file:
|
||||
if os.path.exists(task.output_file):
|
||||
os.remove(task.output_file)
|
||||
logger.info("Cleaned up temp file: %s", task.output_file)
|
||||
else:
|
||||
logger.info("Skipped cleanup of template file: %s", task.output_file)
|
||||
except OSError as e:
|
||||
logger.warning("Failed to cleanup temp file %s: %s", task.output_file, e)
|
Reference in New Issue
Block a user