working version. something wrong with icons

This commit is contained in:
2025-10-16 14:24:49 +03:00
parent 414cb4d38e
commit add2e15d5f
12 changed files with 371 additions and 104 deletions
+3
View File
@@ -12,3 +12,6 @@ wheels/
main.todo
sample.mp3
*.spec
# лютый завоз
ui/assets/question_mark.png
+1 -1
View File
@@ -11,5 +11,5 @@ 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 -c conda-forge -c bioconda
conda install ffmpeg customtkinter openai -c conda-forge -c bioconda
```
+2 -1
View File
@@ -6,9 +6,10 @@ channels:
- bioconda
dependencies:
- pytorch
- ffmpeg
- torchvision
- torchaudio
- transformers
- python=3.12
- ffmpeg
- customtkinter
- openai
-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!"
+35 -6
View File
@@ -1,6 +1,8 @@
import logging
import math
import time
import sys
import gc
import torch
import torchaudio
@@ -27,8 +29,8 @@ class AudioTranscription:
custom_batch_length: int
device = "cuda"
processor: WhisperProcessor
model: WhisperForConditionalGeneration
processor: WhisperProcessor | None
model: WhisperForConditionalGeneration | None
logger: logging.Logger
torch_dtype: torch.dtype
@@ -41,7 +43,7 @@ class AudioTranscription:
filepath: str,
device_configuration: DeviceConfiguration,
logger: logging.Logger,
language="ru",
language: str = "ru",
) -> None:
# TODO: add pretty docs here
self.filepath = filepath
@@ -65,15 +67,29 @@ class AudioTranscription:
def _load_model(self) -> None:
self.logger.info("Loading model WhisperProcessor...")
try:
start_time = time.time()
self.processor = WhisperProcessor.from_pretrained(self.model_name)
self.model = WhisperForConditionalGeneration.from_pretrained(
self.model_name, torch_dtype = self.torch_dtype
self.model_name, torch_dtype=self.torch_dtype
).to(self.device)
self.logger.info("Model loaded.")
end_time = time.time()
self.logger.info(
f"Model loaded successfully in {end_time - start_time:.2f} seconds."
)
except Exception as e:
self.logger.error(f"Error while loading model: {e}")
def _unload_model(self) -> None:
self.logger.info("Unloading model...")
self.model = None
self.processor = None
if self.device == "cuda":
torch.cuda.empty_cache()
# TODO: maybe do something here for MPS
self.logger.info("Model unloaded successfully.")
def _load_file(self) -> None:
self.logger.info(f"Loading file {self.filepath}")
try:
@@ -118,11 +134,14 @@ class AudioTranscription:
for i in range(0, len(self.chunks), self.custom_batch_length):
batch = self.chunks[i : i + self.custom_batch_length]
self.batches.append(batch)
self.logger.info(f"Total: {len(self.batches)} batches")
self.logger.info(f"Total: {len(self.batches)} batches, weight = {sys.getsizeof(self.batches)}")
def _process_all_batches(self) -> None:
start_time = time.time()
try:
assert self.processor is not None
assert self.model is not None
self.all_transcription = []
for idx in tqdm(range(len(self.batches))):
@@ -149,6 +168,15 @@ class AudioTranscription:
predicted_ids, skip_special_tokens=True
)
self.all_transcription.extend(texts)
inputs = None
input_features = None
predicted_ids = None
gc.collect()
if self.device.startswith("cuda"):
torch.cuda.empty_cache()
torch.cuda.ipc_collect()
end_time = time.time()
self.logger.info(
f"Transcription completed in {end_time - start_time:.2f} seconds"
@@ -165,4 +193,5 @@ class AudioTranscription:
self._split_to_chunks()
self._resplit_chunks_to_batches()
self._process_all_batches()
self._unload_model()
return " ".join(self.all_transcription)
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

+4 -1
View File
@@ -1,5 +1,6 @@
import customtkinter as ctk
class ToolTip(ctk.CTkToplevel):
def __init__(self, widget, text):
super().__init__()
@@ -7,7 +8,9 @@ class ToolTip(ctk.CTkToplevel):
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 = ctk.CTkLabel(
self, text=text, fg_color="gray20", corner_radius=6, padx=10, pady=5
)
self.label.pack()
self.widget = widget
+284 -56
View File
@@ -1,8 +1,11 @@
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
@@ -10,11 +13,12 @@ import torch
from transcription.audio_transcription import AudioTranscription
from transcription.device_configuration import DeviceConfiguration
from transcription.torch_checker import check_torch
from ui.ui_log_handler import setup_ui_logger
from ui.tooltip import ToolTip
from ui.ui_log_handler import setup_ui_logger
from utils.requests_to_api import LLMrequest
WINDOW_WIDTH = 900
WINDOW_HEIGHT = 650
WINDOW_WIDTH = 1000
WINDOW_HEIGHT = 725
class TranscriberApp(ctk.CTk):
@@ -25,16 +29,60 @@ class TranscriberApp(ctk.CTk):
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.png")
applied_method = None
try:
self.iconbitmap(icon_path)
applied_method = "iconbitmap"
except Exception as e:
print("iconbitmap did not work:", e)
if applied_method is None:
try:
img = tk.PhotoImage(file=icon_path)
self.iconphoto(False, img)
applied_method = "iconphoto"
except Exception as e:
print("iconphoto did not work:", e)
if applied_method is None:
try:
pil = Image.open(icon_path)
photo = ImageTk.PhotoImage(pil)
self.tk.call('wm', 'iconphoto', self._w, photo)
applied_method = "tk.call"
except Exception as e:
print("wm iconphoto via tk.call did not work:", e)
if applied_method:
print(f"Applied method: {applied_method}")
else:
print("Can't set an icon.")
# states
self.progress_queue = queue.Queue()
self.transcribe_thread = None
self.stop_flag = threading.Event()
# user variables
self.model_var = tk.StringVar(value="openai/whisper-large-v2")
# USER VARIABLES
# transcription model settings
self.model_var = tk.StringVar(value="openai/whisper-large-v3-turbo")
self.batch_var = tk.StringVar(value="32")
self.dtype_var = tk.StringVar(value="torch.float16")
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)
# settings device options
device_opts = []
@@ -62,7 +110,7 @@ class TranscriberApp(ctk.CTk):
# logger
self.ui_logger = setup_ui_logger(self.log_box)
# main transcription tab
### TRANSCRIPTION TAB
def _build_transcription_tab(self):
# file selectors
file_frame = ctk.CTkFrame(self.transcript_tab, corner_radius=10)
@@ -93,15 +141,33 @@ class TranscriberApp(ctk.CTk):
ctk.CTkButton(ctrl_frame, text="Check Torch", command=self._check_torch).pack(
side="left", padx=10, pady=5
)
self.start_btn = ctk.CTkButton(
ctrl_frame, text="Start transcription", command=self._start_transcription
self.start_button = ctk.CTkButton(
ctrl_frame, text="Start", command=self._start_transcription
)
self.start_btn.pack(side="left", padx=10, pady=5)
self.start_button.pack(side="left", padx=10, pady=5)
self.stop_btn = ctk.CTkButton(
self.stop_button = ctk.CTkButton(
ctrl_frame, text="Stop", command=self._stop_transcription, state="disabled"
)
self.stop_btn.pack(side="left", padx=10, pady=5)
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
@@ -114,65 +180,181 @@ class TranscriberApp(ctk.CTk):
)
self.log_box.pack(padx=20, pady=10, expand=True, fill="both")
### SETTINGS TAB
def _build_settings_tab(self):
# TODO: add tooltips here
pad = 20
### Model
ctk.CTkLabel(self.settings_tab, text="Model:").pack(
anchor="w", padx=pad, pady=(pad, 5)
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)
)
ctk.CTkOptionMenu(
self.settings_tab,
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 speed recognition",
variable=self.model_var,
values=[
# maybe delete this option?
"openai/whisper-large-v3-turbo",
"openai/whisper-large-v2",
"openai/whisper-large",
"openai/whisper-medium",
"openai/whisper-small",
"openai/whisper-tiny",
],
).pack(fill="x", padx=pad, pady=5)
### Batch size
ctk.CTkLabel(self.settings_tab, text="Batch size:").pack(
anchor="w", padx=pad, pady=(pad, 5)
)
ctk.CTkOptionMenu(
self.settings_tab,
### 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"],
).pack(fill="x", padx=pad, pady=5)
### Data type
ctk.CTkLabel(self.settings_tab, text="Data type:").pack(
anchor="w", padx=pad, pady=(pad, 5)
)
ctk.CTkOptionMenu(
self.settings_tab,
# 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"],
).pack(fill="x", padx=pad, pady=5)
### Chunk length
ctk.CTkLabel(self.settings_tab, text="Chunk length (s):").pack(
anchor="w", padx=pad, pady=(pad, 5)
)
ctk.CTkOptionMenu(
self.settings_tab,
### 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", "25", "20", "15", "10", "5"],
).pack(fill="x", padx=pad, pady=5)
### Device
ctk.CTkLabel(self.settings_tab, text="Device:").pack(
anchor="w", padx=pad, pady=(pad, 5)
)
ctk.CTkOptionMenu(
self.settings_tab,
# 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,
).pack(fill="x", padx=pad, pady=5)
)
### 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,
)
### 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):
@@ -183,7 +365,7 @@ class TranscriberApp(ctk.CTk):
("Audio files", "*.wav *.mp3 *.m4a *.flac *.ogg"),
("Video files", "*.mp4 *.mkv *.avi"),
("All files", "*.*"),
]
],
)
if path:
self.input_file_var.set(path)
@@ -205,14 +387,34 @@ class TranscriberApp(ctk.CTk):
def _check_torch(self):
check_torch(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"
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"=======================")
def _start_transcription(self):
infile = self.input_file_var.get().strip()
if not infile or not os.path.isfile(infile):
messagebox.showerror("Error", "Please select a valid input file.")
return
self.start_btn.configure(state="disabled")
self.stop_btn.configure(state="normal")
self.start_button.configure(state="disabled")
self.stop_button.configure(state="normal")
self.ui_logger.info("Starting transcription...")
self.stop_flag.clear()
@@ -221,10 +423,12 @@ class TranscriberApp(ctk.CTk):
)
self.transcribe_thread.start()
def _stop_transcription(self):
def _stop_transcription(self, Audio: AudioTranscription):
self.stop_flag.set()
self.ui_logger.info("Stopping transcription...")
self.stop_btn.configure(state="disabled")
self.ui_logger.info("Unloading model...")
Audio._unload_model()
self.stop_button.configure(state="disabled")
def _transcribe_worker(self, infile: str):
try:
@@ -239,20 +443,44 @@ class TranscriberApp(ctk.CTk):
filepath=infile,
device_configuration=config,
logger=self.ui_logger,
language=self.transcription_lang_var.get(),
)
transcription = Audio.transcribe_audio()
outfile = self.output_file_var.get().strip()
if not outfile:
outfile = infile + ".txt"
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
self.ui_logger.info(f"Starting creating conspect via {self.api_model_var.get()}...")
with open("utils/default_prompt.json", "r", encoding="utf-8") as f:
default_prompt = json.load(f)["prompt"]
prompt = transcription + default_prompt # if custom prompt is empty do something
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}.")
self.ui_logger.info(f"Transcription saved to {outfile}")
except Exception as e:
self.ui_logger.error(f"Error: {e}")
messagebox.showerror("Error", str(e))
finally:
self.start_btn.configure(state="normal")
self.stop_btn.configure(state="disabled")
self.start_button.configure(state="normal")
self.stop_button.configure(state="disabled")
+3
View File
@@ -0,0 +1,3 @@
{
"prompt": "Используй ТОЛЬКО текст из транскрибации. Любая информация, которой нет в транскрибации, запрещена. Если какой-то факт или определение раскрыт не до конца, или лектор сказал что-то неполно, или тему нужно дополнить для лучшего понимания — вставляй дополнение сразу в нужное место, но только под отдельной подписью: AI overview: текст дополнения (confidence: low|medium|high) Ответ должен быть строго в формате Markdown (.md). Никаких 'привет', 'вот результат' и т. п. — только готовый текст. Все цитаты из транскрибации — буквально, максимум 25 слов подряд."
}
+17
View File
@@ -0,0 +1,17 @@
import openai
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)