Source code for pycharmers.cli.lyricVideo

#coding: utf-8
import os
import cv2
import sys
import json
import argparse
import subprocess
import numpy as np
import moviepy.editor as mp
from PIL import Image

from ..utils._colorings import toBLUE, toGREEN, toACCENT
from ..utils.argparse_utils import ListParamProcessorCreate
from ..utils.generic_utils import now_str
from ..utils.monitor_utils import ProgressMonitor
from ..utils.pil_utils import draw_text_in_pil

[docs]def lyricVideo(argv=sys.argv[1:]): """Create a lyric Video. Args: json (str) : Path to parameter json file. --ttfontname (str) : A filename or file-like object containing a TrueType font. If the file is not found in this filename, the loader may also search in other directories, such as the ``fonts/`` directory on Windows or ``/Library/Fonts/`` , ``/System/Library/Fonts/`` and ``~/Library/Fonts/`` on macOS. --margin (int) : The margin size. --mode (str) : Optional mode to use for color values. --fontsize (int) : The font size. --fontwidth (int) : The font width. --fontheight (int) : The font height. --img-size (tuple) : The image size. --bgRGB (tuple) : he color of background image. (RGB) --textRGB (tuple) : The color of text. (RGB) --alpha-range (float) : How many seconds to set alpha to 1 (maximum). --fps (float) : The video fps. --span (int) : The span between lyrics. .. code-block:: python >>> # Create a json. >>> import numpy as np >>> def func(word, start, end, indent='\\t'*4): ... print(f'{indent}"words": "{word}",') ... print(f'{indent}"seconds": {[round(e,3) for e in np.linspace(start, end, len(word))]},') .. code-block:: python >>> import json >>> from pycharmers.utils import dumps_json >>> with open("era-it-doo/era-it.json") as f: ... data = json.load(f) >>> print(dumps_json(data)) { "kwargs": { "ttfontname": "/Users/iwasakishuto/Library/Fonts/851MkPOP_002.ttf", "img_size": [ 360, 640 ], "fontsize": 38, "fontwidth": 30, "margin": 10, "bgRGB": [ 0, 0, 0, 255 ], "mode": "RGBA", "ret_position": "line" }, "texts": [ [{"words":"1日今日も、","seconds":[2.4, 2.624, 2.848, 3.072, 3.296, 3.52],"x":-1,"y":50},{"words":"こなした労働、","seconds":[4.1, 4.317, 4.533, 4.75, 4.967, 5.183, 5.4],"x":-1,"y":-1},{"words":"辛かったけど","seconds":[5.7, 5.96, 6.22, 6.48, 6.74, 7.0],"x":-1,"y":-1},{"words":"本当がんばったじゃん","seconds":[7.0, 7.222, 7.444, 7.667, 7.889, 8.111, 8.333, 8.556, 8.778, 9.0],"x":-1,"y":-1},{"words":"1日今日も、","seconds":[9.1, 9.38, 9.66, 9.94, 10.22, 10.5],"x":-1,"y":260},{"words":"いちいちどうこう、","seconds":[10.8, 10.962, 11.125, 11.288, 11.45, 11.612, 11.775, 11.938, 12.1],"x":-1,"y":-1},{"words":"いわれたけど","seconds":[12.3, 12.52, 12.74, 12.96, 13.18, 13.4],"x":-1,"y":-1},{"words":"本当にがんばったじゃん","seconds":[13.4, 13.59, 13.78, 13.97, 14.16, 14.35, 14.54, 14.73, 14.92, 15.11, 15.3],"x":-1,"y":-1}] ] } Note: When you run from the command line, execute as follows:: $ lyricVideo dodo-era-it.json --audio dodo-era-it.mp4[.mp3] +----------------------------------------+ | Sample | +========================================+ | .. image:: _images/cli.lyricVideo.gif | +----------------------------------------+ """ parser = argparse.ArgumentParser(prog="lyricVideo", description="Create a lyric Video.", add_help=True) parser.add_argument("json", type=str, help="Path to parameter json file.") parser.add_argument("--ttfontname", type=str, default=None, help="A filename or file-like object containing a TrueType font. If the file is not found in this filename, the loader may also search in other directories, such as the ``fonts/`` directory on Windows or ``/Library/Fonts/`` , ``/System/Library/Fonts/`` and ``~/Library/Fonts/`` on macOS.") parser.add_argument("--margin", type=int, default=None, help="The margin size.") parser.add_argument("--mode", type=str, default=None, help="Optional mode to use for color values.") parser.add_argument("--fontsize", type=int, default=None, help="The font size.") parser.add_argument("--fontwidth", type=int, default=None, help="The font width.") parser.add_argument("--fontheight", type=int, default=None, help="The font height.") parser.add_argument("--img-size", action=ListParamProcessorCreate(type=int), default=None, help="The image size.") parser.add_argument("--bgRGB", action=ListParamProcessorCreate(type=int), default=None, help="The color of background image. (RGB)") parser.add_argument("--textRGB", action=ListParamProcessorCreate(type=int), default=None, help="The color of text. (RGB)") parser.add_argument("--alpha-range", type=float, help="The video length [s].", default=1.) parser.add_argument("--fps", type=float, help="The video fps.", default=30.) parser.add_argument("--span", type=int, help="The span between lyrics", default=None) parser.add_argument("--audio", type=str, help="The audio path.", default=None) args = parser.parse_args() json_path = args.json with open(json_path, mode="r") as f: data = json.load(f) args_kwargs = dict(args._get_kwargs()) data_kwargs = data.get("kwargs", {}) get_kwargs = lambda x,default=None:args_kwargs.get(x) or data_kwargs.get(x, default) img_size = tuple(get_kwargs("img_size", default=[360,640])[:2]) bgRGB = tuple(get_kwargs("bgRGB", default=(0,0,0,255))) textRGB = tuple(get_kwargs("textRGB", default=(255,255,255,255))) mode = get_kwargs("mode", default="RGBA") margin = get_kwargs("margin", default=10) fontsize = get_kwargs("fontsize", default=40) fontwidth = get_kwargs("fontwidth", default=None) fontheight = get_kwargs("fontheight", default=None) span = get_kwargs("span", default=1) audio_path = get_kwargs("audio", default=None) alpha_range = args.alpha_range fps = args.fps spf = 1/fps kwargs = { "ttfontname": get_kwargs("ttfontname", default=""), "img_size": img_size, "fontsize": fontsize, "fontwidth": fontwidth, "fontheight": fontheight, "margin": margin, "bgRGB": bgRGB, "mode": mode, # "ret_position": "line", } text_data = data.get("texts", [[]]) num_texts = len(text_data) sec_filter = [] for i,page in enumerate(text_data): start, end = (1e16, 0) for line in page: secs = line.get("seconds", []) start = min(start, *secs)-alpha_range end = max(end, *secs) sec_filter.append([i,start,end+alpha_range+span]) duration = max([e[2] for e in sec_filter]) + alpha_range + 1 num_frames = int(duration*fps) def set_pos(data, name, default): p = data.get(name, -1) if p==-1: p = default return p def set_fontcolor(start, sec, textRGB, alpha_range): fc = [0]*4 fc[:3] = textRGB[:3] fc[3] = min(255, max(0, int((sec-start)/alpha_range*255))) return tuple(fc) params = { "duration" : duration, "alpha range" : alpha_range, "FPS": fps, "span": span, "audio": audio_path, } params.update(kwargs) print("[Parameters]") for k,v in params.items(): print(f"* {toACCENT(k)}: {toGREEN(v)}") root, ext = os.path.splitext(json_path) video_path = f"{root}_{now_str()}.mp4" fourcc = cv2.VideoWriter_fourcc('m','p','4','v') out_video = cv2.VideoWriter(video_path, fourcc, fps, img_size) monitor = ProgressMonitor(max_iter=num_frames, barname="Editing") it = sec = 0 while True: it+=1 bg = Image.new(mode=mode, size=img_size, color=bgRGB) sec += spf for i,*_ in filter(lambda x:x[1]<= sec <x[2], sec_filter): texts = text_data[i] init_x = x = y = margin for j,line in enumerate(texts): words = line.get("words", "") startSecs = line.get("seconds", []) x = set_pos(data=line, name="x", default=x) y = set_pos(data=line, name="y", default=y) for w,start in zip(words, startSecs): fc = set_fontcolor(start, sec, textRGB, alpha_range) bg,(x,_) = draw_text_in_pil(text=w, img=bg, ret_position="word", textRGB=fc, x=x, y=y, **kwargs) y += fontsize x = init_x break out_video.write(np.asanyarray(bg.convert("RGB"))) monitor.report(it, sec=f"{sec:.2f}/{duration}", text=f"{j+1}/{num_texts}") if sec>duration: break out_video.release() monitor.remove() print(f"{toBLUE(video_path)} (No Sound) is created.") if audio_path is not None: root, ext = os.path.splitext(audio_path) if ext!=".mp3": audio_clip = mp.VideoFileClip(audio_path).subclip() audio_path = root + ".mp3" audio_clip.audio.write_audiofile(audio_path) print(f"Audio file ({toBLUE(audio_path)}) is created.") video_with_audio_path = f"_audio".join(os.path.splitext(video_path)) clip = mp.VideoFileClip(video_path).subclip() clip.write_videofile( video_with_audio_path, audio=audio_path, codec='libx264', audio_codec='aac', temp_audiofile='temp-audio.m4a', remove_temp=True ) command = [ "ffmpeg", "-y", "-i", video_path, "-i", audio_path, "-c:v", "copy", "-c:a", "copy", video_with_audio_path, ] with open("ffmpeg.log", 'w') as f: process = subprocess.Popen(command, stderr=f) print(f"{toBLUE(video_with_audio_path)} (With Sound) is created.")