transcribe-matrix-live

A fork for matrix-live transcription@home by fyyd.de
git clone git://archive.git.mtrnord.blog/MTRNord/transcribe-matrix-live.git
Log | Files | Refs | README

commit 0e243c183f2106756db221202ee0253140effe76
parent 304726c1e5ea2346d699628fe4ac6dfd2fbd87d3
Author: MTRNord <mtrnord1@gmail.com>
Date:   Thu, 22 Feb 2024 18:30:05 +0100

Move to py script

Diffstat:
M.gitignore | 1+
A.vscode/settings.json | 3+++
AInter-Regular.otf | 0
Atranscribe.py | 366+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtranscribe.sh | 55++++++++++++++++++++++++++++++-------------------------
5 files changed, 400 insertions(+), 25 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -2,3 +2,4 @@ whisper.cpp/ trancribe.cfg test/ testing/ +audio_lengths_histogram.png diff --git a/.vscode/settings.json b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.analysis.autoImportCompletions": true +} diff --git a/Inter-Regular.otf b/Inter-Regular.otf Binary files differ. diff --git a/transcribe.py b/transcribe.py @@ -0,0 +1,366 @@ +#!/usr/bin/env python3 + +import argparse +import logging +import os +import shutil +import signal +import subprocess +from pathlib import Path +from typing import Any, Dict, List + +import librosa +import matplotlib +import matplotlib.pyplot as plt +import tqdm +import yt_dlp +from ffmpeg_normalize import FFmpegNormalize +from matplotlib import font_manager + +# Configure logging +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" +) + +# Define constants +CONFIG_FILENAMES: List[str] = ["transcribe.cfg", "trancribe.cfg"] +DEFAULT_THREADS: int = 1 +DEFAULT_MODEL: str = "medium" +DEFAULT_PLAYLIST_URL: str = "https://www.youtube.com/@Matrixdotorg" +SCRIPT_DIR: Path = Path(__file__).resolve().parent + + +# Define the function to handle Ctrl+C +def ctrl_c(sig: int, frame: Any) -> None: + logging.info("------------------------------") + logging.info("STOPPING...") + logging.info("bye bye") + logging.info("------------------------------") + exit(0) + + +# Set the signal handler +signal.signal(signal.SIGINT, ctrl_c) + + +def load_configuration(config_filenames: List[str]) -> Dict[str, str]: + config: Dict[str, str] = {} + for filename in config_filenames: + if Path(filename).is_file(): + with open(filename, "r") as cfg_file: + for line in cfg_file: + if line.strip() and not line.startswith("#"): + key, value = line.strip().split("=") + config[key.strip()] = value.strip() + return config + + +def create_config_file() -> None: + threads: int = get_thread_count() + model: str = ( + input("Enter the model (Press Enter for default): ").strip() or DEFAULT_MODEL + ) + + # Save configuration + with open("transcribe.cfg", "w") as cfg_file: + cfg_file.write(f"THREADS={threads}\n") + cfg_file.write("PIDFILE=~/.trancribe-transcribe.pid\n") + cfg_file.write(f"MODEL={model}\n") + + +def check_dependencies() -> None: + dependencies: List[str] = ["ffmpeg", "git"] + for dependency in dependencies: + if shutil.which(dependency) is None: + logging.error(f"Can't find {dependency}. Please install.") + exit(1) + + +def setup_whisper() -> None: + whisper_dir: Path = SCRIPT_DIR / "whisper.cpp" + if not (whisper_dir / "whisper.cpp").exists(): + logging.info("Downloading whisper.cpp") + subprocess.run( + ["git", "clone", "https://github.com/ggerganov/whisper.cpp"], + cwd=SCRIPT_DIR, + check=True, + ) + logging.info("Compiling whisper...") + subprocess.run(["make", "-j", "WHISPER_CLBLAST=1"], cwd=whisper_dir, check=True) + logging.info("Downloading the model") + subprocess.run( + ["./models/download-ggml-model.sh", "medium"], cwd=whisper_dir, check=True + ) + else: + os.chdir(whisper_dir) + + +def get_thread_count() -> int: + threads: int = 0 + try: + threads = os.cpu_count() or 0 + except Exception as e: + logging.error(f"Error getting number of threads: {str(e)}") + + if threads == 0: + logging.warn("Could not find the number of threads.") + else: + logging.info(f"Maximum of {threads} threads found.") + + input_threads: str = input( + "How many threads to use for transcription? (Press Enter for default): " + ) + + if input_threads.strip(): + try: + threads = int(input_threads) + except ValueError: + logging.error("Invalid input. Using default.") + return threads + + +def run_transcription_test(config: Dict[str, str]) -> None: + threads: int = int(config.get("THREADS", DEFAULT_THREADS)) + model: str = config.get("MODEL", DEFAULT_MODEL) + + logging.info("Starting test. This might take some minutes, please wait...") + + thread_opt: str = f"-t {threads}" if threads else "" + + try: + subprocess.run( + [ + "./main", + "-m", + f"models/ggml-{model}.bin", + thread_opt, + "-l", + "de", + "-di", + "../test.wav", + ], + check=True, + ) + except subprocess.CalledProcessError: + logging.error("Error transcribing. Stopping.") + + +def download_audio_files(playlist_url: str, output_directory: str) -> None: + # Download audio files from YouTube playlist + ydl_opts: yt_dlp.YDLOpts = { + "format": "bestaudio/best", + "outtmpl": f"{output_directory}/%(id)s.%(ext)s", + "postprocessors": [{"key": "FFmpegExtractAudio", "preferredcodec": "wav"}], + "concurrent_fragment_downloads": 3, + "download_archive": f"{output_directory}/downloaded.txt", + "live_from_start": True, + "extractor_args": {"youtube": {"player_client": "android"}}, + } # type: ignore + + with yt_dlp.YoutubeDL(ydl_opts) as ydl: + ydl.download([playlist_url]) + + +def normalize_audio_files(input_directory: str, output_directory: str) -> None: + # Normalize audio files using FFmpegNormalize + files: List[str] = [ + file for file in os.listdir(input_directory) if file.endswith(".wav") + ] + files_in: List[str] = [f"{input_directory}/{file}" for file in files] + files_out: List[str] = [f"{output_directory}/{file}" for file in files] + + normalizer = FFmpegNormalize(progress=True) + for input_file, output_file in zip(files_in, files_out): + normalizer.add_media_file(input_file, output_file) + normalizer.run_normalization() + + +def generate_graph(audio_directory: str) -> None: + logging.info("Generating graph over audio files") + audio_files: List[str] = [ + file for file in os.listdir(audio_directory) if file.endswith(".wav") + ] + + # Get lengths of audio files + audio_lengths: List[float] = [] + progress_bar = tqdm.tqdm(total=len(audio_files), desc="Processing audio files") + + for file in audio_files: + audio_path = os.path.join(audio_directory, file) + duration = librosa.get_duration(path=audio_path) + audio_lengths.append(duration) + progress_bar.update(1) + + progress_bar.close() + + # Create histogram with specified bins + plt.figure(figsize=(15, 8)) + counts, bins, bars = plt.hist( + audio_lengths, + bins=50, + color="skyblue", + rwidth=0.7, + ) + + plt.xlabel( + "Audio Length", + labelpad=15, + color="#333333", + fontname="Inter", + ) + plt.ylabel( + "Frequency", + labelpad=15, + color="#333333", + fontname="Inter", + ) + plt.title( + "Distribution of Audio Lengths", + pad=15, + color="#333333", + weight="bold", + fontname="Inter", + ) + + # Format x-axis ticks for better readability + formatter = matplotlib.ticker.FuncFormatter( # type: ignore + lambda x, _: "{:.0f}h {:.0f}m".format(x // 3600, (x % 3600) // 60) + ) + plt.gca().xaxis.set_major_formatter(formatter) + plt.gca().set_xticks(bins) + plt.gca().set_xticklabels(plt.gca().get_xticklabels(), rotation=45, ha="right") + plt.gca().spines["top"].set_visible(False) + plt.gca().spines["right"].set_visible(False) + plt.gca().spines["left"].set_visible(False) + plt.gca().spines["bottom"].set_color("#DDDDDD") + plt.gca().tick_params(bottom=True, left=False) + plt.gca().set_axisbelow(True) + plt.gca().yaxis.grid(True, color="#EEEEEE") + plt.gca().xaxis.grid(False) + + plt.tight_layout() + + # Save the histogram as an image file + plt.savefig("../audio_lengths_histogram.png") + + # Close the plot to release resources + plt.close() + + +def transcribe_audio_files(files_directory: str, model: str, threads: int) -> None: + files: List[str] = [ + file for file in os.listdir(files_directory) if file.endswith(".wav") + ] + + for file in files: + input_file: str = f"{files_directory}/{file}" + output_file: str = f"output/{os.path.splitext(file)[0]}.txt" + + whisper_cmd: List[str] = [ + "./main", + "-m", + f"models/ggml-{model}.bin", + "-t", + str(threads), + "-l", + "en", + "-otxt", + "-ovtt", + "-pc", + "--file", + input_file, + "--output-file", + output_file, + "-et", + "3.0", + ] + + whisper_process: subprocess.CompletedProcess = subprocess.run( + whisper_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + if whisper_process.returncode != 0: + logging.error("Error transcribing") + else: + logging.info("Transcription successful") + + +def backup_files( + input_directory: str, normalized_directory: str, backup_directory: str +) -> None: + # Backup input files + input_files: List[str] = [ + file for file in os.listdir(input_directory) if file.endswith(".wav") + ] + for file in input_files: + input_file: str = f"{input_directory}/{file}" + shutil.move(input_file, f"{backup_directory}/input/{file}") + logging.info(f"Moved input file {file} to backup directory") + + # Backup normalized input files + normalized_files: List[str] = [ + file for file in os.listdir(normalized_directory) if file.endswith(".wav") + ] + for file in normalized_files: + normalized_file: str = f"{normalized_directory}/{file}" + shutil.move(normalized_file, f"{backup_directory}/normalized/{file}") + logging.info(f"Moved normalized file {file} to backup directory") + + +def run() -> None: + # Load configuration + config: Dict[str, str] = load_configuration(CONFIG_FILENAMES) + threads: int = int(config.get("THREADS", DEFAULT_THREADS)) + model: str = config.get("MODEL", DEFAULT_MODEL) + playlist_url: str = config.get("PLAYLIST_URL", DEFAULT_PLAYLIST_URL) + + # Set LC_NUMERIC + os.environ["LC_NUMERIC"] = "en_US.UTF-8" + + logging.info("Starting engines! Let's transcribe some episodes") + + # Change directory to whisper.cpp + os.chdir("whisper.cpp") + + # Create necessary directories if they don't exist + os.makedirs("./playlist", exist_ok=True) + open("./playlist/downloaded.txt", "a").close() + os.makedirs("./playlist_normalized", exist_ok=True) + + download_audio_files(playlist_url, "./playlist") + + generate_graph("./playlist") + + # Normalize audio files + normalize_audio_files("./playlist", "./playlist_normalized") + + # Transcribe audio files + transcribe_audio_files("./playlist_normalized", model, threads) + + # Cleanup + backup_files("./playlist", "./output", "./backup") + + logging.info("Transcription completed!") + + # Change back to the original directory + os.chdir("..") + + +def main() -> None: + parser: argparse.ArgumentParser = argparse.ArgumentParser( + description="Transcription script" + ) + parser.add_argument("--setup", help="Mode of operation") + args: argparse.Namespace = parser.parse_args() + + if args.setup: + create_config_file() + config: Dict[str, str] = load_configuration(CONFIG_FILENAMES) + check_dependencies() + setup_whisper() + run_transcription_test(config) + else: + run() + + +if __name__ == "__main__": + main() diff --git a/transcribe.sh b/transcribe.sh @@ -14,12 +14,11 @@ export LC_NUMERIC="en_US.UTF-8" trap ctrl_c INT function ctrl_c() { - echo "------------------------------" echo "STOPPING..." echo "bye bye" echo "------------------------------" - exit + exit } echo "Starting engines! Let's transcribe some episodes" @@ -70,7 +69,7 @@ do done files_out=$(echo "${files}" | sed -e 's/playlist/playlist_normalized/g') -[[ $files = *[!\ ]* ]] && python3 -m ffmpeg_normalize $files -p -o $files_out -ar 16000 +[[ $files = *[!\ ]* ]] && python3 -m ffmpeg_normalize ${files} -p -o ${files_out} -ar 16000 ## This logic is used to not process stuff twice #files_already_done=($(find . -wholename "./output/*.vtt" -type f | tr '\n' ' ' | sed -e 's/output/playlist_normalized/g' | sed -e 's/vtt/wav/g')) @@ -80,31 +79,37 @@ for already_done_file in "${files_already_done[@]}" do files=$(echo "${files}" | sed -e "s@${already_done_file}@@g") done -out_files=$(echo "${files}" | sed -e 's/playlist_normalized/output/g' | sed -e 's/.wav//g') - +out_files=($(echo "${files}" | sed -e 's/playlist_normalized/output/g' | sed -e 's/.wav//g')) + echo "starting whisper" if [[ $files = *[!\ ]* ]]; then - #if ! nice -n 18 ./main -m "models/ggml-${MODEL}.bin" -t "$THREADS" -l en -ovtt -pc "${files}.wav"; #>/dev/null 2>/dev/null; - if ! nice -n 18 ./main -m "models/ggml-${MODEL}.bin" -t "$THREADS" -l en -otxt -ovtt -pc --file $files --output-file $out_files -et 3.0; #>/dev/null 2>/dev/null; - then - echo "error transcribing" - fi - - echo "cleanup" - files_array=(./playlist_normalized/*.wav) - for next_file in "${files_array[@]}" - do - FILENAME=$(basename "${next_file}" ".wav") - # Removes some junk from hallucination - sed -i -e 's/ Subtitles by the Amara.org community//g' "./output/${FILENAME}.txt" - sed -i -e 's/Subtitles by the Amara.org community//g' "./output/${FILENAME}.vtt" - - mv "./playlist/${FILENAME}.wav" "./playlist_bak/${FILENAME}.wav" - rm "./playlist_normalized/${FILENAME}.wav" - #rm "./${FILENAME}.vtt" - mv "./${FILENAME}.wav.vtt" "./output/${FILENAME}.vtt" - mv "./${FILENAME}.wav.txt" "./output/${FILENAME}.txt" + array_files=(in cafiles) + for i in "${!array_files[@]}"; do + echo "Starting with ${array_files[$1]}" + #if ! nice -n 18 ./main -m "models/ggml-${MODEL}.bin" -t "$THREADS" -l en -ovtt -pc "${files}.wav"; #>/dev/null 2>/dev/null; + if ! nice -n 18 ./main -m "models/ggml-${MODEL}.bin" -t "$THREADS" -l en -otxt -ovtt -pc --file "${array_files[$i]}" --output-file "${out_files[$i]}" -et 3.0; #>/dev/null 2>/dev/null; + then + echo "error transcribing" + fi + + echo "cleanup" + files_array=(./playlist_normalized/*.wav) + for next_file in "${files_array[@]}" + do + FILENAME=$(basename "${next_file}" ".wav") + # Removes some junk from hallucination + sed -i -e 's/ Subtitles by the Amara.org community//g' "./output/${FILENAME}.txt" + sed -i -e 's/Subtitles by the Amara.org community//g' "./output/${FILENAME}.vtt" + + mkdir ./playlist_bak + mkdir ./playlist_normalized_bak + mv "./playlist/${FILENAME}.wav" "./playlist_bak/${FILENAME}.wav" + mv "./playlist_normalized/${FILENAME}.wav" "./playlist_normalized_bak/${FILENAME}.wav" + #rm "./${FILENAME}.vtt" + mv "./${FILENAME}.wav.vtt" "./output/${FILENAME}.vtt" + mv "./${FILENAME}.wav.txt" "./output/${FILENAME}.txt" + done done fi