You've already forked FrameTour-RenderWorker
Init
This commit is contained in:
4
.env
Normal file
4
.env
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
TEMPLATE_DIR=template/
|
||||||
|
API_ENDPOINT=/task
|
||||||
|
API_TOKEN=123456
|
||||||
|
TEMP_DIR=tmp/
|
28
.gitignore
vendored
Normal file
28
.gitignore
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
.idea/
|
||||||
|
.idea_modules/
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
*.egg-info/
|
||||||
|
*.egg
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
*.log
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
docs/_build/
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
.ipynb_checkpoints
|
||||||
|
.venv
|
||||||
|
venv/
|
||||||
|
cython_debug/
|
56
biz/ffmpeg.py
Normal file
56
biz/ffmpeg.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
from entity.ffmpeg import FfmpegTask
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from util import ffmpeg
|
||||||
|
|
||||||
|
logger = logging.getLogger('biz/ffmpeg')
|
||||||
|
|
||||||
|
|
||||||
|
def parse_ffmpeg_task(task_info, template_info):
|
||||||
|
tasks = []
|
||||||
|
# 中间片段
|
||||||
|
for part in template_info.get("video_parts"):
|
||||||
|
sub_ffmpeg_task = parse_video_part(part, task_info)
|
||||||
|
if not sub_ffmpeg_task:
|
||||||
|
continue
|
||||||
|
tasks.append(sub_ffmpeg_task)
|
||||||
|
task = FfmpegTask(tasks, output_file="test.mp4")
|
||||||
|
task.correct_task_type()
|
||||||
|
overall = template_info.get("overall_template")
|
||||||
|
task.add_lut(*overall.get('luts', []))
|
||||||
|
task.add_audios(*overall.get('audios', []))
|
||||||
|
task.add_overlay(*overall.get('overlays', []))
|
||||||
|
return task
|
||||||
|
|
||||||
|
|
||||||
|
def parse_video_part(video_part, task_info):
|
||||||
|
source = select_video_if_needed(video_part.get('source'), task_info)
|
||||||
|
if not source:
|
||||||
|
logger.warning("no video found for part: " + str(video_part))
|
||||||
|
return None
|
||||||
|
task = FfmpegTask(source)
|
||||||
|
task.add_lut(*video_part.get('luts', []))
|
||||||
|
task.add_audios(*video_part.get('audios', []))
|
||||||
|
task.add_overlay(*video_part.get('overlays', []))
|
||||||
|
return task
|
||||||
|
|
||||||
|
|
||||||
|
def select_video_if_needed(source, task_info):
|
||||||
|
if source.startswith('PLACEHOLDER_'):
|
||||||
|
placeholder_id = source.replace('PLACEHOLDER_', '')
|
||||||
|
new_sources = task_info.get('user_videos', {}).get(placeholder_id, [])
|
||||||
|
if type(new_sources) is list:
|
||||||
|
if len(new_sources) == 0:
|
||||||
|
logger.debug("no video found for placeholder: " + placeholder_id)
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
# TODO: Random Pick / Policy Pick
|
||||||
|
new_sources = new_sources[0]
|
||||||
|
return new_sources
|
||||||
|
return source
|
||||||
|
|
||||||
|
|
||||||
|
def start_ffmpeg_task(ffmpeg_task):
|
||||||
|
for task in ffmpeg_task.analyze_input_render_tasks():
|
||||||
|
start_ffmpeg_task(task)
|
||||||
|
ffmpeg.start_render(ffmpeg_task)
|
0
biz/render.py
Normal file
0
biz/render.py
Normal file
15
biz/task.py
Normal file
15
biz/task.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from template import get_template_def
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_task(task_info):
|
||||||
|
...
|
||||||
|
return task_info
|
||||||
|
|
||||||
|
|
||||||
|
def start_task(task_info):
|
||||||
|
from biz.ffmpeg import parse_ffmpeg_task, start_ffmpeg_task
|
||||||
|
task_info = normalize_task(task_info)
|
||||||
|
task_template = "test_template"
|
||||||
|
template_info = get_template_def(task_template)
|
||||||
|
ffmpeg_task = parse_ffmpeg_task(task_info, template_info)
|
||||||
|
result = start_ffmpeg_task(ffmpeg_task)
|
16
config/__init__.py
Normal file
16
config/__init__.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
root_logger = logging.getLogger()
|
||||||
|
rf_handler = TimedRotatingFileHandler('all_log.log', when='midnight')
|
||||||
|
rf_handler.setFormatter(logging.Formatter("[%(asctime)s][%(name)s]%(levelname)s - %(message)s"))
|
||||||
|
rf_handler.setLevel(logging.DEBUG)
|
||||||
|
f_handler = TimedRotatingFileHandler('error.log', when='midnight')
|
||||||
|
f_handler.setLevel(logging.ERROR)
|
||||||
|
f_handler.setFormatter(logging.Formatter("[%(asctime)s][%(name)s][:%(lineno)d]%(levelname)s - - %(message)s"))
|
||||||
|
root_logger.addHandler(rf_handler)
|
||||||
|
root_logger.addHandler(f_handler)
|
6
constant/__init__.py
Normal file
6
constant/__init__.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
SUPPORT_FEATURE = (
|
||||||
|
'simple_render_algo',
|
||||||
|
'gpu_accelerate',
|
||||||
|
'intel_gpu_accelerate',
|
||||||
|
)
|
||||||
|
SOFTWARE_VERSION = '0.0.1'
|
111
entity/ffmpeg.py
Normal file
111
entity/ffmpeg.py
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
|
||||||
|
class FfmpegTask(object):
|
||||||
|
|
||||||
|
def __init__(self, input_file, task_type='copy', output_file=''):
|
||||||
|
if type(input_file) is str:
|
||||||
|
self.input_file = [input_file]
|
||||||
|
elif type(input_file) is list:
|
||||||
|
self.input_file = input_file
|
||||||
|
else:
|
||||||
|
self.input_file = []
|
||||||
|
self.task_type = task_type
|
||||||
|
self.output_file = output_file
|
||||||
|
self.mute = True
|
||||||
|
self.speed = 1
|
||||||
|
self.subtitles = []
|
||||||
|
self.luts = []
|
||||||
|
self.audios = []
|
||||||
|
self.overlays = []
|
||||||
|
self.annexb = False
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
_str = f'FfmpegTask(input_file={self.input_file}, task_type={self.task_type}'
|
||||||
|
if len(self.luts) > 0:
|
||||||
|
_str += f', luts={self.luts}'
|
||||||
|
if len(self.audios) > 0:
|
||||||
|
_str += f', audios={self.audios}'
|
||||||
|
if len(self.overlays) > 0:
|
||||||
|
_str += f', overlays={self.overlays}'
|
||||||
|
if self.annexb:
|
||||||
|
_str += f', annexb={self.annexb}'
|
||||||
|
if self.mute:
|
||||||
|
_str += f', mute={self.mute}'
|
||||||
|
return _str + ')'
|
||||||
|
|
||||||
|
def analyze_input_render_tasks(self):
|
||||||
|
for i in self.input_file:
|
||||||
|
if type(i) is str:
|
||||||
|
continue
|
||||||
|
elif type(i) is FfmpegTask:
|
||||||
|
if i.need_run():
|
||||||
|
yield i
|
||||||
|
|
||||||
|
def need_run(self):
|
||||||
|
"""
|
||||||
|
判断是否需要运行
|
||||||
|
:rtype: bool
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if self.annexb:
|
||||||
|
return True
|
||||||
|
# TODO: copy from url
|
||||||
|
return not self.check_can_copy()
|
||||||
|
|
||||||
|
def add_inputs(self, *inputs):
|
||||||
|
self.input_file.extend(inputs)
|
||||||
|
|
||||||
|
def add_overlay(self, *overlays):
|
||||||
|
self.overlays.extend(overlays)
|
||||||
|
self.correct_task_type()
|
||||||
|
|
||||||
|
def add_audios(self, *audios):
|
||||||
|
self.audios.extend(audios)
|
||||||
|
self.correct_task_type()
|
||||||
|
self.check_audio_track()
|
||||||
|
|
||||||
|
def add_lut(self, *luts):
|
||||||
|
self.luts.extend(luts)
|
||||||
|
self.correct_task_type()
|
||||||
|
|
||||||
|
def get_output_file(self):
|
||||||
|
if self.task_type == 'copy':
|
||||||
|
return self.input_file
|
||||||
|
return self.output_file
|
||||||
|
|
||||||
|
def correct_task_type(self):
|
||||||
|
if self.check_can_copy():
|
||||||
|
self.task_type = 'copy'
|
||||||
|
elif self.check_can_concat():
|
||||||
|
self.task_type = 'concat'
|
||||||
|
else:
|
||||||
|
self.task_type = 'encode'
|
||||||
|
|
||||||
|
def check_can_concat(self):
|
||||||
|
if len(self.luts) > 0:
|
||||||
|
return False
|
||||||
|
if len(self.overlays) > 0:
|
||||||
|
return False
|
||||||
|
if len(self.subtitles) > 0:
|
||||||
|
return False
|
||||||
|
if self.speed != 1:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def check_can_copy(self):
|
||||||
|
if len(self.luts) > 0:
|
||||||
|
return False
|
||||||
|
if len(self.overlays) > 0:
|
||||||
|
return False
|
||||||
|
if len(self.subtitles) > 0:
|
||||||
|
return False
|
||||||
|
if self.speed != 1:
|
||||||
|
return False
|
||||||
|
if len(self.audios) > 1:
|
||||||
|
return False
|
||||||
|
if len(self.input_file) > 1:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def check_audio_track(self):
|
||||||
|
if len(self.audios) > 0:
|
||||||
|
self.mute = False
|
19
index.py
Normal file
19
index.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
from time import sleep
|
||||||
|
|
||||||
|
import biz.task
|
||||||
|
import config
|
||||||
|
from template import load_local_template
|
||||||
|
from util import api
|
||||||
|
from util.system import get_sys_info
|
||||||
|
|
||||||
|
load_local_template()
|
||||||
|
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# print(get_sys_info())
|
||||||
|
print("waiting for task...")
|
||||||
|
task_list = api.get_render_task()
|
||||||
|
for task in task_list:
|
||||||
|
print("start task:", task)
|
||||||
|
biz.task.start_task(task)
|
||||||
|
break
|
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
requests~=2.32.3
|
||||||
|
psutil~=6.1.0
|
||||||
|
python-dotenv~=1.0.1
|
1
template/.gitignore
vendored
Normal file
1
template/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
**/*
|
79
template/__init__.py
Normal file
79
template/__init__.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
TEMPLATES = {}
|
||||||
|
logger = logging.getLogger("template")
|
||||||
|
|
||||||
|
def check_local_template(local_name):
|
||||||
|
template_def = TEMPLATES[local_name]
|
||||||
|
base_dir = template_def.get("local_path")
|
||||||
|
for video_part in template_def.get("video_parts", []):
|
||||||
|
source_file = video_part.get("source", "")
|
||||||
|
if str(source_file).startswith("http"):
|
||||||
|
# download file
|
||||||
|
...
|
||||||
|
elif str(source_file).startswith("PLACEHOLDER_"):
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if not os.path.isabs(source_file):
|
||||||
|
source_file = os.path.join(base_dir, source_file)
|
||||||
|
if not os.path.exists(source_file):
|
||||||
|
logger.error(f"{source_file} not found, please check the template definition")
|
||||||
|
raise Exception(f"{source_file} not found, please check the template definition")
|
||||||
|
for audio in video_part.get("audios", []):
|
||||||
|
if not os.path.isabs(audio):
|
||||||
|
audio = os.path.join(base_dir, audio)
|
||||||
|
if not os.path.exists(audio):
|
||||||
|
logger.error(f"{audio} not found, please check the template definition")
|
||||||
|
raise Exception(f"{audio} not found, please check the template definition")
|
||||||
|
for lut in video_part.get("luts", []):
|
||||||
|
if not os.path.isabs(lut):
|
||||||
|
lut = os.path.join(base_dir, lut)
|
||||||
|
if not os.path.exists(lut):
|
||||||
|
logger.error(f"{lut} not found, please check the template definition")
|
||||||
|
raise Exception(f"{lut} not found, please check the template definition")
|
||||||
|
for mask in video_part.get("overlays", []):
|
||||||
|
if not os.path.isabs(mask):
|
||||||
|
mask = os.path.join(base_dir, mask)
|
||||||
|
if not os.path.exists(mask):
|
||||||
|
logger.error(f"{mask} not found, please check the template definition")
|
||||||
|
raise Exception(f"{mask} not found, please check the template definition")
|
||||||
|
|
||||||
|
|
||||||
|
def load_template(template_name, local_path):
|
||||||
|
global TEMPLATES
|
||||||
|
logger.info(f"加载视频模板定义:【{template_name}({local_path})】")
|
||||||
|
template_def_file = os.path.join(local_path, "template.json")
|
||||||
|
if os.path.exists(template_def_file):
|
||||||
|
TEMPLATES[template_name] = json.load(open(template_def_file, 'rb'))
|
||||||
|
TEMPLATES[template_name]["local_path"] = local_path
|
||||||
|
try:
|
||||||
|
check_local_template(template_name)
|
||||||
|
logger.info(f"完成加载【{template_name}】模板")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"模板定义文件【{template_def_file}】有误,正在尝试重新下载模板", exc_info=e)
|
||||||
|
download_template(template_name)
|
||||||
|
|
||||||
|
|
||||||
|
def load_local_template():
|
||||||
|
for template_name in os.listdir(os.getenv("TEMPLATE_DIR")):
|
||||||
|
if template_name.startswith("_"):
|
||||||
|
continue
|
||||||
|
if template_name.startswith("."):
|
||||||
|
continue
|
||||||
|
target_path = os.path.join(os.getenv("TEMPLATE_DIR"), template_name)
|
||||||
|
if os.path.isdir(target_path):
|
||||||
|
load_template(template_name, target_path)
|
||||||
|
|
||||||
|
|
||||||
|
def get_template_def(template_id):
|
||||||
|
return TEMPLATES.get(template_id)
|
||||||
|
|
||||||
|
def download_template(template_id):
|
||||||
|
logger.info(f"下载模板:{template_id}")
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def analyze_template(template_id):
|
||||||
|
...
|
63
util/api.py
Normal file
63
util/api.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import requests
|
||||||
|
|
||||||
|
session = requests.Session()
|
||||||
|
|
||||||
|
|
||||||
|
def get_render_task():
|
||||||
|
"""
|
||||||
|
通过接口获取任务
|
||||||
|
:return: 任务列表
|
||||||
|
"""
|
||||||
|
tasks = []
|
||||||
|
tasks.append({
|
||||||
|
'user_videos': {
|
||||||
|
'CAM_ID': 'paper-planes.mp4'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return tasks
|
||||||
|
|
||||||
|
|
||||||
|
def get_template_info(template_id):
|
||||||
|
"""
|
||||||
|
通过接口获取模板信息
|
||||||
|
:rtype: Template
|
||||||
|
:param template_id: 模板id
|
||||||
|
:type template_id: str
|
||||||
|
:return: 模板信息
|
||||||
|
"""
|
||||||
|
template = {
|
||||||
|
'id': template_id,
|
||||||
|
'name': '模板名称',
|
||||||
|
'description': '模板描述',
|
||||||
|
'video_size': '1920x1080',
|
||||||
|
'frame_rate': 30,
|
||||||
|
'overall_duration': 30,
|
||||||
|
'video_parts': [
|
||||||
|
{
|
||||||
|
'source': './template/test_template/1.mp4',
|
||||||
|
'mute': True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'source': 'PLACEHOLDER_CAM_ID',
|
||||||
|
'mute': True,
|
||||||
|
'overlays': [
|
||||||
|
'./template/test_template/2.mov'
|
||||||
|
],
|
||||||
|
'luts': [
|
||||||
|
'./template/test_template/cube.cube'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'source': './template/test_template/3.mp4',
|
||||||
|
'mute': True,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'overall_template': {
|
||||||
|
'source': None,
|
||||||
|
'mute': False,
|
||||||
|
'audios': [
|
||||||
|
'./template/test_template/bgm.acc'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return template
|
27
util/ffmpeg.py
Normal file
27
util/ffmpeg.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from typing import Optional, IO
|
||||||
|
|
||||||
|
from entity.ffmpeg import FfmpegTask
|
||||||
|
|
||||||
|
|
||||||
|
def start_render(ffmpeg_task: FfmpegTask):
|
||||||
|
print(ffmpeg_task)
|
||||||
|
|
||||||
|
def handle_ffmpeg_output(stdout: Optional[IO[bytes]]) -> str:
|
||||||
|
out_time = "0:0:0.0"
|
||||||
|
if stdout is None:
|
||||||
|
print("[!]STDOUT is null")
|
||||||
|
return out_time
|
||||||
|
speed = "0"
|
||||||
|
while True:
|
||||||
|
line = stdout.readline()
|
||||||
|
if line == b"":
|
||||||
|
break
|
||||||
|
if line.strip() == b"progress=end":
|
||||||
|
# 处理完毕
|
||||||
|
break
|
||||||
|
if line.startswith(b"out_time="):
|
||||||
|
out_time = line.replace(b"out_time=", b"").decode().strip()
|
||||||
|
if line.startswith(b"speed="):
|
||||||
|
speed = line.replace(b"speed=", b"").decode().strip()
|
||||||
|
print("[ ]Speed:", out_time, "@", speed)
|
||||||
|
return out_time
|
17
util/oss.py
Normal file
17
util/oss.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
def upload_to_oss_use_signed_url(url, file_path):
|
||||||
|
"""
|
||||||
|
使用签名URL上传文件到OSS
|
||||||
|
:param str url: 签名URL
|
||||||
|
:param str file_path: 文件路径
|
||||||
|
:return bool: 是否成功
|
||||||
|
"""
|
||||||
|
with open(file_path, 'rb') as f:
|
||||||
|
try:
|
||||||
|
response = requests.put(url, data=f)
|
||||||
|
return response.status_code == 200
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return False
|
21
util/system.py
Normal file
21
util/system.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import psutil
|
||||||
|
from constant import SUPPORT_FEATURE, SOFTWARE_VERSION
|
||||||
|
|
||||||
|
|
||||||
|
def get_sys_info():
|
||||||
|
"""
|
||||||
|
Returns a dictionary with system information.
|
||||||
|
"""
|
||||||
|
info = {
|
||||||
|
'version': SOFTWARE_VERSION,
|
||||||
|
'platform': platform.system(),
|
||||||
|
'runtime_version': 'Python ' + platform.python_version(),
|
||||||
|
'cpu_count': os.cpu_count(),
|
||||||
|
'cpu_usage': psutil.cpu_percent(),
|
||||||
|
'memory_total': psutil.virtual_memory().total,
|
||||||
|
'memory_available': psutil.virtual_memory().available,
|
||||||
|
'support_feature': SUPPORT_FEATURE
|
||||||
|
}
|
||||||
|
return info
|
Reference in New Issue
Block a user