Compare commits
10 Commits
ecbbbfd5d1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d48f473b0 | |||
| 9e67b36842 | |||
| 6147eb1597 | |||
| 7f92a27b49 | |||
| 633ac7e081 | |||
| add2e15d5f | |||
| 414cb4d38e | |||
| 7291b148e5 | |||
| 7cf4c5259b | |||
| 8f41105e4b |
@@ -11,3 +11,6 @@ wheels/
|
|||||||
|
|
||||||
main.todo
|
main.todo
|
||||||
sample.mp3
|
sample.mp3
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
ui/assets/question_mark.png
|
||||||
@@ -2,9 +2,14 @@
|
|||||||
|
|
||||||
**Notecast** is a simple and minimalistic utilite to transcribe audiofiles to text and create a conspect.
|
**Notecast** is a simple and minimalistic utilite to transcribe audiofiles to text and create a conspect.
|
||||||
|
|
||||||
Requires nvcc v12.8
|
Requires nvidia cuda toolkit v12.4
|
||||||
|
|
||||||
You can create environment running
|
You can create environment running
|
||||||
```
|
```
|
||||||
conda env create -f environment.yml
|
conda env create -f environment.yml
|
||||||
```
|
```
|
||||||
|
or for CUDA/Nvidia:
|
||||||
|
```
|
||||||
|
conda create -n notecast -c pytorch -c nvidia pytorch torchvision torchaudio transformers python=3.12
|
||||||
|
conda install ffmpeg customtkinter openai -c conda-forge -c bioconda
|
||||||
|
```
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
name: notecast-cpu-mps
|
||||||
|
channels:
|
||||||
|
- pytorch
|
||||||
|
- conda-forge
|
||||||
|
- defaults
|
||||||
|
- bioconda
|
||||||
|
dependencies:
|
||||||
|
- pytorch
|
||||||
|
- ffmpeg
|
||||||
|
- torchvision
|
||||||
|
- torchaudio
|
||||||
|
- transformers
|
||||||
|
- python=3.12
|
||||||
|
- customtkinter
|
||||||
|
- openai
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
name: notecast-cuda
|
||||||
|
channels:
|
||||||
|
- pytorch
|
||||||
|
- nvidia
|
||||||
|
- conda-forge
|
||||||
|
- bioconda
|
||||||
|
dependencies:
|
||||||
|
- pytorch
|
||||||
|
- ffmpeg
|
||||||
|
- torchvision
|
||||||
|
- torchaudio
|
||||||
|
- transformers
|
||||||
|
- python=3.12
|
||||||
|
- customtkinter
|
||||||
|
- openai
|
||||||
-195
@@ -1,195 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,13 +1,5 @@
|
|||||||
from transcription.audio_transcription import AudioTranscription
|
from ui.ui import TranscriberApp
|
||||||
from transcription.torch_checker import check_torch
|
|
||||||
from utils.logger import setup_logger
|
|
||||||
|
|
||||||
logger = setup_logger("main")
|
if __name__ == "__main__":
|
||||||
|
app = TranscriberApp()
|
||||||
check_torch()
|
app.mainloop()
|
||||||
|
|
||||||
try:
|
|
||||||
track = AudioTranscription("sample.mp3")
|
|
||||||
print(track.transcribe_audio())
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Execution error: {e}")
|
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
$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!"
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
ENV_NAME="notecast"
|
|
||||||
|
|
||||||
echo ">>> Creating environment $ENV_NAME from environment.yml"
|
|
||||||
conda env create -f environment.yml || conda env update -f environment.yml --prune
|
|
||||||
|
|
||||||
echo ">>> Activating environment"
|
|
||||||
eval "$(conda shell.bash hook)"
|
|
||||||
conda activate "$ENV_NAME"
|
|
||||||
|
|
||||||
echo ">>> Download completed!"
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
import torchaudio
|
||||||
|
import torch
|
||||||
|
|
||||||
|
class Audio:
|
||||||
|
waveform: torch.Tensor
|
||||||
|
sr: int
|
||||||
|
|
||||||
|
def load(self, filepath):
|
||||||
|
"""
|
||||||
|
Loads audio from file's path
|
||||||
|
"""
|
||||||
|
self.waveform, self.sr = torchaudio.load(
|
||||||
|
filepath,
|
||||||
|
backend="ffmpeg",
|
||||||
|
)
|
||||||
|
return self
|
||||||
@@ -1,143 +1,50 @@
|
|||||||
from transformers import WhisperProcessor, WhisperForConditionalGeneration
|
from transcription.audio import Audio
|
||||||
import torch
|
from transcription.preprocessing.audio_preprocessor import AudioPreprocessor
|
||||||
import torchaudio
|
from transcription.preprocessing.splitter import Splitter
|
||||||
from utils.logger import setup_logger
|
from transcription.engines.whisper import WhisperEngine
|
||||||
import time
|
from transcription.configuration import Configuration
|
||||||
import math
|
|
||||||
from tqdm import tqdm
|
|
||||||
|
|
||||||
logger = setup_logger("AudioTranscribe module")
|
|
||||||
|
|
||||||
|
# maybe inherit from AudioTranscription and rename to something like WhisperTranscription?
|
||||||
class AudioTranscription:
|
class AudioTranscription:
|
||||||
model_name = "openai/whisper-large-v2"
|
# add multimodel ability
|
||||||
|
|
||||||
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__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
filepath: str,
|
filepath: str,
|
||||||
language = "ru",
|
config: Configuration,
|
||||||
device = "cuda",
|
language,
|
||||||
model_name = "openai/whisper-large-v2"
|
# logger
|
||||||
) -> None:
|
) -> None:
|
||||||
self.filepath = filepath
|
self.filepath = filepath
|
||||||
self.language = language
|
self.language = language
|
||||||
self.device = device
|
# self.logger = logger
|
||||||
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.audio = Audio()
|
||||||
self.model_name,
|
self.preprocessor = AudioPreprocessor()
|
||||||
torch_dtype=torch.float16,
|
self.splitter = Splitter(
|
||||||
device_map="auto"
|
chunkSize=config.chunkSize,
|
||||||
).to(self.device)
|
batchSize=config.batchSize,
|
||||||
|
)
|
||||||
logger.info("Model loaded.")
|
self.engine = WhisperEngine(
|
||||||
|
modelName=config.modelName,
|
||||||
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,
|
language=self.language,
|
||||||
task="transcribe",
|
dType=config.dType,
|
||||||
temperature=0.0
|
device=config.device,
|
||||||
)
|
)
|
||||||
|
|
||||||
text = self.processor.batch_decode(predicted_ids, skip_special_tokens=True)[0]
|
# maybe add something like temperature here?
|
||||||
|
def transcribeAudio(self) -> str:
|
||||||
|
transcription: list = []
|
||||||
|
|
||||||
return text
|
self.engine.loadModel()
|
||||||
|
|
||||||
def process_all_chunks(self, batch_size: int = 16) -> None:
|
self.preprocessor.prepare(self.audio.load(self.filepath))
|
||||||
start_time = time.time()
|
|
||||||
try:
|
|
||||||
self.all_transcription = []
|
|
||||||
|
|
||||||
for i in tqdm(range(math.ceil(len(self.chunks) / batch_size))):
|
batches = self.splitter.split(self.audio.waveform)
|
||||||
# TODO: rewrite batching as a separate function
|
|
||||||
batch = self.chunks[i*batch_size:(i+1)*batch_size]
|
|
||||||
|
|
||||||
inputs = self.processor(
|
for batch in batches:
|
||||||
batch,
|
batchText: str = self.engine.transcribeBatch(batch)
|
||||||
sampling_rate=16000,
|
transcription.append(batchText)
|
||||||
return_tensors="pt",
|
|
||||||
padding=True
|
|
||||||
)
|
|
||||||
|
|
||||||
input_features = inputs.input_features.to(self.device).to(torch.float16)
|
self.engine.unloadModel()
|
||||||
|
|
||||||
with torch.no_grad():
|
return str(" ".join(transcription))
|
||||||
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)
|
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Configuration:
|
||||||
|
# add new models
|
||||||
|
device: str = "cuda"
|
||||||
|
modelName: str = "openai/whisper-large-v2"
|
||||||
|
chunkSize: int = 30
|
||||||
|
batchSize: int = 16
|
||||||
|
dataType: str = "torch.float16"
|
||||||
|
|
||||||
|
_dtype_map = {
|
||||||
|
"torch.float16": torch.float16,
|
||||||
|
"torch.float32": torch.float32,
|
||||||
|
"torch.bfloat16": torch.bfloat16,
|
||||||
|
}
|
||||||
|
|
||||||
|
dType: torch.dtype = None
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
self.dType = self._dtype_map[self.dataType]
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import torch
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
class BaseEngine(ABC):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
modelName: str,
|
||||||
|
language: str,
|
||||||
|
dType: torch.dtype,
|
||||||
|
device: str
|
||||||
|
):
|
||||||
|
self.modelName = modelName
|
||||||
|
self.device = device
|
||||||
|
self.language = language
|
||||||
|
self.dType = dType
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def loadModel(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def unloadModel(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def transcribeBatch(
|
||||||
|
self,
|
||||||
|
batch
|
||||||
|
) -> str:
|
||||||
|
pass
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# from logging import Logger
|
||||||
|
import time
|
||||||
|
import torch
|
||||||
|
import gc
|
||||||
|
from transformers import WhisperForConditionalGeneration, WhisperProcessor
|
||||||
|
|
||||||
|
from transcription.engines.base_engine import BaseEngine
|
||||||
|
|
||||||
|
class WhisperEngine(BaseEngine):
|
||||||
|
def loadModel(self) -> None:
|
||||||
|
self.processor = WhisperProcessor.from_pretrained(self.modelName)
|
||||||
|
self.model = WhisperForConditionalGeneration.from_pretrained(
|
||||||
|
self.modelName,
|
||||||
|
torch_dtype = self.dType # check twice
|
||||||
|
).to(self.device) # ??? recheck
|
||||||
|
|
||||||
|
def unloadModel(self) -> None:
|
||||||
|
self.model = None
|
||||||
|
self.processor = None
|
||||||
|
|
||||||
|
# TODO: MPS?
|
||||||
|
if self.device == "cuda":
|
||||||
|
torch.cuda.empty_cache()
|
||||||
|
|
||||||
|
def transcribeBatch(
|
||||||
|
self,
|
||||||
|
batch,
|
||||||
|
) -> str:
|
||||||
|
assert self.processor is not None
|
||||||
|
assert self.model is not None
|
||||||
|
|
||||||
|
inputs = self.processor(
|
||||||
|
batch,
|
||||||
|
sampling_rate=16000,
|
||||||
|
return_tensors="pt",
|
||||||
|
padding=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
input_features = inputs.input_features.to(self.device).to(self.dType)
|
||||||
|
|
||||||
|
with torch.no_grad():
|
||||||
|
predicted_ids = self.model.generate(
|
||||||
|
input_features,
|
||||||
|
language=self.language,
|
||||||
|
task="transcribe",
|
||||||
|
temperature=0.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
batchText = self.processor.batch_decode(
|
||||||
|
predicted_ids,
|
||||||
|
skip_special_tokens=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
inputs = None
|
||||||
|
input_features = None
|
||||||
|
predicted_ids = None
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
|
# maybe do here something with MPS?
|
||||||
|
if self.device.startswith("cuda"):
|
||||||
|
torch.cuda.empty_cache()
|
||||||
|
torch.cuda.ipc_collect()
|
||||||
|
|
||||||
|
return " ".join(batchText)
|
||||||
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
from transcription.audio import Audio
|
||||||
|
import torchaudio
|
||||||
|
|
||||||
|
class AudioPreprocessor:
|
||||||
|
TARGET_SAMPLING_RATE: int = 16000
|
||||||
|
|
||||||
|
# for different models in future
|
||||||
|
# def __init__(self, model):
|
||||||
|
# pass
|
||||||
|
|
||||||
|
def _resample(
|
||||||
|
self,
|
||||||
|
audio: Audio
|
||||||
|
) -> None:
|
||||||
|
if audio.sr != self.TARGET_SAMPLING_RATE:
|
||||||
|
audio.waveform = torchaudio.functional.resample(
|
||||||
|
audio.waveform,
|
||||||
|
audio.sr,
|
||||||
|
self.TARGET_SAMPLING_RATE
|
||||||
|
)
|
||||||
|
|
||||||
|
def _to_mono(
|
||||||
|
self,
|
||||||
|
audio: Audio
|
||||||
|
) -> None:
|
||||||
|
if audio.waveform.shape[0] > 1:
|
||||||
|
audio.waveform = audio.waveform.mean(dim=0, keepdim=True)
|
||||||
|
audio.waveform = audio.waveform.squeeze(0)
|
||||||
|
|
||||||
|
def prepare(
|
||||||
|
self,
|
||||||
|
audio: Audio
|
||||||
|
):
|
||||||
|
self._resample(audio)
|
||||||
|
self._to_mono(audio)
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import torch
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
class Splitter:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
chunkSize: int,
|
||||||
|
batchSize: int,
|
||||||
|
) -> None:
|
||||||
|
self.chunkSize = chunkSize * 16000 # 16 kHz after resampling
|
||||||
|
self.batchSize = batchSize
|
||||||
|
|
||||||
|
# maybe raise some exceptions here?
|
||||||
|
def _split_to_chunks(
|
||||||
|
self,
|
||||||
|
waveform: torch.Tensor,
|
||||||
|
) -> List:
|
||||||
|
totalSamples = waveform.shape[0]
|
||||||
|
chunksCount = (totalSamples + self.chunkSize - 1) // self.chunkSize
|
||||||
|
|
||||||
|
chunks: List = []
|
||||||
|
# tqdm or something here?
|
||||||
|
for chunkNum in range(chunksCount):
|
||||||
|
start = chunkNum * self.chunkSize
|
||||||
|
end = min((chunkNum + 1) * self.chunkSize, totalSamples)
|
||||||
|
|
||||||
|
chunk = waveform[start : end].cpu().numpy().astype("float32")
|
||||||
|
chunks.append(chunk)
|
||||||
|
|
||||||
|
return chunks
|
||||||
|
|
||||||
|
def _split_to_batches(
|
||||||
|
self,
|
||||||
|
chunks: List,
|
||||||
|
) -> List:
|
||||||
|
batches: List = []
|
||||||
|
|
||||||
|
for i in range(0, len(chunks), self.batchSize):
|
||||||
|
batch = chunks[i : i + self.batchSize]
|
||||||
|
batches.append(batch)
|
||||||
|
|
||||||
|
return batches
|
||||||
|
|
||||||
|
def split(
|
||||||
|
self,
|
||||||
|
waveform: torch.Tensor
|
||||||
|
) -> List:
|
||||||
|
chunks = self._split_to_chunks(waveform)
|
||||||
|
batches = self._split_to_batches(chunks)
|
||||||
|
return batches
|
||||||
@@ -1,11 +1,36 @@
|
|||||||
|
import logging
|
||||||
import torch
|
import torch
|
||||||
|
|
||||||
def check_torch() -> None:
|
|
||||||
print("=== Checking PyTorch ===")
|
def checkTorch(logger: logging.Logger) -> None:
|
||||||
print(f"Torch version: {torch.version}")
|
logger.info("=== Checking PyTorch ===")
|
||||||
print(f"CUDA is available: {torch.cuda.is_available()}")
|
logger.info(f"Torch version: {torch.__version__}")
|
||||||
|
|
||||||
|
# NVIDIA / AMD (CUDA API)
|
||||||
if torch.cuda.is_available():
|
if torch.cuda.is_available():
|
||||||
print(f"CUDA version: {torch.version.cuda}")
|
backend = "CUDA"
|
||||||
print(f"Number of GPU: {torch.cuda.device_count()}")
|
if torch.version.hip is not None:
|
||||||
print(f"Name of GPU: {torch.cuda.get_device_name(0)}")
|
backend = "ROCm (AMD HIP)"
|
||||||
print("=== Check completed ===")
|
|
||||||
|
logger.info(f"{backend} backend is available")
|
||||||
|
logger.info(
|
||||||
|
f"Compiled with: CUDA {torch.version.cuda}, ROCm {torch.version.hip}"
|
||||||
|
)
|
||||||
|
logger.info(f"Number of devices: {torch.cuda.device_count()}")
|
||||||
|
|
||||||
|
for i in range(torch.cuda.device_count()):
|
||||||
|
logger.info(f"GPU {i}: {torch.cuda.get_device_name(i)}")
|
||||||
|
|
||||||
|
# Apple Silicon (MPS)
|
||||||
|
elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
|
||||||
|
logger.info("MPS backend is available (Apple Silicon)")
|
||||||
|
logger.info(
|
||||||
|
f"MPS version: {getattr(torch.backends.mps, '__version__', 'unknown')}"
|
||||||
|
)
|
||||||
|
logger.info("GPU: Apple Silicon (Metal)")
|
||||||
|
|
||||||
|
# CPU only mode
|
||||||
|
else:
|
||||||
|
logger.info("Only CPU is available")
|
||||||
|
|
||||||
|
logger.info("=== Check completed ===")
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
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()
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
@@ -0,0 +1,18 @@
|
|||||||
|
from transcription.configuration import Configuration
|
||||||
|
|
||||||
|
|
||||||
|
# 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,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def load_config():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def delete_config():
|
||||||
|
pass
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
import tkinter as tk
|
||||||
|
import torch
|
||||||
|
|
||||||
|
class State:
|
||||||
|
def __init__(self, root):
|
||||||
|
# transcription
|
||||||
|
self.model = tk.StringVar(root, "openai/whisper-large-v3-turbo")
|
||||||
|
self.batch = tk.StringVar(root, "32")
|
||||||
|
self.chunk = tk.StringVar(root, "30")
|
||||||
|
self.dtype = tk.StringVar(root, "torch.float16")
|
||||||
|
self.language = tk.StringVar(root, "ru")
|
||||||
|
|
||||||
|
# llm
|
||||||
|
self.api_key = tk.StringVar(root, "")
|
||||||
|
self.api_model = tk.StringVar(root, "")
|
||||||
|
self.base_url = tk.StringVar(root, "")
|
||||||
|
self.conspect_lang = tk.StringVar(root, "Russian")
|
||||||
|
|
||||||
|
# flags
|
||||||
|
self.create_conspect = tk.BooleanVar(root, False)
|
||||||
|
self.remove_transcription = tk.BooleanVar(root, False)
|
||||||
|
|
||||||
|
# files
|
||||||
|
self.input_file = tk.StringVar(root)
|
||||||
|
self.output_file = tk.StringVar(root)
|
||||||
|
|
||||||
|
# device
|
||||||
|
devices = []
|
||||||
|
if torch.cuda.is_available():
|
||||||
|
devices.append("cuda")
|
||||||
|
if torch.backends.mps.is_available():
|
||||||
|
devices.append("mps")
|
||||||
|
devices.append("cpu")
|
||||||
|
|
||||||
|
self.device_opts = devices
|
||||||
|
self.device = tk.StringVar(root, devices[0])
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# new structure coming soon
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# new structure coming soon
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# new structure coming soon
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import customtkinter as ctk
|
||||||
|
|
||||||
|
|
||||||
|
class ToolTip(ctk.CTkToplevel):
|
||||||
|
def __init__(self, widget, text):
|
||||||
|
super().__init__()
|
||||||
|
self.withdraw()
|
||||||
|
self.overrideredirect(True)
|
||||||
|
self.attributes("-topmost", True)
|
||||||
|
|
||||||
|
self.label = ctk.CTkLabel(
|
||||||
|
self, text=text, fg_color="gray20", corner_radius=6, padx=10, pady=5
|
||||||
|
)
|
||||||
|
self.label.pack()
|
||||||
|
|
||||||
|
self.widget = widget
|
||||||
|
widget.bind("<Enter>", self.show_tooltip)
|
||||||
|
widget.bind("<Leave>", self.hide_tooltip)
|
||||||
|
|
||||||
|
def show_tooltip(self, event=None):
|
||||||
|
x = self.widget.winfo_rootx() + 20
|
||||||
|
y = self.widget.winfo_rooty() + 20
|
||||||
|
self.geometry(f"+{x}+{y}")
|
||||||
|
self.deiconify()
|
||||||
|
|
||||||
|
def hide_tooltip(self, event=None):
|
||||||
|
self.withdraw()
|
||||||
@@ -0,0 +1,480 @@
|
|||||||
|
import os
|
||||||
|
import queue
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import threading
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import filedialog, messagebox, scrolledtext
|
||||||
|
from PIL import Image, ImageTk
|
||||||
|
|
||||||
|
import customtkinter as ctk
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from transcription.audio_transcription import AudioTranscription
|
||||||
|
from transcription.configuration import Configuration
|
||||||
|
from transcription.torch_checker import checkTorch
|
||||||
|
from ui.tooltip import ToolTip
|
||||||
|
from ui.ui_log_handler import setup_ui_logger
|
||||||
|
from utils.requests_to_api import LLMrequest
|
||||||
|
|
||||||
|
WINDOW_WIDTH = 1000
|
||||||
|
WINDOW_HEIGHT = 725
|
||||||
|
|
||||||
|
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: fix this stuff
|
||||||
|
base_path = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
icon_path = os.path.join(base_path, "assets", "logo.ico")
|
||||||
|
self.iconbitmap(icon_path)
|
||||||
|
|
||||||
|
# states
|
||||||
|
self.progress_queue = queue.Queue()
|
||||||
|
self.transcribe_thread = None
|
||||||
|
self.stop_flag = threading.Event()
|
||||||
|
|
||||||
|
# USER VARIABLES
|
||||||
|
# transcription model settings
|
||||||
|
self.model_var = tk.StringVar(value="openai/whisper-large-v3-turbo")
|
||||||
|
self.batch_var = tk.StringVar(value="32")
|
||||||
|
self.chunk_var = tk.StringVar(value="30")
|
||||||
|
self.dtype_var = tk.StringVar(value="torch.float16")
|
||||||
|
self.transcription_lang_var = tk.StringVar(value="ru")
|
||||||
|
# llm settings
|
||||||
|
self.conspect_transcription_lang_var = tk.StringVar(value="Russian")
|
||||||
|
self.api_key_var = tk.StringVar(value="")
|
||||||
|
self.base_url_var = tk.StringVar(value="")
|
||||||
|
self.api_model_var = tk.StringVar(value="")
|
||||||
|
# checkboxes
|
||||||
|
self.create_conspect = tk.BooleanVar(value=False)
|
||||||
|
self.remove_transcription = tk.BooleanVar(value=False)
|
||||||
|
self.latex_compiler = tk.StringVar(value="")
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# input & output file variables
|
||||||
|
self.input_file_var = tk.StringVar()
|
||||||
|
self.output_file_var = tk.StringVar()
|
||||||
|
|
||||||
|
# tabs packing
|
||||||
|
self.tabview = ctk.CTkTabview(self)
|
||||||
|
self.tabview.pack(expand=True, fill="both", padx=10, pady=10)
|
||||||
|
|
||||||
|
self.transcript_tab = self.tabview.add("Transcription")
|
||||||
|
self.settings_tab = self.tabview.add("Settings")
|
||||||
|
|
||||||
|
self._build_transcription_tab()
|
||||||
|
self._build_settings_tab()
|
||||||
|
|
||||||
|
# logger
|
||||||
|
self.ui_logger = setup_ui_logger(self.log_box)
|
||||||
|
|
||||||
|
### 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")
|
||||||
|
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
ctk.CTkButton(file_frame, text="Browse", command=self._browse_input).pack(
|
||||||
|
side="left", padx=5, pady=5
|
||||||
|
)
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
ctk.CTkButton(ctrl_frame, text="Check Torch", command=self._check_torch).pack(
|
||||||
|
side="left", padx=10, pady=5
|
||||||
|
)
|
||||||
|
self.start_button = ctk.CTkButton(
|
||||||
|
ctrl_frame, text="Start", command=self._start_transcription
|
||||||
|
)
|
||||||
|
self.start_button.pack(side="left", padx=10, pady=5)
|
||||||
|
|
||||||
|
self.stop_button = ctk.CTkButton(
|
||||||
|
ctrl_frame, text="Stop", command=self._stop_transcription, state="disabled"
|
||||||
|
)
|
||||||
|
self.stop_button.pack(side="left", padx=10, pady=5)
|
||||||
|
|
||||||
|
self.create_conspect_checkbox = ctk.CTkCheckBox(
|
||||||
|
ctrl_frame,
|
||||||
|
text="Create conspect",
|
||||||
|
variable=self.create_conspect,
|
||||||
|
onvalue=True,
|
||||||
|
offvalue=False,
|
||||||
|
)
|
||||||
|
self.create_conspect_checkbox.pack(side="left", padx=10, pady=5)
|
||||||
|
|
||||||
|
self.remove_transcription_checkbox = ctk.CTkCheckBox(
|
||||||
|
ctrl_frame,
|
||||||
|
text="Remove transcription file after",
|
||||||
|
variable=self.remove_transcription,
|
||||||
|
onvalue=True,
|
||||||
|
offvalue=False,
|
||||||
|
)
|
||||||
|
self.remove_transcription_checkbox.pack(side="left", padx=10, pady=5)
|
||||||
|
|
||||||
|
# TODO: add unload model button here
|
||||||
|
|
||||||
|
# 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")
|
||||||
|
|
||||||
|
### SETTINGS TAB
|
||||||
|
def _build_settings_tab(self):
|
||||||
|
pad = 20
|
||||||
|
|
||||||
|
def add_setting(parent, row, col, text, tooltip, variable, values: list | None):
|
||||||
|
frame = ctk.CTkFrame(parent)
|
||||||
|
frame.grid(row=row, column=col, padx=pad, pady=(pad, 5), sticky="nsew")
|
||||||
|
|
||||||
|
label = ctk.CTkLabel(frame, text=text)
|
||||||
|
label.grid(row=0, column=0, sticky="w")
|
||||||
|
help_icon = ctk.CTkLabel(frame, text="?", width=20, cursor="question_arrow")
|
||||||
|
help_icon.grid(row=0, column=1, sticky="w", padx=(5, 0))
|
||||||
|
ToolTip(help_icon, tooltip)
|
||||||
|
|
||||||
|
if values:
|
||||||
|
ctk.CTkOptionMenu(frame, variable=variable, values=values).grid(
|
||||||
|
row=1, column=0, columnspan=2, sticky="ew", pady=(5, 0)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
ctk.CTkEntry(frame, textvariable=variable).grid(
|
||||||
|
row=1, column=0, columnspan=2, sticky="ew", pady=(5, 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
frame.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
|
grid = ctk.CTkFrame(self.settings_tab)
|
||||||
|
grid.pack(fill="both", expand=True)
|
||||||
|
|
||||||
|
# first row
|
||||||
|
### Model setting
|
||||||
|
add_setting(
|
||||||
|
parent=grid,
|
||||||
|
row=0,
|
||||||
|
col=0,
|
||||||
|
text="Model:",
|
||||||
|
tooltip="Choose model for speech recognition",
|
||||||
|
variable=self.model_var,
|
||||||
|
values=[
|
||||||
|
"openai/whisper-large-v3-turbo",
|
||||||
|
"openai/whisper-large-v2",
|
||||||
|
"openai/whisper-large",
|
||||||
|
"openai/whisper-medium",
|
||||||
|
"openai/whisper-small",
|
||||||
|
"openai/whisper-tiny",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
### Batch size setting
|
||||||
|
add_setting(
|
||||||
|
parent=grid,
|
||||||
|
row=0,
|
||||||
|
col=1,
|
||||||
|
text="Batch size:",
|
||||||
|
tooltip="Chunks count for one iteration",
|
||||||
|
variable=self.batch_var,
|
||||||
|
values=["32", "16", "8", "4", "2"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# second row
|
||||||
|
### Data type setting
|
||||||
|
add_setting(
|
||||||
|
parent=grid,
|
||||||
|
row=1,
|
||||||
|
col=0,
|
||||||
|
text="Data type:",
|
||||||
|
tooltip="Data type for calculations",
|
||||||
|
variable=self.dtype_var,
|
||||||
|
values=["torch.float16", "torch.float32", "torch.bfloat16"],
|
||||||
|
)
|
||||||
|
### Chunk Length setting
|
||||||
|
add_setting(
|
||||||
|
parent=grid,
|
||||||
|
row=1,
|
||||||
|
col=1,
|
||||||
|
text="Chunk length (s):",
|
||||||
|
tooltip="Maximum length of processing audio fragment",
|
||||||
|
variable=self.chunk_var,
|
||||||
|
values=["30", "24", "20", "14", "10", "6"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# third row
|
||||||
|
### Device setting
|
||||||
|
add_setting(
|
||||||
|
parent=grid,
|
||||||
|
row=2,
|
||||||
|
col=0,
|
||||||
|
text="Device:",
|
||||||
|
tooltip="Choose device\n- CUDA for CUDA & ROCm\n- MPS for Apple Silicon \n- CPU for CPU-only mode",
|
||||||
|
variable=self.device_var,
|
||||||
|
values=self.device_opts,
|
||||||
|
)
|
||||||
|
### Transcription language setting
|
||||||
|
add_setting(
|
||||||
|
parent=grid,
|
||||||
|
row=2,
|
||||||
|
col=1,
|
||||||
|
text="Transcription language:",
|
||||||
|
tooltip="Choose the transcription language",
|
||||||
|
variable=self.transcription_lang_var,
|
||||||
|
values=["ru", "en"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# fourth row
|
||||||
|
### OpenAI API key setting
|
||||||
|
add_setting(
|
||||||
|
parent=grid,
|
||||||
|
row=3,
|
||||||
|
col=0,
|
||||||
|
text="Insert OpenAI API key here:",
|
||||||
|
tooltip="Give this programm access to LLM that would create a fully prepared conspect with AI overviews",
|
||||||
|
variable=self.api_key_var,
|
||||||
|
values=None,
|
||||||
|
)
|
||||||
|
### Model name setting
|
||||||
|
add_setting(
|
||||||
|
parent=grid,
|
||||||
|
row=3,
|
||||||
|
col=1,
|
||||||
|
text="Model name:",
|
||||||
|
tooltip="Name of the model that you are going to use",
|
||||||
|
variable=self.api_model_var,
|
||||||
|
values=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
# fifth row
|
||||||
|
### Base URL setting
|
||||||
|
add_setting(
|
||||||
|
parent=grid,
|
||||||
|
row=4,
|
||||||
|
col=0,
|
||||||
|
text="Base URL:",
|
||||||
|
tooltip="OpenAI base URL. Blank for None.",
|
||||||
|
variable=self.base_url_var,
|
||||||
|
values=None,
|
||||||
|
)
|
||||||
|
### Output (conspect) language setting
|
||||||
|
add_setting(
|
||||||
|
parent=grid,
|
||||||
|
row=4,
|
||||||
|
col=1,
|
||||||
|
text="Conspect language:",
|
||||||
|
tooltip="Conspect language. Blank for English (default)",
|
||||||
|
variable=self.conspect_transcription_lang_var,
|
||||||
|
values=None,
|
||||||
|
)
|
||||||
|
add_setting(
|
||||||
|
parent=grid,
|
||||||
|
row=4,
|
||||||
|
col=0,
|
||||||
|
text="LaTeX compiler:",
|
||||||
|
tooltip="Choose LaTeX compiler",
|
||||||
|
variable=self.latex_compiler,
|
||||||
|
values=[
|
||||||
|
"latexmk",
|
||||||
|
"lualatex",
|
||||||
|
"xelatex",
|
||||||
|
"bibtex"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
### Custom Prompt setting
|
||||||
|
customPromptFrame = ctk.CTkFrame(grid)
|
||||||
|
customPromptFrame.grid(
|
||||||
|
row=5, column=0, columnspan=2, padx=20, pady=(20, 5), sticky="nsew"
|
||||||
|
)
|
||||||
|
|
||||||
|
label = ctk.CTkLabel(customPromptFrame, text="Custom Prompt:")
|
||||||
|
label.grid(row=0, column=0, sticky="w")
|
||||||
|
|
||||||
|
help_icon = ctk.CTkLabel(
|
||||||
|
customPromptFrame, text="?", width=20, cursor="question_arrow"
|
||||||
|
)
|
||||||
|
help_icon.grid(row=0, column=1, sticky="w", padx=(5, 0))
|
||||||
|
ToolTip(
|
||||||
|
help_icon,
|
||||||
|
"Enter your custom prompt for model.",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.custom_prompt_textbox = ctk.CTkTextbox(
|
||||||
|
customPromptFrame, width=400, height=150
|
||||||
|
)
|
||||||
|
self.custom_prompt_textbox.grid(
|
||||||
|
row=1, column=0, columnspan=2, sticky="nsew", pady=(5, 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
customPromptFrame.grid_columnconfigure(0, weight=1)
|
||||||
|
customPromptFrame.grid_rowconfigure(1, weight=1)
|
||||||
|
|
||||||
|
grid.grid_columnconfigure((0, 1), weight=1)
|
||||||
|
|
||||||
|
# action buttons
|
||||||
|
def _browse_input(self):
|
||||||
|
path = filedialog.askopenfilename(
|
||||||
|
title="Select input audio file",
|
||||||
|
filetypes=[
|
||||||
|
("Media files", "*.wav *.mp3 *.m4a *.flac *.ogg *.mp4 *.mkv *.avi"),
|
||||||
|
("Audio files", "*.wav *.mp3 *.m4a *.flac *.ogg"),
|
||||||
|
("Video files", "*.mp4 *.mkv *.avi"),
|
||||||
|
("All files", "*.*"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
if path:
|
||||||
|
self.input_file_var.set(path)
|
||||||
|
|
||||||
|
def _browse_output(self):
|
||||||
|
# TODO: add custom filename here
|
||||||
|
directory = filedialog.askdirectory(
|
||||||
|
title="Select output directory",
|
||||||
|
)
|
||||||
|
if directory:
|
||||||
|
input_name = os.path.basename(self.input_file_var.get())
|
||||||
|
name, _ = os.path.splitext(input_name)
|
||||||
|
# TODO: redo output_name logic maybe?
|
||||||
|
output_name = f"{"".join(name.split(".")[:-1:])}.txt"
|
||||||
|
path = os.path.join(directory, output_name)
|
||||||
|
|
||||||
|
self.output_file_var.set(path)
|
||||||
|
|
||||||
|
def _check_torch(self):
|
||||||
|
checkTorch(self.ui_logger)
|
||||||
|
|
||||||
|
self.ui_logger.info(f"==== Transcription ====")
|
||||||
|
self.ui_logger.info(f"Transcription model: {self.model_var.get()}")
|
||||||
|
self.ui_logger.info(f"Batch size (in chunks): {self.batch_var.get()}")
|
||||||
|
self.ui_logger.info(f"Chunk size (in seconds): {self.chunk_var.get()}")
|
||||||
|
self.ui_logger.info(f"Data type: {self.dtype_var.get()}")
|
||||||
|
self.ui_logger.info(f"Transcription language: {self.transcription_lang_var.get()}")
|
||||||
|
self.ui_logger.info(f"=======================")
|
||||||
|
|
||||||
|
debug_api_key_var: str = self.api_key_var.get() if self.api_key_var.get() else "Not set"
|
||||||
|
debug_api_model_var: str = self.api_model_var.get() if self.api_model_var.get() else "Not set"
|
||||||
|
debug_base_url_var: str = self.base_url_var.get() if self.base_url_var.get() else "Not set"
|
||||||
|
debug_transcription_lang_var = self.transcription_lang_var.get() if self.transcription_lang_var.get() else "Not set"
|
||||||
|
debug_custom_prompt_var: str = self.custom_prompt_textbox.get() if self.custom_prompt_textbox.get() else "Not set"
|
||||||
|
|
||||||
|
self.ui_logger.info(f"===== LLM Setting =====")
|
||||||
|
self.ui_logger.info(f"API key: {debug_api_key_var}")
|
||||||
|
self.ui_logger.info(f"Model name setting: {debug_api_model_var}")
|
||||||
|
self.ui_logger.info(f"Base URL setting: {debug_base_url_var}")
|
||||||
|
# self.ui_logger.info(f"Custom prompt: {debug_transcription_lang_var}") # <-- issue here
|
||||||
|
self.ui_logger.info(f"=======================")
|
||||||
|
|
||||||
|
|
||||||
|
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_button.configure(state="disabled")
|
||||||
|
self.stop_button.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, Audio: AudioTranscription):
|
||||||
|
self.stop_flag.set()
|
||||||
|
self.ui_logger.info("Stopping transcription...")
|
||||||
|
self.ui_logger.info("Unloading model...")
|
||||||
|
Audio._unload_model()
|
||||||
|
self.stop_button.configure(state="disabled")
|
||||||
|
|
||||||
|
def _transcribe_worker(self, infile: str):
|
||||||
|
try:
|
||||||
|
config = Configuration(
|
||||||
|
device=self.device_var.get(),
|
||||||
|
modelName=self.model_var.get(),
|
||||||
|
batchSize=int(self.batch_var.get()),
|
||||||
|
chunkSize=int(self.chunk_var.get()),
|
||||||
|
data_type=self.dtype_var.get(),
|
||||||
|
)
|
||||||
|
Audio = AudioTranscription(
|
||||||
|
filepath=infile,
|
||||||
|
config=config,
|
||||||
|
language=self.transcription_lang_var.get(),
|
||||||
|
)
|
||||||
|
transcription = Audio.transcribeAudio()
|
||||||
|
|
||||||
|
# remove from here
|
||||||
|
outfile = self.output_file_var.get().strip()
|
||||||
|
if not outfile:
|
||||||
|
outfile = infile
|
||||||
|
|
||||||
|
if not self.remove_transcription.get():
|
||||||
|
outfile += ".txt"
|
||||||
|
|
||||||
|
with open(outfile, "w", encoding="utf-8") as f:
|
||||||
|
f.write(transcription)
|
||||||
|
self.ui_logger.info(f"Transcription saved to {outfile}.")
|
||||||
|
|
||||||
|
# if self.create_conspect.get():
|
||||||
|
# # TODO: add custom prompt ability here
|
||||||
|
# # TODO: add logging here
|
||||||
|
# # TODO: add progressbar instead of tqdm
|
||||||
|
# self.ui_logger.info(f"Starting creating conspect via {self.api_model_var.get()}...")
|
||||||
|
# with open("utils/prompts/default_prompt.txt", "r", encoding="utf-8") as f:
|
||||||
|
# default_prompt = "\n".join(f.readlines())
|
||||||
|
|
||||||
|
# # if self.custom_prompt_textbox.get(): # <-- issue here
|
||||||
|
# # prompt = transcription + "\n" + self.custom_prompt_textbox.get()
|
||||||
|
# # else:
|
||||||
|
# # prompt = transcription + "\n" + default_prompt
|
||||||
|
|
||||||
|
# prompt = transcription + "\n" + default_prompt
|
||||||
|
|
||||||
|
# request = LLMrequest(
|
||||||
|
# api_key=self.api_key_var.get(),
|
||||||
|
# model_name=self.api_model_var.get(),
|
||||||
|
# base_url=self.base_url_var.get(),
|
||||||
|
# )
|
||||||
|
# response = request.get_response(prompt=prompt)
|
||||||
|
# outfile += ".md"
|
||||||
|
# with open(outfile, "w", encoding="utf-8") as f:
|
||||||
|
# f.write(response)
|
||||||
|
|
||||||
|
# self.ui_logger.info(f"Conspect saved to {outfile}.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.ui_logger.error(f"Error: {e}")
|
||||||
|
messagebox.showerror("Error", str(e))
|
||||||
|
finally:
|
||||||
|
self.start_button.configure(state="normal")
|
||||||
|
self.stop_button.configure(state="disabled")
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import logging
|
||||||
|
import tkinter as tk
|
||||||
|
|
||||||
|
|
||||||
|
class UILogHandler(logging.Handler):
|
||||||
|
def __init__(self, text_widget: tk.Text):
|
||||||
|
super().__init__()
|
||||||
|
self.text_widget = text_widget
|
||||||
|
|
||||||
|
def emit(self, record):
|
||||||
|
log_entry = self.format(record)
|
||||||
|
self.text_widget.insert(tk.END, log_entry + "\n")
|
||||||
|
self.text_widget.see(tk.END)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: status should be here
|
||||||
|
def setup_ui_logger(text_widget: tk.Text, level=logging.INFO):
|
||||||
|
logger = logging.getLogger("UI_LOGGER")
|
||||||
|
logger.setLevel(level)
|
||||||
|
logger.handlers.clear()
|
||||||
|
|
||||||
|
handler = UILogHandler(text_widget)
|
||||||
|
handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
|
||||||
|
logger.addHandler(handler)
|
||||||
|
|
||||||
|
return logger
|
||||||
|
|
||||||
|
# def setup_ui_logger(
|
||||||
|
# text_widget: tk.Text,
|
||||||
|
# base_logger: logging.Logger,
|
||||||
|
# level=logging.INFO,
|
||||||
|
# ):
|
||||||
|
# handler = UILogHandler(text_widget)
|
||||||
|
# handler.setLevel(level)
|
||||||
|
# handler.setFormatter(
|
||||||
|
# logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
|
||||||
|
# )
|
||||||
|
|
||||||
|
# base_logger.addHandler(handler)
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# not sure, but why not?
|
||||||
|
|
||||||
|
class AudioPreparationError(Exception):
|
||||||
|
"""Base exception for audio preparation"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ResamplingError(AudioPreparationError):
|
||||||
|
"""Raises when resampling fails"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class DownmixingError(AudioPreparationError):
|
||||||
|
"""Raises when downmixing to mono fails"""
|
||||||
|
pass
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
Create a complete, structured **Obsidian Markdown** educational summary based on the transcript.
|
||||||
|
|
||||||
|
The final document must be written in **{{ language }}**.
|
||||||
|
|
||||||
|
Use the transcript strictly as the factual basis:
|
||||||
|
- Do NOT invent facts.
|
||||||
|
- You may add clarifications and explanations, but they MUST be placed in special blocks.
|
||||||
|
- Preserve the logical order and meaning of the original speaker.
|
||||||
|
- Transform speech into clear academic exposition.
|
||||||
|
- Quotes must be accurate and **<= 25 words**.
|
||||||
|
- Do not reference the transcript or the speaker. Write as a stand-alone educational text.
|
||||||
|
|
||||||
|
You must follow the template below exactly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# {{ title }}
|
||||||
|
|
||||||
|
## Key Ideas
|
||||||
|
- bullet points summarizing the main concepts
|
||||||
|
|
||||||
|
## Core Concepts and Definitions
|
||||||
|
- **Term** — definition
|
||||||
|
- **Another Term** — definition
|
||||||
|
|
||||||
|
## Detailed Summary
|
||||||
|
Write a logically structured, academically clear exposition.
|
||||||
|
Break it into paragraphs, subsections, lists, and quotes.
|
||||||
|
|
||||||
|
To include quotes from the transcript, use:
|
||||||
|
|
||||||
|
> "{{ quote }}"
|
||||||
|
(Ensure that every quote is ≤ 25 words.)
|
||||||
|
|
||||||
|
## Examples and Explanations
|
||||||
|
Use examples that appear in the transcript.
|
||||||
|
Additional clarifications must use the following block:
|
||||||
|
|
||||||
|
> AI clarification:
|
||||||
|
> Your extended explanation, analogy, or contextual detail.
|
||||||
|
|
||||||
|
Short notes should use:
|
||||||
|
|
||||||
|
**AI note:** short clarification
|
||||||
|
|
||||||
|
## Conclusions and Key Insights
|
||||||
|
- summary points
|
||||||
|
- what the reader should remember
|
||||||
|
|
||||||
|
## Self-Assessment Questions
|
||||||
|
- question 1
|
||||||
|
- question 2
|
||||||
|
- question 3
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Transcript (for your reference only):
|
||||||
|
{{ transcription }}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
Create a clean, structured **LaTeX educational summary** based strictly on the transcript.
|
||||||
|
|
||||||
|
The final document must be written in **{{ language }}**.
|
||||||
|
|
||||||
|
Guidelines:
|
||||||
|
- Use ONLY facts from the transcript as the base.
|
||||||
|
- You MAY add explanations, but they MUST appear in designated blocks.
|
||||||
|
- Do NOT reference the transcript or the speaker.
|
||||||
|
- Maintain the speaker`s logical order.
|
||||||
|
- Produce academically clear, structured LaTeX text.
|
||||||
|
- Quotes must be accurate and <= 25 words.
|
||||||
|
|
||||||
|
Use the structure below.
|
||||||
|
Assume that stylistic environments are already defined in the `.sty` file.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
\section*{ {{ title }} }
|
||||||
|
|
||||||
|
\subsection*{Key Ideas}
|
||||||
|
\begin{itemize}
|
||||||
|
\item main idea
|
||||||
|
\item …
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
\subsection*{Core Concepts and Definitions}
|
||||||
|
\begin{description}
|
||||||
|
\item[\textbf{Term}] definition
|
||||||
|
\item[\textbf{Another Term}] definition
|
||||||
|
\end{description}
|
||||||
|
|
||||||
|
\subsection*{Detailed Summary}
|
||||||
|
Write a clear, logically structured academic explanation.
|
||||||
|
Break content into paragraphs, subsections, lists.
|
||||||
|
|
||||||
|
Quotes from the transcript must be included as:
|
||||||
|
|
||||||
|
\begin{quote}
|
||||||
|
"{{ quote }}"
|
||||||
|
\end{quote}
|
||||||
|
|
||||||
|
(Ensure each quote is ≤ 25 words.)
|
||||||
|
|
||||||
|
\subsection*{Examples and Explanations}
|
||||||
|
|
||||||
|
Examples based on the transcript.
|
||||||
|
|
||||||
|
Additional clarifications must use the following custom environment (assumed to exist in the .sty):
|
||||||
|
|
||||||
|
\begin{aiclarification}
|
||||||
|
Your additional explanation, analogy, or contextual expansion.
|
||||||
|
\end{aiclarification}
|
||||||
|
|
||||||
|
Short notes should use:
|
||||||
|
|
||||||
|
\ainote{short clarification}
|
||||||
|
|
||||||
|
\subsection*{Conclusions and Key Insights}
|
||||||
|
\begin{itemize}
|
||||||
|
\item conclusion
|
||||||
|
\item conclusion
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
\subsection*{Self-Assessment Questions}
|
||||||
|
\begin{enumerate}
|
||||||
|
\item question
|
||||||
|
\item question
|
||||||
|
\item question
|
||||||
|
\end{enumerate}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
% Transcript included only for reference (model must not copy or analyze or mention this directly)
|
||||||
|
% ======================================================
|
||||||
|
% {{ transcription | replace("%", "\\%") }}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from jinja2 import Environment, FileSystemLoader, StrictUndefined
|
||||||
|
|
||||||
|
|
||||||
|
class PromptManager:
|
||||||
|
"""
|
||||||
|
Self-written class for Jinja2 templates especially for summarization.
|
||||||
|
|
||||||
|
Supports:
|
||||||
|
- md
|
||||||
|
- latex
|
||||||
|
|
||||||
|
Usage examples:
|
||||||
|
```
|
||||||
|
from prompts.prompt_manager import PromptManager
|
||||||
|
|
||||||
|
pm = PromptManager("notecast/prompts")
|
||||||
|
|
||||||
|
markdown_text = pm.render(
|
||||||
|
"markdown_default_prompt.md.j2",
|
||||||
|
language="Russian",
|
||||||
|
title="Calculus - Lecture 1",
|
||||||
|
transcription=raw_transcription_text
|
||||||
|
)
|
||||||
|
|
||||||
|
latex_text = pm.render(
|
||||||
|
"latex_default_prompt.j2",
|
||||||
|
language="Russian",
|
||||||
|
title="Calculus - Lecture 1",
|
||||||
|
transcription=raw_transcription_text
|
||||||
|
)
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, templates_dir: str) -> None:
|
||||||
|
if not os.path.isdir(templates_dir):
|
||||||
|
raise NotADirectoryError(f"Templates directory not found: {templates_dir}")
|
||||||
|
|
||||||
|
self.templates_dir = templates_dir
|
||||||
|
|
||||||
|
self.env = Environment(
|
||||||
|
loader=FileSystemLoader(self.templates_dir),
|
||||||
|
autoescape=False,
|
||||||
|
trim_blocks=True,
|
||||||
|
lstrip_blocks=True,
|
||||||
|
undefined=StrictUndefined
|
||||||
|
)
|
||||||
|
|
||||||
|
def list_templates(self) -> list[str]:
|
||||||
|
templates = []
|
||||||
|
for file in os.listdir(self.templates_dir):
|
||||||
|
if file.endswith(".j2"):
|
||||||
|
templates.append(file)
|
||||||
|
return templates
|
||||||
|
|
||||||
|
def load(self, template_name: str):
|
||||||
|
try:
|
||||||
|
return self.env.get_template(template_name)
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeError(f"Failed to load template '{template_name}': {e}")
|
||||||
|
|
||||||
|
def render(
|
||||||
|
self,
|
||||||
|
template_name: str,
|
||||||
|
*,
|
||||||
|
language: str,
|
||||||
|
title: str,
|
||||||
|
transcription: str
|
||||||
|
) -> str:
|
||||||
|
template = self.load(template_name)
|
||||||
|
|
||||||
|
return template.render(
|
||||||
|
language=language,
|
||||||
|
title=title,
|
||||||
|
transcription=transcription
|
||||||
|
)
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import openai
|
||||||
|
|
||||||
|
# maybe make it asynchronous?
|
||||||
|
class LLMrequest:
|
||||||
|
def __init__(self, api_key: str, model_name: str, base_url: str = None):
|
||||||
|
if base_url:
|
||||||
|
self.client = openai.OpenAI(api_key=api_key, base_url=base_url)
|
||||||
|
else:
|
||||||
|
self.client = openai.OpenAI(api_key=api_key)
|
||||||
|
self.model = model_name
|
||||||
|
|
||||||
|
def get_response(self, prompt: str) -> str:
|
||||||
|
response = self.client.chat.completions.create(
|
||||||
|
model=self.model,
|
||||||
|
messages=[{"role": "user", "content": prompt}],
|
||||||
|
)
|
||||||
|
return response.choices[0].message.content
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
def save_to_file(
|
||||||
|
thing: str | dict,
|
||||||
|
path: str,
|
||||||
|
):
|
||||||
|
if isinstance(thing, dict):
|
||||||
|
pass
|
||||||
|
elif isinstance(thing, str):
|
||||||
|
with open(path, "w") as outputfile:
|
||||||
|
outputfile.write(thing)
|
||||||
Reference in New Issue
Block a user