diff --git a/.gitignore b/.gitignore index c13799d..9f0633b 100644 --- a/.gitignore +++ b/.gitignore @@ -161,5 +161,10 @@ cython_debug/ .idea/ -removing_music.log +./removing_music.log +/input/* +!/input/.gitkeep + +/output/* +!/output/.gitkeep diff --git a/removing_music.log b/input/.gitkeep similarity index 100% rename from removing_music.log rename to input/.gitkeep diff --git a/main.py b/main.py index 9009f4d..09be18f 100644 --- a/main.py +++ b/main.py @@ -4,69 +4,78 @@ from itertools import chain from pathlib import Path +logging.basicConfig( + encoding='utf-8', + format='%(asctime)s - %(levelname)s - %(message)s', + level=logging.INFO, + handlers=[ + logging.FileHandler("removing_music.log", encoding='utf-8'), + logging.StreamHandler(sys.stdout) + ] +) -def process_file(original_video: Path): + +def cleanup(no_music_sound: Path, original_video: Path) -> None: """ - removes music from videos in input folder - :return: True if there's no file to process, which marks the end of the mass process + delete Intermediate audio files and original video """ - try: - logging.info(f'Processing file "{original_video.name}"') + logging.info(f'"{original_video.name}": deleting original video...') + original_video.unlink() + logging.info(f'"{original_video.name}": original video deleted successfully') + logging.info(f'"{original_video.name}": deleting vocal sound...') + for file in [file for file in no_music_sound.parent.iterdir()]: + file.unlink() + no_music_sound.parent.rmdir() + logging.info(f'"{original_video.name}": vocal sound deleted successfully') - no_music_sound: Path = Path(f'separated/htdemucs/{original_video.stem}/vocals.mp3') - if no_music_sound.exists(): - logging.info(f'"{original_video.name}": vocal already separated, skipping separating vocal') - else: - logging.info(f'"{original_video.name}": start separating vocal...') - # TODO: export vocal sound as `wav` extension - remove_music_command: list[str] = ['pipenv', 'run', 'demucs', '--mp3', '--two-stems=vocals', - original_video.absolute()] - completed_process = subprocess.run(remove_music_command, encoding='utf-8', text=True, - capture_output=True, - check=True) - # raise exception if vocal sound is not created, exception raised manually - # because demucs command doesn't return error code - if not no_music_sound.exists(): - raise subprocess.CalledProcessError(returncode=1, - cmd=completed_process.args, - output=completed_process.stdout, - stderr=completed_process.stderr) - logging.info(f'"{original_video.name}": vocal seperated successfully') - # 3- replace the sound of the video with the no music version, - # and save the new video in folder 'output' - # if not already done - no_music_video: Path = Path(f'output/{original_video.name}') - if no_music_video.exists(): - logging.info( - f'"{original_video.name}": a video with no music already exists, skipping creating a new video') - else: - logging.info(f'"{original_video.name}": creating a new video with no music...') - create_no_music_video_command: list[str] = ['ffmpeg', '-i', original_video.absolute(), - '-i', no_music_sound.absolute(), - '-c:v', 'copy', '-map', '0:v:0', '-map', '1:a:0', - no_music_video.absolute()] - subprocess.run(create_no_music_video_command, encoding='utf-8', text=True, capture_output=True, check=True) - logging.info(f'"{original_video.name}": a new video with no music has been created') - # clean up - logging.info(f'"{original_video.name}": deleting original video...') - original_video.unlink() - logging.info(f'"{original_video.name}": original video deleted successfully') - logging.info(f'"{original_video.name}": deleting vocal sound...') - for file in [file for file in no_music_sound.parent.iterdir()]: - file.unlink() - no_music_sound.parent.rmdir() - logging.info(f'"{original_video.name}": vocal sound deleted successfully') - except subprocess.CalledProcessError as error: - logging.exception(error.stderr) - raise error - except KeyboardInterrupt as error: - logging.exception('the process was stopped by user') - raise error - except BaseException as error: - logging.exception('Unexpected error happened') - raise error + +def create_video_without_music(no_music_sound: Path, original_video: Path) -> Path: + """ + replace the sound of the video with the no music version, + and save the new video in folder 'output' + if not already done + """ + no_music_video: Path = Path(f'output/{original_video.name}') + if no_music_video.exists(): + logging.info( + f'"{original_video.name}": a video with no music already exists, skipping creating a new video') else: - logging.info(f'"{original_video.name}": Processing finished') + logging.info(f'"{original_video.name}": creating a new video with no music...') + create_no_music_video_command: list[str] = ['ffmpeg', '-i', original_video.absolute(), + '-i', no_music_sound.absolute(), + '-c:v', 'copy', '-map', '0:v:0', '-map', '1:a:0', + no_music_video.absolute()] + subprocess.run(create_no_music_video_command, encoding='utf-8', text=True, capture_output=True, check=True) + logging.info(f'"{original_video.name}": a new video with no music has been created') + return no_music_video + + +def separate_vocal(original_video: Path) -> Path: + """ + separate vocal from music using demucs machine learning algorithm + + :exception subprocess.CalledProcessError + """ + no_music_sound: Path = Path(f'separated/htdemucs/{original_video.stem}/vocals.mp3') + if no_music_sound.exists(): + logging.info(f'"{original_video.name}": vocal already separated, skipping separating vocal') + else: + logging.info(f'"{original_video.name}": start separating vocal...') + # TODO: export vocal sound as `wav` extension + remove_music_command: list[str] = ['pipenv', 'run', 'demucs', '--mp3', '--two-stems=vocals', + original_video.absolute()] + completed_process = subprocess.run(remove_music_command, encoding='utf-8', text=True, + capture_output=True, + check=True) + # raise exception if vocal sound is not created, exception raised manually + # because demucs command doesn't return error code + if not no_music_sound.exists(): + raise subprocess.CalledProcessError(returncode=1, + cmd=completed_process.args, + output=completed_process.stdout, + stderr=completed_process.stderr) + logging.info(f'"{original_video.name}": vocal seperated successfully') + return no_music_sound def get_original_video() -> Path | None: @@ -79,20 +88,14 @@ def get_original_video() -> Path | None: def main() -> None: - logging.basicConfig( - encoding='utf-8', - format='%(asctime)s - %(levelname)s - %(message)s', - level=logging.INFO, - handlers=[ - logging.FileHandler("removing_music.log", encoding='utf-8'), - logging.StreamHandler(sys.stdout) - ] - ) - logging.info('Mass processing started') while original_video := get_original_video(): - process_file(original_video) + logging.info(f'Processing file "{original_video.name}"') + no_music_sound: Path = separate_vocal(original_video) + create_video_without_music(no_music_sound, original_video) + cleanup(no_music_sound, original_video) + logging.info(f'"{original_video.name}": Processing finished') else: logging.info("There's no file to process") diff --git a/output/.gitkeep b/output/.gitkeep new file mode 100644 index 0000000..e69de29