initial commit

This commit is contained in:
2024-12-29 21:27:11 +01:00
commit 5da81e2ee6
19 changed files with 733 additions and 0 deletions
View File
Binary file not shown.
Binary file not shown.
+72
View File
@@ -0,0 +1,72 @@
import os
import random
from pydub import AudioSegment
from pydub.silence import split_on_silence
def split_audio(audio_file_path: str,
chunk_folder: str,
max_num_chunks: int,
min_silence_level: int,
min_silence_length: int,
keep_silence: int) -> list:
"""
Split the audio file into chunks.
:param audio_file_path: path to the audio file
:param chunk_folder: path to the chunk folder
:param max_num_chunks: number of chunks to split the audio file into
:param min_silence_level: minimum silence level
:param min_silence_length: minimum silence length
:param keep_silence: whether to keep silence
:return: list of chunked audio file paths
"""
# create the chunk folder
if not os.path.exists(chunk_folder):
os.makedirs(chunk_folder)
# load audio file
audio = AudioSegment.from_file(audio_file_path)
audio_name = os.path.basename(audio_file_path).split('.')[0]
# split audio file into chunks
chunks = split_on_silence(audio,
silence_thresh=min_silence_level,
min_silence_len=min_silence_length,
keep_silence=keep_silence)
if len(chunks) > max_num_chunks:
# randomly combine chunks
while len(chunks) > max_num_chunks:
random_chunk_index = random.randint(0, len(chunks) - 2)
chunks[random_chunk_index] += chunks[random_chunk_index + 1]
chunks.pop(random_chunk_index + 1)
# save chunks
chunked_audio_files = []
for i, chunk in enumerate(chunks):
chunk_file_path = os.path.join(chunk_folder, f'{audio_name}_{i}.wav')
chunk.export(chunk_file_path, format='wav')
chunked_audio_files.append(chunk_file_path)
return chunked_audio_files
def convert_to_wav(audio_file_path: str,
output_file_path: str) -> str:
"""
Convert an audio file to wav format.
:param audio_file_path: path to the audio file
:param output_folder_path: path to the output folder
:return: path to the converted audio file
"""
input_audio = AudioSegment.from_file(audio_file_path)
if ".wav" in output_file_path:
new_audio_file_path = output_file_path
else:
new_audio_file_path = output_file_path + ".wav"
input_audio.export(new_audio_file_path, format='wav')
return new_audio_file_path
View File
+202
View File
@@ -0,0 +1,202 @@
import os
import shutil
from multiprocessing import Lock
# multiprocessing lock for thread safety
lock = Lock()
chunk_input_subdir = 'input_chunks'
chunk_output_subdir = 'output_chunks'
def get_job_data(job_name: str, config: dict) -> dict:
"""
Get job data from the database.
:param job_name: name of the job
:param config: app configuration
:return: job data
"""
job_dir = os.path.join(config['jobs']['root_directory'], job_name)
if not os.path.exists(job_dir):
raise ValueError(f'Job {job_name} does not exist')
job_data = dict()
job_data['name'] = job_name
output_file = os.path.join(job_dir, 'output.txt')
if os.path.exists(output_file):
with open(output_file, 'r') as f:
job_data['output'] = f.read()
job_data["completed"] = True
else:
job_data['output'] = None
job_data["completed"] = False
processing_file = os.path.join(job_dir, '_PROCESSING')
if os.path.exists(processing_file):
job_data["processing"] = True
else:
job_data["processing"] = False
# get progress of task
job_data["progress"] = get_progress(job_name, config)
return job_data
def get_existing_jobs(config: dict) -> list:
"""
Get existing jobs from the database.
:param config: app configuration
:return: list of jobs
"""
job_root_dir = config['jobs']['root_directory']
if not os.path.exists(job_root_dir):
return []
jobs = list()
for folder in os.listdir(job_root_dir):
job_data = get_job_data(folder, config)
jobs.append(job_data)
return jobs
def is_job(job_name: str, config: dict) -> bool:
"""
Check if a job exists.
:param job_name: name of the job
:param config: app configuration
:return: True if the job exists, False otherwise
"""
try:
get_job_data(job_name, config)
return True
except ValueError:
return False
def create_job(job_name: str, config: dict) -> None:
"""
Create a job in the file system.
:param job_name: name of the job
:param config: app configuration
"""
# check, if job root directory exists
if not os.path.exists(config['jobs']['root_directory']):
os.makedirs(config['jobs']['root_directory'])
job_dir = os.path.join(config['jobs']['root_directory'], job_name)
if os.path.exists(job_dir):
raise ValueError(f'Job {job_name} already exists')
lock.acquire()
os.mkdir(job_dir)
lock.release()
def rename_job(job_name: str, new_job_name: str, config: dict) -> None:
"""
Rename a job in the file system.
:param job_name: name of the job
:param new_job_name: new name of the job
:param config: app configuration
"""
job_dir = os.path.join(config['jobs']['root_directory'], job_name)
new_job_dir = os.path.join(config['jobs']['root_directory'], new_job_name)
if not os.path.exists(job_dir):
raise ValueError(f'Job {job_name} does not exist')
if os.path.exists(new_job_dir):
raise ValueError(f'Job {new_job_name} already exists')
lock.acquire()
os.rename(job_dir, new_job_dir)
lock.release()
def remove_job(job_name: str, config: dict) -> None:
"""
Remove a job from the file system.
:param job_name: name of the job
:param config: app configuration
"""
job_dir = os.path.join(config['jobs']['root_directory'], job_name)
if not os.path.exists(job_dir):
raise ValueError(f'Job {job_name} does not exist')
lock.acquire()
# remove the directory
shutil.rmtree(job_dir)
lock.release()
def get_processing(job_name: str, config: dict) -> bool:
"""
Get the processing status of a job.
:param job_name: name of the job
:param config: app configuration
:return: True if the job is processing, False otherwise
"""
job_dir = os.path.join(config['jobs']['root_directory'], job_name)
processing_file = os.path.join(job_dir, '_PROCESSING')
return os.path.exists(processing_file)
def set_processing(job_name: str, config: dict, processing: bool) -> None:
"""
Set the processing status of a job.
:param job_name: name of the job
:param config: app configuration
:param processing: processing status
"""
lock.acquire()
job_dir = os.path.join(config['jobs']['root_directory'], job_name)
processing_file = os.path.join(job_dir, '_PROCESSING')
if processing:
open(processing_file, 'w').close()
else:
os.remove(processing_file)
lock.release()
def get_progress(job_name: str, config: dict) -> int:
"""
Get the progress of a job.
:param job_name: name of the job
:param config: app configuration
:return: progress of the job
"""
input_dir = os.path.join(config['jobs']['root_directory'], job_name, chunk_input_subdir)
output_dir = os.path.join(config['jobs']['root_directory'], job_name, chunk_output_subdir)
num_input_files = len(os.listdir(input_dir))
num_output_files = len(os.listdir(output_dir))
if num_input_files == 0 or num_output_files == 0:
return 0
if num_output_files >= num_input_files:
return 100
return int(num_output_files / num_input_files * 100)
+57
View File
@@ -0,0 +1,57 @@
import whisper
# clojure for model
def model_loader() -> callable:
"""
Clojure for loading the model
:return: function that loads the model
"""
model = None
def load_model(config: dict):
"""
Load the model
:return: model
"""
nonlocal model
if model is not None:
return model
else:
model_type = config['model']['type']
model = whisper.load_model(model_type)
return model
return load_model
load_model = model_loader()
def transcribe_audio(audio_file_path: str,
output_file_path: str,
config: dict) -> str:
"""
Transcribe an audio file with openai whisper
:param audio_file_path: filepath of the audio file
:param output_file_path: filepath of the output file
:param config: app config
:return: transcribed text
"""
# load the model
model = load_model(config)
# move model to desired device
desired_device = config['model']['device']
model.to(desired_device)
result = model.transcribe(audio_file_path)
result_text = result['text']
# write the result to a text file
with open(output_file_path, 'w') as result_file:
result_file.write(result["text"])
return result_text
+155
View File
@@ -0,0 +1,155 @@
import concurrent.futures
import os
from multiprocessing import Lock
from concurrent.futures import ProcessPoolExecutor
import job_utils
from utils import get_logger
from audio_processing import split_audio, convert_to_wav
from job_utils import is_job, remove_job, create_job, get_processing, set_processing
from transcription import transcribe_audio
job_update_lock = Lock()
default_input_audio_file_name = 'input_audio'
logger = get_logger(__name__)
# define a global executor for non-blocking transcription jobs
executor = ProcessPoolExecutor()
def transcribe(audio_file_path: str,
config: dict,
blocking: bool = True,
overwrite: bool = False,
executor: concurrent.futures.Executor = executor) -> str:
"""
Transcribe an audio file.
:param audio_file_path: path to the audio file
:param config: app configuration
:param blocking: block until the job is finished
:param overwrite: overwrite existing job
:param executor: executor for non-blocking transcription
:return: name of the created job
"""
job_name = os.path.basename(audio_file_path).split('.')[0]
job_dir = os.path.join(config['jobs']['root_directory'], job_name)
logger.info(f"Transcribing {audio_file_path} to {job_dir}")
# check, if a job with the same audio file already exists
job_already_exists = is_job(job_name, config)
if job_already_exists and not overwrite:
logger.error(f'Job for {audio_file_path} already exists and overwrite is not set')
raise ValueError(f'Job for {audio_file_path} already exists')
# remove, if overwrite is set
if job_already_exists and overwrite:
logger.info(f'Removing existing job {job_name} since overwrite is set')
remove_job(job_name, config)
# create a new base job
logger.info(f'Creating job {job_name}')
create_job(job_name, config)
# copy the audio file to the job directory
logger.info(f'Copying audio file {audio_file_path} to {job_dir}')
input_audio_file_name = os.path.basename(audio_file_path).split('.')[0]
input_audio_file_type = os.path.splitext(audio_file_path)[1]
audio_file_destination = os.path.join(job_dir, f"{input_audio_file_name}{input_audio_file_type}")
os.system(f'cp {audio_file_path} {audio_file_destination}')
# convert the audio file to wav, if it is not already
if not audio_file_destination.endswith('.wav'):
logger.info(f'Converting audio file {audio_file_destination} to wav')
convert_to_wav(audio_file_path,
os.path.join(job_dir, f"{default_input_audio_file_name}.wav"))
# run the transcription job
logger.info(f'Running transcription job {job_name}')
if blocking:
logger.info(f'Starting blocking transcription job {job_name}')
run_transcription_job(job_name, config)
else:
logger.info(f'Starting async transcription job {job_name}')
executor.submit(run_transcription_job, job_name, config, 100)
logger.info(f'Started async transcription job {job_name}')
return job_name
def run_transcription_job(job_name: str,
config: dict,
num_chunks=100) -> None:
"""
Start a transcription job.
:param job_name: id of the job
:param config: app configuration
:param num_chunks: number of chunks to split the audio file into
"""
# check if the job is already running
if get_processing(job_name, config):
raise ValueError(f'Job {job_name} is already running')
# set the job to processing
set_processing(job_name, config, processing=True)
try:
# split the audio file into chunks
logger.info(f'Splitting audio file into chunks for job {job_name}')
audio_file_path = os.path.join(config['jobs']['root_directory'], job_name,
f"{default_input_audio_file_name}.wav")
audio_chunk_folder = os.path.join(config['jobs']['root_directory'], job_name, job_utils.chunk_input_subdir)
audio_chunks = split_audio(audio_file_path,
audio_chunk_folder,
num_chunks,
config["audio"]["chunking"]["min_silence_level"],
config["audio"]["chunking"]["min_silence_length"],
config["audio"]["chunking"]["ms_silence_to_keep"])
# transcribe the audio chunks
logger.info(f'Transcribing {len(audio_chunks)} audio chunks for job {job_name}')
# create chunk output folder, if not done yet
chunk_output_folder = os.path.join(config['jobs']['root_directory'], job_name, job_utils.chunk_output_subdir)
if not os.path.exists(chunk_output_folder):
os.makedirs(chunk_output_folder)
for i, audio_chunk in enumerate(audio_chunks):
# transcribe the audio
logger.info(f'Transcribing audio chunk {i + 1} of {len(audio_chunks)} for job {job_name}')
output_file_path = os.path.join(chunk_output_folder, f'output_{i}.txt')
transcribe_audio(audio_chunk,
output_file_path,
config)
# combine the output text chunks
logger.info(f'Combining output text chunks for job {job_name}')
output_chunk_folder = os.path.join(config['jobs']['root_directory'], job_name, job_utils.chunk_output_subdir)
output_text = ''
for text_chunk_file in sorted(os.listdir(output_chunk_folder)):
with open(os.path.join(output_chunk_folder, text_chunk_file), 'r') as text_chunk:
output_text += text_chunk.read() + ' '
# remove trailing space
output_text = output_text.strip()
# write the output to a file
logger.info(f'Writing output text to file for job {job_name}')
output_file = os.path.join(config['jobs']['root_directory'], job_name, 'output.txt')
with open(output_file, 'w') as f:
f.write(output_text)
# set the job to not processing
set_processing(job_name, config, processing=False)
logger.info(f'Job {job_name} completed')
except Exception as e:
# set the job to not processing
set_processing(job_name, config, processing=False)
raise e
+32
View File
@@ -0,0 +1,32 @@
import sys
import logging
from logging import Logger
def get_logger(name: str) -> Logger:
"""
Get a logger instance.
:param name: name of the logger
:return: logger instance
"""
logger = logging.getLogger(name)
logger.setLevel(logging.INFO)
# create a file handler
file_handler = logging.FileHandler(f'{name}.log')
file_handler.setLevel(logging.INFO)
# create stdout handler
stdout_handler = logging.StreamHandler(sys.stdout)
stdout_handler.setLevel(logging.INFO)
# create a logging format
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)
stdout_handler.setFormatter(formatter)
# add the handlers to the logger
logger.addHandler(file_handler)
logger.addHandler(stdout_handler)
return logger