Minor update

- added transcription folder with checker and transcription modules
- logger is a separated module now
This commit is contained in:
2025-09-06 23:36:33 +03:00
parent 6b05918895
commit ecbbbfd5d1
9 changed files with 437 additions and 139 deletions
+10
View File
@@ -0,0 +1,10 @@
# Notecast
**Notecast** is a simple and minimalistic utilite to transcribe audiofiles to text and create a conspect.
Requires nvcc v12.8
You can create environment running
```
conda env create -f environment.yml
```
+195
View File
@@ -0,0 +1,195 @@
name: notecast
channels:
- conda-forge
- defaults
- nvidia
- pytorch
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
+6 -133
View File
@@ -1,140 +1,13 @@
from transformers import WhisperProcessor, WhisperForConditionalGeneration
import torch
import torchaudio
import logging
import time
from tqdm import tqdm
from transcription.audio_transcription import AudioTranscription
from transcription.torch_checker import check_torch
from utils.logger import setup_logger
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
logger = setup_logger("main")
print("=== Checking PyTorch ===")
print(f"Torch version: {torch.version}")
print(f"CUDA is available: {torch.cuda.is_available()}")
if torch.cuda.is_available():
print(f"CUDA version: {torch.version.cuda}")
print(f"Number of GPU: {torch.cuda.device_count()}")
print(f"Name of GPU: {torch.cuda.get_device_name(0)}")
print("=== Check completed ===")
class AudioTranscription:
model_name = "openai/whisper-large-v2"
filepath: str
waveform: torch.Tensor
sampling_rate: int
chunks: list = []
# one chunk size in seconds
chunk_size: int
device = "cuda"
processor: WhisperProcessor
model: WhisperForConditionalGeneration
# self.device here and all previous shit
language = "ru"
all_transcription: list = []
def __init__(
self,
filepath: str,
language = "ru",
device = "cuda",
model_name = "openai/whisper-large-v2"
) -> None:
self.filepath = filepath
self.language = language
self.device = device
self.model_name = model_name
self.chunks: list = []
try:
logger.info("Loading model WhisperProcessor...")
self.processor = WhisperProcessor.from_pretrained(self.model_name)
self.model = WhisperForConditionalGeneration.from_pretrained(
self.model_name,
torch_dtype=torch.float16,
device_map="auto"
).to(self.device)
logger.info("Model loaded.")
self.waveform, self.sampling_rate = torchaudio.load(filepath, format="mp3", backend="ffmpeg")
logger.info(f"Successfully loaded file {filepath}.")
except Exception as e:
logger.error(f"Unable to load file {self.filepath}: {e}")
raise
def resample(self) -> None:
self.waveform = torchaudio.functional.resample(self.waveform, self.sampling_rate, 16000)
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:
logger.info(f"Splitting audio on chunks...")
self.chunk_size = chunk_length_s * 16000 # 16kHz after resampling
total_samples = self.waveform.shape[0]
chunks_count = (total_samples + self.chunk_size - 1) // self.chunk_size
logger.info(f"File length - {total_samples / 16000:.1f} seconds, splitting on {chunks_count} chunks by {chunk_length_s} seconds per chunk.")
self.chunks = []
for idx in range(chunks_count):
start = idx * self.chunk_size
end = min((idx + 1) * self.chunk_size, total_samples)
chunk = self.waveform[start:end].cpu().numpy().astype("float32")
self.chunks.append(chunk)
def process_chunk(
self,
chunk
) -> str:
inputs = self.processor(chunk, sampling_rate=16000, return_tensors="pt")
input_features = inputs.input_features.to(self.device).to(torch.float16)
with torch.no_grad():
predicted_ids = self.model.generate(
input_features,
language=self.language,
task="transcribe",
temperature=0.0
)
text = self.processor.batch_decode(predicted_ids, skip_special_tokens=True)[0]
return text
def process_all_chunks(self) -> None:
start_time = time.time()
try:
for chunk_idx in tqdm(range(len(self.chunks))):
self.all_transcription.append(self.process_chunk(self.chunks[chunk_idx]))
end_time = time.time()
logger.info(f"Transcription completed in {end_time - start_time:.2f} seconds")
except Exception as e:
logger.error(f"Errors occured while processing chunks: {e}")
def transcribe_audio(self) -> str:
self.resample()
self.to_mono()
self.split_to_chunks()
self.process_all_chunks()
return " ".join(self.all_transcription)
check_torch()
try:
track = AudioTranscription("sample.mp3")
print(track.transcribe_audio())
except Exception as e:
logger.error(f"Execution error: {e}")
logger.error(f"Execution error: {e}")
+13
View File
@@ -0,0 +1,13 @@
$envName = "notecast"
Write-Output >>> Creating environment $envName from environment.yml"
conda env create -f environment.yml -n $envName
if ($LASTEXITCODE -ne 0) {
Write-Output ">>> Environment already exists, updating..."
conda env update -f environment.yml --prune
}
Write-Output ">>> Activating environment"
conda activate $envName
Write-Output ">>> Download completed!"
+10 -5
View File
@@ -1,8 +1,13 @@
# python -m venv .venv
#!/bin/bash
set -e
# pip install -r requirements.txt --extra-index-url https://download.pytorch.org/whl/cu128
ENV_NAME="notecast"
conda init -c speech-to-conspect
echo ">>> Creating environment $ENV_NAME from environment.yml"
conda env create -f environment.yml || conda env update -f environment.yml --prune
conda install pytorch torchvision torchaudio -c pytorch -c nvidia
conda install accelerate transformers ffmpeg -c conda-forge
echo ">>> Activating environment"
eval "$(conda shell.bash hook)"
conda activate "$ENV_NAME"
echo ">>> Download completed!"
+143
View File
@@ -0,0 +1,143 @@
from transformers import WhisperProcessor, WhisperForConditionalGeneration
import torch
import torchaudio
from utils.logger import setup_logger
import time
import math
from tqdm import tqdm
logger = setup_logger("AudioTranscribe module")
class AudioTranscription:
model_name = "openai/whisper-large-v2"
filepath: str
waveform: torch.Tensor
sampling_rate: int
chunks: list = []
chunk_size: int
device = "cuda"
processor: WhisperProcessor
model: WhisperForConditionalGeneration
language = "ru"
all_transcription: list = []
def __init__(
self,
filepath: str,
language = "ru",
device = "cuda",
model_name = "openai/whisper-large-v2"
) -> None:
self.filepath = filepath
self.language = language
self.device = device
self.model_name = model_name
self.chunks: list = []
try:
logger.info("Loading model WhisperProcessor...")
self.processor = WhisperProcessor.from_pretrained(self.model_name)
self.model = WhisperForConditionalGeneration.from_pretrained(
self.model_name,
torch_dtype=torch.float16,
device_map="auto"
).to(self.device)
logger.info("Model loaded.")
self.waveform, self.sampling_rate = torchaudio.load(filepath, format="mp3", backend="ffmpeg")
logger.info(f"Successfully loaded file {filepath}.")
except Exception as e:
logger.error(f"Unable to load file {self.filepath}: {e}")
raise
def resample(self) -> None:
self.waveform = torchaudio.functional.resample(self.waveform, self.sampling_rate, 16000)
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:
logger.info(f"Splitting audio on chunks...")
self.chunk_size = chunk_length_s * 16000 # 16kHz after resampling
total_samples = self.waveform.shape[0]
chunks_count = (total_samples + self.chunk_size - 1) // self.chunk_size
logger.info(f"File length - {total_samples / 16000:.1f} seconds, splitting on {chunks_count} chunks by {chunk_length_s} seconds per chunk.")
self.chunks = []
for idx in range(chunks_count):
start = idx * self.chunk_size
end = min((idx + 1) * self.chunk_size, total_samples)
chunk = self.waveform[start:end].cpu().numpy().astype("float32")
self.chunks.append(chunk)
def process_chunk(
self,
chunk
) -> str:
inputs = self.processor(chunk, sampling_rate=16000, return_tensors="pt")
input_features = inputs.input_features.to(self.device).to(torch.float16)
with torch.no_grad():
predicted_ids = self.model.generate(
input_features,
language=self.language,
task="transcribe",
temperature=0.0
)
text = self.processor.batch_decode(predicted_ids, skip_special_tokens=True)[0]
return text
def process_all_chunks(self, batch_size: int = 16) -> None:
start_time = time.time()
try:
self.all_transcription = []
for i in tqdm(range(math.ceil(len(self.chunks) / batch_size))):
# TODO: rewrite batching as a separate function
batch = self.chunks[i*batch_size:(i+1)*batch_size]
inputs = self.processor(
batch,
sampling_rate=16000,
return_tensors="pt",
padding=True
)
input_features = inputs.input_features.to(self.device).to(torch.float16)
with torch.no_grad():
predicted_ids = self.model.generate(
input_features,
language=self.language,
task="transcribe",
temperature=0.0
)
texts = self.processor.batch_decode(predicted_ids, skip_special_tokens=True)
self.all_transcription.extend(texts)
end_time = time.time()
logger.info(f"Transcription completed in {end_time - start_time:.2f} seconds")
except Exception as e:
logger.error(f"Errors occured while processing chunks: {e}")
def transcribe_audio(self) -> str:
self.resample()
self.to_mono()
self.split_to_chunks()
self.process_all_chunks()
return " ".join(self.all_transcription)
+11
View File
@@ -0,0 +1,11 @@
import torch
def check_torch() -> None:
print("=== Checking PyTorch ===")
print(f"Torch version: {torch.version}")
print(f"CUDA is available: {torch.cuda.is_available()}")
if torch.cuda.is_available():
print(f"CUDA version: {torch.version.cuda}")
print(f"Number of GPU: {torch.cuda.device_count()}")
print(f"Name of GPU: {torch.cuda.get_device_name(0)}")
print("=== Check completed ===")
+13 -1
View File
@@ -1 +1,13 @@
# empty((
import tkinter as tk
root = tk.Tk()
root.title("Audio Transcriptor")
root.geometry("800x600")
path_entry = tk.Entry(root, width=40)
path_entry.pack(padx=10, pady=(5, 10))
transcribe_button = tk.Button(text="Transcribe")
transcribe_button.pack(anchor="e", rely=25, relx=15)
root.mainloop()
+36
View File
@@ -0,0 +1,36 @@
import logging
import sys
def setup_logger(
name: str = __name__,
level: int = logging.INFO,
log_to_file: bool = False,
filename: str = "app.log"
) -> logging.Logger:
"""
Logger configuration with output to command line and (optional) to file
:param name: logger name
:param level: logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
:param log_to_file: logging to filename
:param filename: filename for logs
:return: logger instance
"""
logger = logging.getLogger(name)
logger.setLevel(level)
formatter = logging.Formatter(
fmt="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
if log_to_file:
file_handler = logging.FileHandler(filename, encoding="utf-8")
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
return logger