This commit is contained in:
2024-11-27 11:07:20 +08:00
commit a8abb92b84
16 changed files with 466 additions and 0 deletions

4
.env Normal file
View File

@ -0,0 +1,4 @@
TEMPLATE_DIR=template/
API_ENDPOINT=/task
API_TOKEN=123456
TEMP_DIR=tmp/

28
.gitignore vendored Normal file
View 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
View 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
View File

15
biz/task.py Normal file
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,3 @@
requests~=2.32.3
psutil~=6.1.0
python-dotenv~=1.0.1

1
template/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
**/*

79
template/__init__.py Normal file
View 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
View 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
View 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
View 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
View 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