Compare commits

...

10 Commits

Author SHA1 Message Date
svlqd 3d48f473b0 new structure for transcription engine
- found problems with mps
- integrated jinja2 templates for rendering (.tex?)
- raw ui & bad structure
2026-02-24 02:58:28 +03:00
svlqd 9e67b36842 added correct envs
- env.yml needs fixing (not working)
- ui.py still needs improvements
2025-11-13 12:00:38 +03:00
svlqd 6147eb1597 Icon fixed 2025-11-04 20:23:26 +03:00
svlqd 7f92a27b49 Minor improvements & docs added 2025-11-04 15:57:52 +03:00
svlqd 633ac7e081 minor corrections 2025-10-16 16:37:50 +03:00
svlqd add2e15d5f working version. something wrong with icons 2025-10-16 14:24:49 +03:00
svlqd 414cb4d38e Working version
- audio/video files support added
2025-09-17 00:59:43 +03:00
svlqd 7291b148e5 isort & black
- trying to fix environment issues
2025-09-14 03:07:06 +04:00
svlqd 7cf4c5259b Major updates
- UI is now on customtkinter
- new environment.yml file
- entry point is now in main.py
- minor improvements in audio_transcription
2025-09-12 17:43:53 +03:00
svlqd 8f41105e4b WIP - first working version with UI, hooray!
- added logging in UI
- added switcher for models, batch size etc.
- added device configuration dataclass
- minor improvements in audio transcription
2025-09-09 00:49:21 +03:00
37 changed files with 1190 additions and 425 deletions
+4 -1
View File
@@ -10,4 +10,7 @@ wheels/
.venv .venv
main.todo main.todo
sample.mp3 sample.mp3
*.spec
ui/assets/question_mark.png
+6 -1
View File
@@ -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
``` ```
+15
View File
@@ -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
+15
View File
@@ -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
View File
@@ -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
+4 -12
View File
@@ -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}")
-13
View File
@@ -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!"
-13
View File
@@ -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!"
View File
+17
View File
@@ -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
+40 -133
View File
@@ -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 = [] self.audio = Audio()
try: self.preprocessor = AudioPreprocessor()
logger.info("Loading model WhisperProcessor...") self.splitter = Splitter(
self.processor = WhisperProcessor.from_pretrained(self.model_name) chunkSize=config.chunkSize,
batchSize=config.batchSize,
self.model = WhisperForConditionalGeneration.from_pretrained( )
self.model_name, self.engine = WhisperEngine(
torch_dtype=torch.float16, modelName=config.modelName,
device_map="auto" language=self.language,
).to(self.device) dType=config.dType,
device=config.device,
logger.info("Model loaded.") )
self.waveform, self.sampling_rate = torchaudio.load(filepath, format="mp3", backend="ffmpeg") # maybe add something like temperature here?
logger.info(f"Successfully loaded file {filepath}.") def transcribeAudio(self) -> str:
transcription: list = []
except Exception as e: self.engine.loadModel()
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 self.preprocessor.prepare(self.audio.load(self.filepath))
total_samples = self.waveform.shape[0]
chunks_count = (total_samples + self.chunk_size - 1) // self.chunk_size batches = self.splitter.split(self.audio.waveform)
for batch in batches:
batchText: str = self.engine.transcribeBatch(batch)
transcription.append(batchText)
self.engine.unloadModel()
logger.info(f"File length - {total_samples / 16000:.1f} seconds, splitting on {chunks_count} chunks by {chunk_length_s} seconds per chunk.") return str(" ".join(transcription))
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)
+23
View File
@@ -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]
View File
+30
View File
@@ -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
+65
View File
@@ -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)
+50
View File
@@ -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
+33 -8
View File
@@ -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 ===")
-13
View File
@@ -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()
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

+18
View File
@@ -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
View File
@@ -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])
+1
View File
@@ -0,0 +1 @@
# new structure coming soon
+1
View File
@@ -0,0 +1 @@
# new structure coming soon
+1
View File
@@ -0,0 +1 @@
# new structure coming soon
+27
View File
@@ -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()
+480
View File
@@ -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")
+39
View File
@@ -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)
+13
View File
@@ -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
-36
View File
@@ -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
+58
View File
@@ -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 }}
+75
View File
@@ -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("%", "\\%") }}
+78
View File
@@ -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
)
+17
View File
@@ -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
+9
View File
@@ -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)