Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve audio concat; cleanup temp files; add -T(temp_dir) and -P (video_pool_size) args #1

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 37 additions & 24 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from multiprocessing import Pool
from pathlib import Path
from tempfile import NamedTemporaryFile, TemporaryDirectory
from typing import List
from typing import List, Tuple

import ffmpeg
from pydub import AudioSegment, silence
Expand Down Expand Up @@ -76,11 +76,11 @@ def concatenate_videos(temp_dir, chunks: List[str], outfile: str):
subprocess.check_output(concat_args)


def trim_silence(infile: str, outfile: str, min_silence_len: int, silence_thresh: int, margin: int) -> bool:
def trim_silence(infile: str, min_silence_len: int, silence_thresh: int, margin: int) -> Tuple[str, str, List[Tuple[int, int]]]:
"""
Returns
-------
True trimming was successful and resulted in nonempty outfile
Video output, audio output and a list of nonsilent intervals in form of (start_ms, stop_ms)
"""
infile_video = infile
infile_audio = str(Path(infile_video).with_suffix('.mp3'))
Expand All @@ -103,7 +103,7 @@ def trim_silence(infile: str, outfile: str, min_silence_len: int, silence_thresh
logging.info(parts)

if not parts:
return False
return infile_video, infile_audio, []

logging.info("Trimming audio")
segments = AudioSegment._sync(*[audio[start:stop] for start, stop in parts])
Expand All @@ -112,32 +112,36 @@ def trim_silence(infile: str, outfile: str, min_silence_len: int, silence_thresh

logging.info(f"Writing trimmed audio into {infile_audio} with duration {trimmed_audio.duration_seconds} s.")
trimmed_audio.export(infile_audio)
return infile_video, infile_audio, parts


def process_chunk(workdir: str, args: argparse.Namespace, total_segments: int, id_: int, segment: str):
logging.info(f"Processing chunk {id_ + 1}/{total_segments}: {segment} -> {Path(segment).with_suffix('.mp3')}")
res = trim_silence(segment,
args.min_silence_len,
args.silence_thresh,
args.margin)
logging.info(f"Done processing audio chunk {id_ + 1}/{total_segments}")
return res


def trim_video(infile_video: str, infile_audio: str, parts: List[Tuple[int, int]]) -> str:
in_path = Path(infile_video)
outfile = str(in_path.with_stem(in_path.stem + '_cropped'))
parts = [(start / 1000, stop / 1000) for (start, stop) in parts]

in_file = ffmpeg.input(infile_video)

joined = ffmpeg.concat(
ffmpeg.concat(
*[in_file.trim(start=start, end=stop).setpts('PTS-STARTPTS')
for start, stop in parts]),
ffmpeg.input(infile_audio),
v=1,
a=1).node
ffmpeg.output(joined[0], joined[1], outfile).run(quiet=True, overwrite_output=True)
return True


def process_chunk(workdir: str, args: argparse.Namespace, total_segments: int, id_: int, segment: str):
outfile = os.path.join(workdir, BASENAME + f"_cropped_{id_}" + Path(args.infile).suffix)
logging.info(f"Processing chunk {id_ + 1}/{total_segments}: {segment} -> {outfile}")
if not trim_silence(segment,
outfile,
args.min_silence_len,
args.silence_thresh,
args.margin):
outfile = None
logging.info(f"Done processing chunk {id_ + 1}/{total_segments}")
logging.info(f'Trimming {infile_video} -> {outfile}')
ffmpeg.output(joined[0], joined[1], outfile).run(quiet=True, overwrite_output=True)
logging.info(f'Done trimming {infile_video}')
return outfile


Expand All @@ -153,8 +157,12 @@ def main():
help='margin (ms)', default=100)
parser.add_argument('-n', dest='n_segments', type=int,
help='number of chunks to split input file to be processed independently', default=10)
parser.add_argument('-p', dest='pool_size', type=int,
help='number of chunks to be processed concurrently', default=1)
parser.add_argument('-p', dest='audio_pool_size', type=int,
help='number of audio chunks to be processed concurrently', default=1)
parser.add_argument('-P', dest='video_pool_size', type=int,
help='number of video chunks to be processed concurrently. '
'FFmpeg (without hwaccel) uses optimal number of threads by default, '
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Проверял с ffmpeg 4.4.2-0ubuntu0.22.04.1, количество используемых потоков равно кол-ву физических ядер процессора.
Запуск нескольких одновременно выполняющихся процессов ffmpeg не улучшает производительность, а наоборот, немного ухудшает её.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*Относится только к предпоследней стадии - trim+concat для каждого чанка.
Для разделения видео и аудио потоков и обработки аудио в каждом чанке используется 1 поток, поэтому на этих этапах нет проблем с параллельным исполнением.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Спасибо
Тоже хотел протестить, но никак не доходили руки
Кажется, когда добавлял эту опцию, тестил и использование многопоточности было оправданным
Сам ещё побенчмаркаю

ffmpeg по дефолту запускается с опцией threads=-1, поэтому звучит правдоподобно, что процесс будет занимать все ядра и будет конкурировать с другими

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vvd170501, а какой порядок улучшений времени? Остались результаты замеров?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Замерил на 30 минутах лекции в качестве 1080p60.
CPU - Ryzen 7 5800HS (8 физических ядер).
Разбиение на чанки и обработка аудио во всех случаях занимали ~8 секунд.

С одним процессом ffmpeg (5a04dcc):

$ time ./main.py -s 150 -m 50 -n 8 -p 8 -T /tmp/ramdisk/ -i ls4_30min.mkv -o lec4_trimmed.mkv
real	6m10,282s
user	50m56,566s
sys	0m46,125s

Во время выполнения trim+concat каждое из 16 логических ядер используется на ~50%, ffmpeg использует ~1 ГБ памяти, load average (1min) ~=9.

С 8 параллельными процессами ffmpeg (c9bc294):

$ time ./main.py -s 150 -m 50 -n 8 -p 8 -T /tmp/ramdisk/ -i lec4_30min.mkv -o lec4_trimmed.mkv
real	7m25,505s
user	110m17,434s
sys	1m48,683s

Все логические ядра загружены на 100%, используется 8 ГБ памяти, load average (1min) ~=70.

C ускорением на RTX 3060 mobile (мб попробую сделать PR, но нужно как-то добавить проверку на доступность nvenc):

$ time ./main.py -s 150 -m 50 -n 8 -p 8 -P 2 -T /tmp/ramdisk/ -i lec4_30min.mkv -o lec4_trimmed.mkv
real	2m22,735s
user	2m0,605s
sys	0m8,618s

'so values greater than 1 usually won\'t improve performance', default=1)
parser.add_argument('-T', dest='temp_dir',
help='directory for temporary files')
parser.add_argument('-d', dest='log_level', action='store_const', const=logging.DEBUG, help='print debugging info',
Expand All @@ -165,11 +173,16 @@ def main():

with TemporaryDirectory(dir=args.temp_dir) as workdir:
segments = split_video(args.infile, workdir, BASENAME, args.n_segments)
cropped_segments = []
with Pool(processes=args.pool_size) as pool:

with Pool(processes=args.audio_pool_size) as pool:
cropped_segments = pool.starmap(functools.partial(process_chunk, workdir, args, len(segments)), enumerate(segments))
cropped_segments = [segment for segment in cropped_segments if segment is not None]
concatenate_videos(args.temp_dir, cropped_segments, args.outfile)
cropped_segments = [(video, audio, parts) for video, audio, parts in cropped_segments if parts]

logging.info('Trimming video chunks')
with Pool(processes=args.video_pool_size) as pool:
resulting_segments = pool.starmap(trim_video, cropped_segments)

concatenate_videos(args.temp_dir, resulting_segments, args.outfile)


if __name__ == '__main__':
Expand Down