From a8abb92b8427edb56587426fd96b1b0a09bac9cb Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Wed, 27 Nov 2024 11:07:20 +0800 Subject: [PATCH] Init --- .env | 4 ++ .gitignore | 28 +++++++++++ biz/ffmpeg.py | 56 ++++++++++++++++++++++ biz/render.py | 0 biz/task.py | 15 ++++++ config/__init__.py | 16 +++++++ constant/__init__.py | 6 +++ entity/ffmpeg.py | 111 +++++++++++++++++++++++++++++++++++++++++++ index.py | 19 ++++++++ requirements.txt | 3 ++ template/.gitignore | 1 + template/__init__.py | 79 ++++++++++++++++++++++++++++++ util/api.py | 63 ++++++++++++++++++++++++ util/ffmpeg.py | 27 +++++++++++ util/oss.py | 17 +++++++ util/system.py | 21 ++++++++ 16 files changed, 466 insertions(+) create mode 100644 .env create mode 100644 .gitignore create mode 100644 biz/ffmpeg.py create mode 100644 biz/render.py create mode 100644 biz/task.py create mode 100644 config/__init__.py create mode 100644 constant/__init__.py create mode 100644 entity/ffmpeg.py create mode 100644 index.py create mode 100644 requirements.txt create mode 100644 template/.gitignore create mode 100644 template/__init__.py create mode 100644 util/api.py create mode 100644 util/ffmpeg.py create mode 100644 util/oss.py create mode 100644 util/system.py diff --git a/.env b/.env new file mode 100644 index 0000000..1949a34 --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +TEMPLATE_DIR=template/ +API_ENDPOINT=/task +API_TOKEN=123456 +TEMP_DIR=tmp/ \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2044f4 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/biz/ffmpeg.py b/biz/ffmpeg.py new file mode 100644 index 0000000..04c4235 --- /dev/null +++ b/biz/ffmpeg.py @@ -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) diff --git a/biz/render.py b/biz/render.py new file mode 100644 index 0000000..e69de29 diff --git a/biz/task.py b/biz/task.py new file mode 100644 index 0000000..ebea240 --- /dev/null +++ b/biz/task.py @@ -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) \ No newline at end of file diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..6ee1b9d --- /dev/null +++ b/config/__init__.py @@ -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) \ No newline at end of file diff --git a/constant/__init__.py b/constant/__init__.py new file mode 100644 index 0000000..ff5a662 --- /dev/null +++ b/constant/__init__.py @@ -0,0 +1,6 @@ +SUPPORT_FEATURE = ( + 'simple_render_algo', + 'gpu_accelerate', + 'intel_gpu_accelerate', +) +SOFTWARE_VERSION = '0.0.1' diff --git a/entity/ffmpeg.py b/entity/ffmpeg.py new file mode 100644 index 0000000..d5383fd --- /dev/null +++ b/entity/ffmpeg.py @@ -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 diff --git a/index.py b/index.py new file mode 100644 index 0000000..9b0640a --- /dev/null +++ b/index.py @@ -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 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..95e38e6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +requests~=2.32.3 +psutil~=6.1.0 +python-dotenv~=1.0.1 \ No newline at end of file diff --git a/template/.gitignore b/template/.gitignore new file mode 100644 index 0000000..01b7e33 --- /dev/null +++ b/template/.gitignore @@ -0,0 +1 @@ +**/* \ No newline at end of file diff --git a/template/__init__.py b/template/__init__.py new file mode 100644 index 0000000..7d9b610 --- /dev/null +++ b/template/__init__.py @@ -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): + ... diff --git a/util/api.py b/util/api.py new file mode 100644 index 0000000..8250d07 --- /dev/null +++ b/util/api.py @@ -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 \ No newline at end of file diff --git a/util/ffmpeg.py b/util/ffmpeg.py new file mode 100644 index 0000000..e377b7a --- /dev/null +++ b/util/ffmpeg.py @@ -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 \ No newline at end of file diff --git a/util/oss.py b/util/oss.py new file mode 100644 index 0000000..fac1178 --- /dev/null +++ b/util/oss.py @@ -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 diff --git a/util/system.py b/util/system.py new file mode 100644 index 0000000..de6fda0 --- /dev/null +++ b/util/system.py @@ -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