diff --git a/.gitignore b/.gitignore index ee7f1f0..d1a2d31 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ dist/ __pycache__/ debug.json *.mp4 +*.png diff --git a/src/args.py b/src/args.py index 726243c..d35012f 100644 --- a/src/args.py +++ b/src/args.py @@ -18,6 +18,13 @@ def init_parser(): type=str, help="path to output video", ) + parser.add_argument( + "-f", + "--frame", + metavar="preview_frame", + type=int, + help="frame for preview", + ) parser.add_argument( "--debug", action="store_true", diff --git a/src/content.py b/src/content.py index 7abde91..d71847d 100644 --- a/src/content.py +++ b/src/content.py @@ -53,6 +53,7 @@ class VSMLContent: style: Style exist_video: bool exist_audio: bool + _second: float def __init__( self, diff --git a/src/converter/__init__.py b/src/converter/__init__.py index 30b5c6f..cbb02e1 100644 --- a/src/converter/__init__.py +++ b/src/converter/__init__.py @@ -1 +1,2 @@ from .main import convert_video +from .preview import * diff --git a/src/converter/main.py b/src/converter/main.py index 9a7b230..5bfb662 100644 --- a/src/converter/main.py +++ b/src/converter/main.py @@ -1,4 +1,3 @@ -import json from typing import Optional from content import SourceContent, VSMLContent, WrapContent @@ -71,20 +70,6 @@ def convert_video( audio_process=process.audio, ) - if debug_mode: - content_str = ( - str(vsml_data.content) - .replace("'", '"') - .replace("True", "true") - .replace("False", "false") - .replace("None", "null") - ) - content_str = json.dumps( - (json.loads(content_str)), indent=2, ensure_ascii=False - ) - with open("./debug.json", "w") as f: - f.write(content_str) - export_video( process.video, process.audio, out_filename, debug_mode, overwrite ) diff --git a/src/converter/preview/__init__.py b/src/converter/preview/__init__.py new file mode 100644 index 0000000..2ec68bd --- /dev/null +++ b/src/converter/preview/__init__.py @@ -0,0 +1 @@ +from .main import convert_image_from_frame diff --git a/src/converter/preview/content.py b/src/converter/preview/content.py new file mode 100644 index 0000000..64f93a9 --- /dev/null +++ b/src/converter/preview/content.py @@ -0,0 +1,84 @@ +from typing import Optional + +from style import Order +from vsml import SourceContent, VSMLContent, WrapContent + + +def pick_data( + vsml_content: VSMLContent, second: float +) -> Optional[VSMLContent]: + if not vsml_content.exist_video: + return None + if isinstance(vsml_content, WrapContent): + if vsml_content.style.order == Order.SEQUENCE: + items = vsml_content.items + vsml_content.items = [] + whole_second = 0 + left_margin_end = 0 + + for item in items: + whole_second += max( + item.style.time_margin_start.get_second(), left_margin_end + ) + if second < whole_second: + return vsml_content + whole_second += item.style.time_padding_start.get_second() + if second < whole_second: + item._second = -1 + vsml_content.items = [item] + return vsml_content + whole_second_with_object_length = ( + whole_second + item.style.object_length.get_second() + ) + if second < whole_second_with_object_length: + child = pick_data(item, second - whole_second) + if child is not None: + vsml_content.items = [child] + return vsml_content + whole_second = whole_second_with_object_length + whole_second += item.style.time_padding_end.get_second() + if second < whole_second: + item._second = -1 + vsml_content.items = [item] + return vsml_content + left_margin_end = item.style.time_margin_end.get_second() + + elif vsml_content.style.order == Order.PARALLEL: + items = vsml_content.items + vsml_content.items = [] + + for item in items: + whole_second = item.style.time_margin_start.get_second() + if second < whole_second: + continue + whole_second += item.style.time_padding_start.get_second() + if second < whole_second: + item._second = -1 + vsml_content.items.append(item) + continue + whole_second_with_object_length = ( + whole_second + item.style.object_length.get_second() + ) + if ( + second < whole_second_with_object_length + or item.style.object_length.is_fit() + ): + child = pick_data(item, second - whole_second) + if child is not None: + vsml_content.items.append(child) + continue + whole_second = whole_second_with_object_length + whole_second += item.style.time_padding_end.get_second() + if second < whole_second: + item._second = -1 + vsml_content.items.append(item) + continue + return vsml_content + + else: + raise Exception() + elif isinstance(vsml_content, SourceContent): + vsml_content._second = second + return vsml_content + else: + raise Exception() diff --git a/src/converter/preview/main.py b/src/converter/preview/main.py new file mode 100644 index 0000000..767c42d --- /dev/null +++ b/src/converter/preview/main.py @@ -0,0 +1,38 @@ +from typing import Optional + +import ffmpeg + +from converter.ffmpeg import set_background_filter +from utils import VSMLManager +from vsml import VSML, WrapContent + +from .content import pick_data +from .process import create_preview_process + + +def convert_image_from_frame( + vsml_data: VSML, frame: int, output_path: Optional[str] +) -> None: + output_path = "preview.png" if output_path is None else output_path + second = frame / VSMLManager.get_root_fps() + if second > vsml_data.content.style.object_length.get_second(): + raise Exception() + vsml_content_for_pick = None + vsml_content = vsml_data.content + if second >= vsml_content.style.time_margin_start.get_second(): + if second < vsml_content.style.time_padding_start.get_second(): + if isinstance(vsml_content, WrapContent): + vsml_content.items = [] + vsml_content_for_pick = vsml_content + else: + vsml_content_for_pick = pick_data(vsml_content, second) + + process = create_preview_process(vsml_content_for_pick) + process.video = set_background_filter( + background_color=vsml_content.style.background_color, + resolution_text=VSMLManager.get_root_resolution().get_str(), + video_process=process.video, + fit_video_process=True, + ) + process = ffmpeg.output(process.video, output_path, vframes=1) + process.run(overwrite_output=True) diff --git a/src/converter/preview/process.py b/src/converter/preview/process.py new file mode 100644 index 0000000..466b2cc --- /dev/null +++ b/src/converter/preview/process.py @@ -0,0 +1,164 @@ +from typing import Optional + +import ffmpeg + +from converter.ffmpeg import ( + get_background_process, + get_source_process, + get_text_process, + layering_filter, + set_background_filter, + width_height_filter, +) +from converter.schemas import Process +from style import GraphicValue, LayerMode, Order +from utils import SourceType +from vsml import SourceContent, VSMLContent, WrapContent + + +def create_preview_source_process(vsml_content: SourceContent) -> Process: + style = vsml_content.style + if vsml_content.type == SourceType.TEXT: + video_process = get_text_process( + vsml_content.src_path, + style.get_width_with_padding(), + style.get_height_with_padding(), + style.padding_left, + style.padding_top, + style.background_color, + style.using_font_path, + style.font_size, + style.font_color, + style.font_border_color, + style.font_border_width, + ) + else: + video_process = get_source_process( + vsml_content.src_path, + exist_video=True, + exist_audio=False, + )["video"] + if vsml_content.type != SourceType.TEXT: + video_process = width_height_filter( + style.width, style.height, video_process + ) + if ( + style.padding_top.is_zero_over() + or style.padding_left.is_zero_over() + ): + video_process = set_background_filter( + width=style.get_width_with_padding(), + height=style.get_height_with_padding(), + background_color=style.background_color, + video_process=video_process, + position_x=style.padding_left, + position_y=style.padding_top, + fit_video_process=True, + ) + if vsml_content._second != -1 and vsml_content.tag_name == "vid": + video_process = ffmpeg.trim(video_process, start=vsml_content._second) + return Process(video_process, None, vsml_content.style) + + +def create_preview_wrap_process( + child_processes: list[Process], vsml_content: WrapContent +) -> Process: + style = vsml_content.style + video_process = None + width = style.get_width_with_padding() + height = style.get_height_with_padding() + if style.order == Order.SEQUENCE: + if len(child_processes): + video_process = child_processes[0].video + else: + video_process = get_background_process( + "{}x{}".format( + width.get_pixel(), + height.get_pixel(), + ), + style.background_color, + ) + + elif style.order == Order.PARALLEL: + video_process = get_background_process( + "{}x{}".format( + width.get_pixel(), + height.get_pixel(), + ), + style.background_color, + ) + is_single = style.layer_mode == LayerMode.SINGLE + is_row = style.direction is None or style.direction.is_row() + is_reverse = style.direction is None or style.direction.is_reverse + current_graphic_length = ( + ( + width - style.padding_right + if is_row + else height - style.padding_bottom + ) + if is_reverse + else (style.padding_left if is_row else style.padding_top) + ) + remain_margin = GraphicValue("0") + + for child_process in child_processes: + child_style = child_process.style + if child_process.video is not None: + max_space = max( + ( + child_style.margin_left + if is_row + else child_style.margin_top + ), + remain_margin, + ) + child_graphic_length = ( + child_style.get_width_with_padding() + if is_row + else child_style.get_height_with_padding() + ) + + current_graphic_length += ( + -child_graphic_length if is_reverse else max_space + ) + video_process = layering_filter( + video_process, + child_process.video, + ( + current_graphic_length + if is_single and is_row + else style.padding_left + child_style.margin_left + ), + ( + current_graphic_length + if is_single and not is_row + else style.padding_top + child_style.margin_top + ), + ) + current_graphic_length += ( + -max_space if is_reverse else child_graphic_length + ) + remain_margin = ( + child_style.margin_right + if is_row + else child_style.margin_bottom + ) + else: + raise Exception() + return Process(video_process, None, style) + + +def create_preview_process(vsml_content: Optional[VSMLContent]) -> Process: + if isinstance(vsml_content, SourceContent): + process = create_preview_source_process(vsml_content) + elif isinstance(vsml_content, WrapContent): + child_processes = [] + for item in vsml_content.items: + child_process = create_preview_process(item) + if child_process is not None: + child_processes.append(child_process) + process = create_preview_wrap_process(child_processes, vsml_content) + else: + raise Exception() + + return process diff --git a/src/main.py b/src/main.py index 6292b60..ecf68c3 100644 --- a/src/main.py +++ b/src/main.py @@ -1,5 +1,7 @@ +import json + from args import get_args -from converter import convert_video +from converter import convert_image_from_frame, convert_video from xml_parser import parsing_vsml @@ -14,13 +16,34 @@ def main(): # ファイルのVSMLを解析 vsml_data = parsing_vsml(args.filename, args.offline) - # 解析したデータをもとにffmpegで動画を構築 - convert_video( - vsml_data, - args.output, - args.debug, - args.overwrite, - ) + if args.debug: + content_str = ( + str(vsml_data.content) + .replace("'", '"') + .replace("True", "true") + .replace("False", "false") + .replace("None", "null") + ) + content_str = json.dumps( + (json.loads(content_str)), indent=2, ensure_ascii=False + ) + with open("./debug.json", "w") as f: + f.write(content_str) + + if args.frame is None: + # 解析したデータをもとにffmpegで動画を構築 + convert_video( + vsml_data, + args.output, + args.debug, + args.overwrite, + ) + else: + convert_image_from_frame( + vsml_data, + args.frame, + args.output, + ) if __name__ == "__main__":