diff --git a/environment.yml b/environment.yml index cbd2d15..5ed8d78 100644 --- a/environment.yml +++ b/environment.yml @@ -1,195 +1,16 @@ name: notecast channels: - - conda-forge - defaults - - nvidia - - pytorch + - conda-forge dependencies: - - accelerate=1.10.1=pyhcf101f3_0 - - aiohappyeyeballs=2.6.1=pyhd8ed1ab_0 - - aiohttp=3.12.15=pyh7db6752_0 - - aiosignal=1.4.0=pyhd8ed1ab_0 - - aom=3.6.0=hd77b12b_0 - - arrow-cpp=19.0.0=h82e8c66_4 - - async-timeout=5.0.1=pyhd8ed1ab_1 - - attrs=25.3.0=pyh71513ae_0 - - aws-c-auth=0.9.0=hd490b63_15 - - aws-c-cal=0.9.2=hd8a8e38_0 - - aws-c-common=0.12.3=h2466b09_0 - - aws-c-compression=0.3.1=h5d0e663_5 - - aws-c-event-stream=0.5.5=ha416645_0 - - aws-c-http=0.10.2=h81282ae_2 - - aws-c-io=0.20.1=hf7624bd_1 - - aws-c-mqtt=0.13.1=h5c1ae27_3 - - aws-c-s3=0.8.3=h1e843c7_0 - - aws-c-sdkutils=0.2.4=h5d0e663_0 - - aws-checksums=0.2.7=h5d0e663_1 - - aws-crt-cpp=0.32.10=h8abd1a4_2 - - aws-sdk-cpp=1.11.528=hd293286_1 - - blas=1.0=mkl - - bottleneck=1.4.2=py312h4b0e54e_0 - - brotlicffi=1.0.9.2=py312h5da7b33_1 - - bzip2=1.0.8=h2bbff1b_6 - - c-ares=1.34.5=h2466b09_0 - - ca-certificates=2025.8.3=h4c7d964_0 - - cairo=1.18.4=he9e932c_0 - - certifi=2025.8.3=py312haa95532_0 - - cffi=1.17.1=py312h827c3e9_1 - - charset-normalizer=3.3.2=pyhd3eb1b0_0 - - colorama=0.4.6=pyhd8ed1ab_1 - - cuda-cccl=13.0.50=h23517cc_1 - - cuda-cccl_win-64=13.0.50=hc667259_1 - - cuda-cudart=12.4.127=0 - - cuda-cudart-dev=12.4.127=0 - - cuda-cupti=12.4.127=0 - - cuda-libraries=12.4.1=0 - - cuda-libraries-dev=12.4.1=0 - - cuda-nvrtc=12.4.127=0 - - cuda-nvrtc-dev=12.4.127=0 - - cuda-nvtx=12.4.127=0 - - cuda-opencl=13.0.39=0 - - cuda-opencl-dev=13.0.39=0 - - cuda-profiler-api=13.0.39=0 - - cuda-runtime=12.4.1=0 - - cuda-version=13.0=3 - - datasets=4.0.0=pyhcf101f3_0 - - dav1d=1.2.1=h2bbff1b_0 - - dill=0.3.8=pyhd8ed1ab_0 - - expat=2.7.1=h8ddb27b_0 - - ffmpeg=4.3.1=ha925a31_0 - - filelock=3.17.0=py312haa95532_0 - - fontconfig=2.14.1=hb33846d_3 - - freeglut=3.4.0=h8a1e904_1 - - freetype=2.13.3=h0620614_0 - - fribidi=1.0.10=h62dcd97_0 - - frozenlist=1.7.0=pyhf298e5d_0 - - fsspec=2025.3.0=pyhd8ed1ab_0 - - gflags=2.2.2=he0c23c2_1005 - - giflib=5.2.2=h7edc060_0 - - glog=0.5.0=h4797de2_0 - - gmp=6.3.0=h537511b_0 - - gmpy2=2.2.1=py312h827c3e9_0 - - graphite2=1.3.14=hd77b12b_1 - - harfbuzz=10.2.0=he2f9f60_1 - - hf-xet=1.1.8=py312h79d111c_0 - - huggingface_hub=0.34.4=pyhd8ed1ab_1 - - icu=73.1=h6c2663c_0 - - idna=3.7=py312haa95532_0 - - intel-openmp=2023.1.0=h59b6b97_46320 - - jinja2=3.1.6=py312haa95532_0 - - jpeg=9e=h827c3e9_3 - - khronos-opencl-icd-loader=2024.05.08=h8cc25b3_0 - - lcms2=2.16=h62be587_1 - - lerc=4.0.0=h5da7b33_0 - - libabseil=20250127.0=cxx17_h4eb7d71_0 - - libavif=1.1.1=h827c3e9_0 - - libbrotlicommon=1.0.9=hcfcfb64_9 - - libbrotlidec=1.0.9=hcfcfb64_9 - - libbrotlienc=1.0.9=hcfcfb64_9 - - libcublas=12.4.5.8=0 - - libcublas-dev=12.4.5.8=0 - - libcufft=11.2.1.3=0 - - libcufft-dev=11.2.1.3=0 - - libcurand=10.4.0.35=0 - - libcurand-dev=10.4.0.35=0 - - libcurl=8.15.0=h2300eb9_0 - - libcusolver=11.6.1.9=0 - - libcusolver-dev=11.6.1.9=0 - - libcusparse=12.3.1.170=0 - - libcusparse-dev=12.3.1.170=0 - - libdeflate=1.22=h5bf469e_0 - - libffi=3.4.4=hd77b12b_1 - - libglib=2.84.2=h405b238_0 - - libgrpc=1.71.0=hf4237ab_0 - - libiconv=1.16=h2bbff1b_3 - - libjpeg-turbo=2.0.0=h196d8e1_0 - - libnpp=12.2.5.30=0 - - libnpp-dev=12.2.5.30=0 - - libnvfatbin=13.0.39=0 - - libnvfatbin-dev=13.0.39=0 - - libnvjitlink=12.4.127=0 - - libnvjitlink-dev=12.4.127=0 - - libnvjpeg=12.3.1.117=0 - - libnvjpeg-dev=12.3.1.117=0 - - libpng=1.6.39=h8cc25b3_0 - - libprotobuf=5.29.3=h65a231f_1 - - libre2-11=2024.07.02=hd248061_3 - - libssh2=1.11.1=h2addb87_0 - - libthrift=0.15.0=ha2884a9_3 - - libtiff=4.7.0=h404307b_0 - - libuv=1.48.0=h827c3e9_0 - - libwebp=1.3.2=h18467be_1 - - libwebp-base=1.3.2=h3d04722_1 - - libxml2=2.13.8=h866ff63_0 - - lz4-c=1.9.4=h2bbff1b_1 - - markupsafe=3.0.2=py312h827c3e9_0 - - mkl=2023.1.0=h6b88ed4_46358 - - mkl-service=2.4.0=py312h827c3e9_2 - - mkl_fft=1.3.11=py312h827c3e9_0 - - mkl_random=1.2.8=py312h0158946_0 - - mpc=1.3.1=h827c3e9_0 - - mpfr=4.2.1=h56c3642_0 - - mpmath=1.3.0=py312haa95532_0 - - multidict=6.6.3=pyh62beb40_0 - - multiprocess=0.70.15=py312haa95532_0 - - networkx=3.5=py312haa95532_0 - - numexpr=2.11.0=py312hdb065b2_0 - - numpy=2.3.1=py312h5f75535_0 - - numpy-base=2.3.1=py312h23d94f8_0 - - openjpeg=2.5.2=h9b5d1b5_1 - - openssl=3.5.2=h725018a_0 - - orc=2.1.1=hd1c1d5c_0 - - packaging=25.0=pyh29332c3_1 - - pandas=2.3.2=py312ha5e6156_0 - - pcre2=10.42=h0ff8eda_1 - - pillow=11.3.0=py312hb328d1f_0 - - pip=25.2=pyhc872135_0 - - pixman=0.46.4=h4043f72_0 - - propcache=0.3.1=pyhe1237c8_0 - - psutil=5.9.0=py312h827c3e9_1 - - pyarrow=19.0.0=py312h5da7b33_1 - - pycparser=2.21=pyhd3eb1b0_0 - - pysocks=1.7.1=py312haa95532_0 - - python=3.12.11=h716150d_0 - - python-dateutil=2.9.0.post0=pyhe01879c_2 - - python-tzdata=2025.2=pyhd8ed1ab_0 - - python-xxhash=3.5.0=py312h827c3e9_0 - - pytorch=2.5.1=py3.12_cuda12.4_cudnn9_0 - - pytorch-cuda=12.4=h3fd98bf_7 - - pytorch-mutex=1.0=cuda - - pytz=2025.2=pyhd8ed1ab_0 - - pyyaml=6.0.2=py312h827c3e9_0 - - re2=2024.07.02=haf4117d_3 - - regex=2024.11.6=py312h827c3e9_0 - - requests=2.32.5=py312haa95532_0 - - safetensors=0.5.3=py312h44068b5_0 - - setuptools=72.1.0=py312haa95532_0 - - six=1.17.0=pyhe01879c_1 - - snappy=1.2.2=h7fa0ca8_0 - - sqlite=3.50.2=hda9a48d_1 - - sympy=1.14.0=py312haa95532_0 - - tbb=2021.8.0=h59b6b97_0 - - tk=8.6.15=hf199647_0 - - tokenizers=0.21.0=py312h482ea96_0 - - tqdm=4.67.1=pyhd8ed1ab_1 - - transformers=4.55.4=pyhd8ed1ab_0 - - typing-extensions=4.15.0=py312haa95532_0 - - typing_extensions=4.15.0=py312haa95532_0 - - tzdata=2025b=h04d1e81_0 - - ucrt=10.0.22621.0=haa95532_0 - - urllib3=2.5.0=py312haa95532_0 - - utf8proc=2.6.1=h2bbff1b_1 - - vc=14.42=haa95532_5 - - vc14_runtime=14.44.35208=h4927774_10 - - vs2015_runtime=14.44.35208=ha6b5a95_10 - - wheel=0.45.1=py312haa95532_0 - - win_inet_pton=1.1.0=py312haa95532_0 - - xxhash=0.8.0=h8ffe710_3 - - xz=5.6.4=h4754444_1 - - yaml=0.2.5=he774522_0 - - yarl=1.20.1=pyhe1237c8_0 - - zlib=1.2.13=h8cc25b3_1 - - zstd=1.5.6=h8880b57_0 - - pip: - - torchaudio==2.5.1 - - torchvision==0.20.1 \ No newline at end of file + - torchvision + - pytorch + - torchaudio + - transformers + - accelerate + - ffmpeg + - pyinstaller + - customtkinter + - isort + - mypy + - black \ No newline at end of file diff --git a/main.py b/main.py index 8cc09e4..51e50f3 100644 --- a/main.py +++ b/main.py @@ -1,13 +1,5 @@ -from transcription.audio_transcription import AudioTranscription -from transcription.torch_checker import check_torch -from utils.logger import setup_logger +from ui.ui import TranscriberApp -logger = setup_logger("main") - -check_torch() - -try: - track = AudioTranscription("sample.mp3") - print(track.transcribe_audio()) -except Exception as e: - logger.error(f"Execution error: {e}") \ No newline at end of file +if __name__ == "__main__": + app = TranscriberApp() + app.mainloop() \ No newline at end of file diff --git a/transcription/audio_transcription.py b/transcription/audio_transcription.py index 27f967e..b2bee31 100644 --- a/transcription/audio_transcription.py +++ b/transcription/audio_transcription.py @@ -8,7 +8,7 @@ import time import math from tqdm import tqdm -# TODO: rename naming +# TODO: implement transcription with shift class AudioTranscription: model_name = "openai/whisper-large-v2" @@ -53,6 +53,7 @@ class AudioTranscription: self.chunks: list = [] self.batches: list = [] + self.all_transcription: list = [] try: logger.info("Loading model WhisperProcessor...") self.processor = WhisperProcessor.from_pretrained(self.model_name) @@ -71,15 +72,15 @@ class AudioTranscription: logger.error(f"Unable to load file {self.filepath}: {e}") raise - def resample(self) -> None: + def _resample(self) -> None: self.waveform = torchaudio.functional.resample(self.waveform, self.sampling_rate, 16000) - def to_mono(self): + def _to_mono(self): if self.waveform.shape[0] > 1: self.waveform = self.waveform.mean(dim=0, keepdim=True) self.waveform = self.waveform.squeeze(0) - def split_to_chunks(self, chunk_length_s: int = 30) -> None: + def _split_to_chunks(self, chunk_length_s: int = 30) -> None: self.logger.info(f"Splitting audio on chunks...") self.chunk_size = chunk_length_s * 16000 # 16kHz after resampling @@ -95,15 +96,14 @@ class AudioTranscription: chunk = self.waveform[start:end].cpu().numpy().astype("float32") self.chunks.append(chunk) - def resplit_to_batches(self) -> None: + def _resplit_to_batches(self) -> None: self.logger.info(f"Splitting chunks into batches...") self.batches = [] for i in range(0, len(self.chunks), self.custom_batch_length): batch = self.chunks[i:i + self.custom_batch_length] self.batches.append(batch) - - def process_all_batches(self) -> None: + def _process_all_batches(self) -> None: start_time = time.time() try: self.all_transcription = [] @@ -136,9 +136,10 @@ class AudioTranscription: def transcribe_audio(self) -> str: - self.resample() - self.to_mono() - self.split_to_chunks() - self.resplit_to_batches() - self.process_all_batches() + # TODO: maybe something else, not str? + self._resample() + self._to_mono() + self._split_to_chunks() + self._resplit_to_batches() + self._process_all_batches() return " ".join(self.all_transcription) \ No newline at end of file diff --git a/ui/configuration_actions.py b/ui/configuration_actions.py new file mode 100644 index 0000000..7872c8b --- /dev/null +++ b/ui/configuration_actions.py @@ -0,0 +1,9 @@ +from transcription.device_configuration import DeviceConfiguration + +# TODO: implement saving & removing configuration +def save_configuration(cfg: DeviceConfiguration): + config = { + "Model": cfg.model_name, + "Batch Size": cfg.batch_size, + "Data Type": cfg.data_type + } \ No newline at end of file diff --git a/ui/ui.py b/ui/ui.py index 782e659..3c98b6c 100644 --- a/ui/ui.py +++ b/ui/ui.py @@ -1,123 +1,245 @@ +import os +import threading +import queue import tkinter as tk -from tkinter.scrolledtext import ScrolledText -from ui.ui_log_handler import UILogHandler, setup_ui_logger +from tkinter import scrolledtext, filedialog, messagebox + +import customtkinter as ctk +import torch + +from ui.ui_log_handler import setup_ui_logger from transcription.torch_checker import check_torch from transcription.device_configuration import DeviceConfiguration from transcription.audio_transcription import AudioTranscription -def main(): - root = tk.Tk() - root.title("Audio Transcriptor") - root.geometry("800x600") - for col in range(4): - root.grid_columnconfigure(col, weight=1) - root.grid_rowconfigure(6, weight=1) +WINDOW_WIDTH = 900 +WINDOW_HEIGHT = 650 - ### Buttons selector - check_torch_baton = tk.Button(root, text="Check Torch") - check_torch_baton.grid(row=0, column=0, padx=5, pady=5, sticky="ew") - # TODO: implement saving function - save_configuration_baton = tk.Button(root, text="Save configuration") - save_configuration_baton.grid(row=0, column=1, padx=5, pady=5, sticky="ew") +class TranscriberApp(ctk.CTk): + def __init__(self): + super().__init__() + self.title("Notecast") + self.geometry(f"{WINDOW_WIDTH}x{WINDOW_HEIGHT}") + ctk.set_appearance_mode("System") + ctk.set_default_color_theme("blue") - # TODO: implement deleting function - delete_configuration_baton = tk.Button(root, text="Delete configuration") - delete_configuration_baton.grid(row=0, column=2, padx=5, pady=5, sticky="ew") + # states + self.progress_queue = queue.Queue() + self.transcribe_thread = None + self.stop_flag = threading.Event() - start_transcription_baton = tk.Button(root, text="Transcript") - start_transcription_baton.grid(row=0, column=3, padx=5, pady=5, sticky="ew") + # user variables + self.model_var = tk.StringVar(value="openai/whisper-large-v2") + self.batch_var = tk.StringVar(value="32") + self.dtype_var = tk.StringVar(value="torch.float16") + self.chunk_var = tk.StringVar(value="30") - ### Model options selector - model_options = [ - "openai/whisper-large-v2", - "openai/whisper-large", - "openai/whisper-medium", - "openai/whisper-small", - "openai/whisper-tiny" - ] - selected_model = tk.StringVar(value=model_options[0]) - label_model = tk.Label(root, text="Model name:") - label_model.grid(row=1, column=0, sticky="w", pady=5, padx=5) - dropdown_model_selection = tk.OptionMenu(root, selected_model, *model_options) - dropdown_model_selection.grid(row=1, column=1, sticky="ew", pady=5, padx=5) + # settings device options + device_opts = [] + if torch.cuda.is_available(): + device_opts.append("cuda") + if torch.backends.mps.is_available(): + device_opts.append("mps") + device_opts.append("cpu") + self.device_var = tk.StringVar(value=device_opts[0]) + self.device_opts = device_opts - ### Batch size selector - batch_sizes = ["32", "16", "8", "4", "2"] - selected_batch_size = tk.StringVar(value=batch_sizes[0]) - label_batch_size = tk.Label(root, text="Batch size:") - label_batch_size.grid(row=1, column=2, sticky="w", pady=5, padx=5) - dropdown_batch_size_selection = tk.OptionMenu(root, selected_batch_size, *batch_sizes) - dropdown_batch_size_selection.grid(row=1, column=3, sticky="ew", pady=5, padx=5) + self.input_file_var = tk.StringVar() + self.output_file_var = tk.StringVar() - ### Data type selector - data_types = ["torch.float16", "torch.float32", "torch.bfloat16"] - selected_data_type = tk.StringVar(value=data_types[0]) - label_data_type = tk.Label(root, text="Data type:") - label_data_type.grid(row=2, column=0, sticky="w", pady=5, padx=5) - dropdown_data_type_selection = tk.OptionMenu(root, selected_data_type, *data_types) - dropdown_data_type_selection.grid(row=2, column=1, sticky="ew", pady=5, padx=5) + # tabs packing + self.tabview = ctk.CTkTabview(self) + self.tabview.pack(expand=True, fill="both", padx=10, pady=10) - ### Chunk length selector - chunk_lengths = ["30", "25", "20", "15", "10", "5"] - selected_chunk_length = tk.StringVar(value=chunk_lengths[0]) - label_chunk_length = tk.Label(root, text="Chunk length:") - label_chunk_length.grid(row=2, column=2, sticky="w", pady=5, padx=5) - dropdown_chunk_length_selection = tk.OptionMenu(root, selected_chunk_length, *chunk_lengths) - dropdown_chunk_length_selection.grid(row=2, column=3, sticky="ew", pady=5, padx=5) - - # TODO: add device selector (cuda/mps/cpu) + self.transcript_tab = self.tabview.add("Transcription") + self.settings_tab = self.tabview.add("Settings") - ### Filepath (input) - # TODO: add checker if path is valid/invalid (i think in utils or something) - label_file_path = tk.Label(root, text="Input filepath:") - label_file_path.grid(row=3, column=0, sticky="w", pady=5, padx=5) - file_path = tk.Text(root, height=1) - file_path.grid(row=3, column=1, columnspan=3, sticky="ew", pady=5, padx=5) + self._build_transcription_tab() + self._build_settings_tab() - ### Filepath (output) - # TODO: add question mark here with tip while mouse is on it - label_output_file_path = tk.Label(root, text="Output filepath:") - label_output_file_path.grid(row=4, column=0, sticky="w", pady=5, padx=5) - output_file_path = tk.Text(root, height=1) - output_file_path.grid(row=4, column=1, columnspan=3, sticky="ew", pady=5, padx=5) + # logger + self.ui_logger = setup_ui_logger(self.log_box) - def show_selections(): - ui_logger.info(f"Selected model: {selected_model.get()}") - ui_logger.info(f"Selected batch size: {selected_batch_size.get()} chunks") - ui_logger.info(f"Selected data type: {selected_data_type.get()}") - - show_selections_baton = tk.Button(root, text="Show Selections", command=show_selections) - show_selections_baton.grid(row=5, column=0, columnspan=4, pady=5, sticky="ew") + # main transcription tab + def _build_transcription_tab(self): + # file selectors + file_frame = ctk.CTkFrame(self.transcript_tab, corner_radius=10) + file_frame.pack(padx=20, pady=10, fill="x") - log_box = ScrolledText(root, wrap="word") - log_box.grid(row=6, column=0, columnspan=4, sticky="nsew", padx=10, pady=5) - ui_logger = setup_ui_logger(log_box) - - def transcribe(): - current_device_config = DeviceConfiguration( - device="cuda", - model_name=selected_model.get(), - batch_size=int(selected_batch_size.get()), - chunk_length_s=30, - data_type=selected_data_type.get() + ctk.CTkLabel(file_frame, text="Input file:").pack(side="left", padx=5, pady=5) + ctk.CTkEntry(file_frame, textvariable=self.input_file_var, width=400).pack( + side="left", padx=5, pady=5, expand=True, fill="x" ) - Audio = AudioTranscription( - filepath=file_path.get("1.0", "end-1c"), - device_configuration=current_device_config, - logger=ui_logger + ctk.CTkButton(file_frame, text="Browse", command=self._browse_input).pack( + side="left", padx=5, pady=5 ) - transcription = Audio.transcribe_audio() - with open(f"{file_path.get('1.0', 'end-1c')}.txt", "w") as output_file: - output_file.write(transcription) - - check_torch_baton.config(command=lambda: check_torch(ui_logger)) - start_transcription_baton.config(command=transcribe) - - root.mainloop() + out_frame = ctk.CTkFrame(self.transcript_tab, corner_radius=10) + out_frame.pack(padx=20, pady=5, fill="x") + ctk.CTkLabel(out_frame, text="Output file:").pack(side="left", padx=5, pady=5) + ctk.CTkEntry(out_frame, textvariable=self.output_file_var, width=400).pack( + side="left", padx=5, pady=5, expand=True, fill="x" + ) + ctk.CTkButton(out_frame, text="Browse", command=self._browse_output).pack( + side="left", padx=5, pady=5 + ) + # controls + ctrl_frame = ctk.CTkFrame(self.transcript_tab, corner_radius=10) + ctrl_frame.pack(padx=20, pady=10, fill="x") -if __name__ == "__main__": - main() + ctk.CTkButton(ctrl_frame, text="Check Torch", command=self._check_torch).pack( + side="left", padx=10, pady=5 + ) + self.start_btn = ctk.CTkButton( + ctrl_frame, text="Start transcription", command=self._start_transcription + ) + self.start_btn.pack(side="left", padx=10, pady=5) + + self.stop_btn = ctk.CTkButton( + ctrl_frame, text="Stop", command=self._stop_transcription, state="disabled" + ) + self.stop_btn.pack(side="left", padx=10, pady=5) + + # log box + self.log_box = scrolledtext.ScrolledText( + self.transcript_tab, + wrap="word", + height=20, + font=("Consolas", 16), + ) + self.log_box.pack(padx=20, pady=10, expand=True, fill="both") + + def _build_settings_tab(self): + pad = 20 + + ### Model + ctk.CTkLabel(self.settings_tab, text="Model:").pack( + anchor="w", padx=pad, pady=(pad, 5) + ) + ctk.CTkOptionMenu( + self.settings_tab, + variable=self.model_var, + values=[ + "openai/whisper-large-v2", + "openai/whisper-large", + "openai/whisper-medium", + "openai/whisper-small", + "openai/whisper-tiny", + ], + ).pack(fill="x", padx=pad, pady=5) + + ### Batch size + ctk.CTkLabel(self.settings_tab, text="Batch size:").pack( + anchor="w", padx=pad, pady=(pad, 5) + ) + ctk.CTkOptionMenu( + self.settings_tab, + variable=self.batch_var, + values=["32", "16", "8", "4", "2"], + ).pack(fill="x", padx=pad, pady=5) + + ### Data type + ctk.CTkLabel(self.settings_tab, text="Data type:").pack( + anchor="w", padx=pad, pady=(pad, 5) + ) + ctk.CTkOptionMenu( + self.settings_tab, + variable=self.dtype_var, + values=["torch.float16", "torch.float32", "torch.bfloat16"], + ).pack(fill="x", padx=pad, pady=5) + + ### Chunk length + ctk.CTkLabel(self.settings_tab, text="Chunk length (s):").pack( + anchor="w", padx=pad, pady=(pad, 5) + ) + ctk.CTkOptionMenu( + self.settings_tab, + variable=self.chunk_var, + values=["30", "25", "20", "15", "10", "5"], + ).pack(fill="x", padx=pad, pady=5) + + ### Device + ctk.CTkLabel(self.settings_tab, text="Device:").pack( + anchor="w", padx=pad, pady=(pad, 5) + ) + ctk.CTkOptionMenu( + self.settings_tab, + variable=self.device_var, + values=self.device_opts, + ).pack(fill="x", padx=pad, pady=5) + + # action buttons + def _browse_input(self): + path = filedialog.askopenfilename( + title="Select input audio file", + filetypes=[("Audio files", "*.wav *.mp3 *.m4a *.flac"), ("All files", "*.*")] + ) + if path: + self.input_file_var.set(path) + + def _browse_output(self): + path = filedialog.asksaveasfilename( + title="Select output file", + defaultextension=".txt", + filetypes=[("Text files", "*.txt"), ("All files", "*.*")] + ) + if path: + self.output_file_var.set(path) + + def _check_torch(self): + check_torch(self.ui_logger) + + def _start_transcription(self): + infile = self.input_file_var.get().strip() + if not infile or not os.path.isfile(infile): + messagebox.showerror("Error", "Please select a valid input file.") + return + + self.start_btn.configure(state="disabled") + self.stop_btn.configure(state="normal") + self.ui_logger.info("Starting transcription...") + + self.stop_flag.clear() + self.transcribe_thread = threading.Thread( + target=self._transcribe_worker, args=(infile,), daemon=True + ) + self.transcribe_thread.start() + + def _stop_transcription(self): + self.stop_flag.set() + self.ui_logger.info("Stopping transcription...") + self.stop_btn.configure(state="disabled") + + def _transcribe_worker(self, infile: str): + try: + config = DeviceConfiguration( + device=self.device_var.get(), + model_name=self.model_var.get(), + batch_size=int(self.batch_var.get()), + chunk_length_s=int(self.chunk_var.get()), + data_type=self.dtype_var.get(), + ) + Audio = AudioTranscription( + filepath=infile, + device_configuration=config, + logger=self.ui_logger, + ) + transcription = Audio.transcribe_audio() + + outfile = self.output_file_var.get().strip() + if not outfile: + outfile = infile + ".txt" + + with open(outfile, "w", encoding="utf-8") as f: + f.write(transcription) + + self.ui_logger.info(f"Transcription saved to {outfile}") + except Exception as e: + self.ui_logger.error(f"Error: {e}") + messagebox.showerror("Error", str(e)) + finally: + self.start_btn.configure(state="normal") + self.stop_btn.configure(state="disabled") \ No newline at end of file