Adding support for Vulkan for whisper.cpp (#1194)

This commit is contained in:
Raivis Dejus 2025-07-20 16:53:36 +03:00 committed by GitHub
commit 93afbead37
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 1161 additions and 382 deletions

View file

@ -1,6 +1,7 @@
[run]
omit =
buzz/whisper_cpp.py
buzz/whisper_cpp/*
buzz/whisper_cpp_vulkan/*
*_test.py
demucs/*

View file

@ -70,8 +70,9 @@ jobs:
poetry add torch==2.2.2 torchaudio==2.2.2
if: "matrix.os == 'macos-13'"
- name: Install dependencies
run: poetry install
- name: Add msbuild to PATH
uses: microsoft/setup-msbuild@v2
if: runner.os == 'Windows'
- name: Install apt dependencies
run: |
@ -82,6 +83,15 @@ jobs:
sudo apt-get install libyaml-dev libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-shape0 libxcb-cursor0 libportaudio2 gettext libpulse0 libgl1-mesa-dev
if: "startsWith(matrix.os, 'ubuntu-')"
- name: Install Vulkan SDK
uses: humbletim/install-vulkan-sdk@v1.2
with:
version: 1.4.309.0
cache: true
- name: Install dependencies
run: poetry install
- name: Test
run: |
poetry run make test
@ -132,6 +142,12 @@ jobs:
- uses: AnimMouse/setup-ffmpeg@v1.2.1
id: setup-ffmpeg
- name: Install Vulkan SDK
uses: humbletim/install-vulkan-sdk@v1.2
with:
version: 1.4.309.0
cache: true
# Adding windows Nvidia CUDA torch and its dependencies
# Linux only packages were removed from the list.
- name: Install CUDA torch for Windows
@ -153,6 +169,10 @@ jobs:
poetry add torch==2.2.2 torchaudio==2.2.2 ctranslate2==4.3.1
if: "matrix.os == 'macos-13'"
- name: Add msbuild to PATH
uses: microsoft/setup-msbuild@v2
if: runner.os == 'Windows'
- name: Install dependencies
run: poetry install
@ -255,19 +275,12 @@ jobs:
with:
submodules: recursive
- name: Copy Windows DLLs
run: |
if [ "$RUNNER_OS" == "Windows" ]; then
cp -r ./dll_backup ./buzz/
fi
shell: bash
- name: Build wheels
uses: pypa/cibuildwheel@v2.22.0
env:
CIBW_ARCHS_WINDOWS: "auto"
CIBW_ARCHS_MACOS: "universal2"
CIBW_ARCHS_LINUX: "auto"
CIBW_ARCHS_WINDOWS: "auto64"
CIBW_ARCHS_MACOS: "auto64"
CIBW_ARCHS_LINUX: "auto64"
- uses: actions/upload-artifact@v4
with:

View file

@ -21,7 +21,7 @@ jobs:
- name: Maximize build space
uses: easimon/maximize-build-space@master
with:
root-reserve-mb: 20000
root-reserve-mb: 25000
swap-size-mb: 1024
remove-dotnet: 'true'
remove-android: 'true'

12
.gitignore vendored
View file

@ -13,13 +13,13 @@ coverage.xml
venv/
# whisper_cpp
libwhisper.*
libwhisper-coreml.*
whisper_cpp
whisper_cpp.exe
whisper.dll
buzz/whisper_cpp.py
buzz/whisper_cpp_coreml.py
*.exe
*.dll
*.dylib
*.so
buzz/whisper_cpp/*
buzz/whisper_cpp_vulkan/*
# Internationalization - compiled binaries
*.mo

View file

@ -1,4 +1,5 @@
# -*- mode: python ; coding: utf-8 -*-
import os
import os.path
import platform
import shutil
@ -43,26 +44,50 @@ if DEBUG:
else:
options = []
binaries = [
(
"buzz/whisper.dll" if platform.system() == "Windows" else "buzz/libwhisper.*",
".",
),
(shutil.which("ffmpeg"), "."),
(shutil.which("ffprobe"), "."),
]
def find_dependency(name: str) -> str:
paths = os.environ["PATH"].split(os.pathsep)
candidates = []
for path in paths:
exe_path = os.path.join(path, name)
if os.path.isfile(exe_path):
candidates.append(exe_path)
# Include libwhisper-coreml.dylib on Apple Silicon
if platform.system() == "Darwin" and platform.machine() == "arm64":
binaries.append(("buzz/libwhisper-coreml.dylib", "."))
# Check for chocolatery shims
shim_path = os.path.normpath(os.path.join(path, "..", "lib", "ffmpeg", "tools", "ffmpeg", "bin", name))
if os.path.isfile(shim_path):
candidates.append(shim_path)
if not candidates:
return None
# Pick the largest file
return max(candidates, key=lambda f: os.path.getsize(f))
if platform.system() == "Windows":
binaries = [
(find_dependency("ffmpeg.exe"), "."),
(find_dependency("ffprobe.exe"), "."),
]
else:
binaries = [
(shutil.which("ffmpeg"), "."),
(shutil.which("ffprobe"), "."),
]
if platform.system() == "Linux":
binaries.append(("buzz/whisper_cpp/*.so", "buzz/whisper_cpp"))
binaries.append(("buzz/whisper_cpp_vulkan/*.so", "buzz/whisper_cpp_vulkan"))
if platform.system() == "Darwin":
binaries.append(("buzz/whisper_cpp/*.dylib", "buzz/whisper_cpp"))
# Include dll_backup folder and its contents on Windows
if platform.system() == "Windows":
datas += [("dll_backup", "dll_backup")]
datas += collect_data_files("msvc-runtime")
binaries.append(("dll_backup/SDL2.dll", "dll_backup"))
binaries.append(("dll_backup/whisper.dll", "dll_backup"))
binaries.append(("buzz/whisper_cpp/*.dll", "buzz/whisper_cpp"))
binaries.append(("buzz/*.exe", "."))
a = Analysis(
["main.py"],

149
Makefile
View file

@ -12,35 +12,23 @@ bundle_mac: dist/Buzz.app codesign_all_mac zip_mac notarize_zip staple_app_mac d
bundle_mac_unsigned: dist/Buzz.app zip_mac dmg_mac_unsigned
UNAME_S := $(shell uname -s)
UNAME_M := $(shell uname -m)
LIBWHISPER :=
ifeq ($(OS), Windows_NT)
LIBWHISPER=whisper.dll
else
ifeq ($(UNAME_S), Darwin)
LIBWHISPER=libwhisper.dylib
else
LIBWHISPER=libwhisper.so
endif
endif
clean:
ifeq ($(OS), Windows_NT)
-del /f buzz\$(LIBWHISPER) 2> nul
-del /f buzz\whisper_cpp.py 2> nul
-rmdir /s /q whisper.cpp\build 2> nul
-rmdir /s /q dist 2> nul
-rm -f buzz/$(LIBWHISPER)
-rm -f buzz/whisper_cpp.py
-rm -rf whisper.cpp/build || true
-rm -rf dist/* || true
-rmdir /s /q buzz\whisper_cpp
-rmdir /s /q buzz\whisper-server.exe
-rmdir /s /q whisper.cpp\build
-rmdir /s /q dist
-Remove-Item -Recurse -Force buzz\whisper_cpp
-Remove-Item -Recurse -Force buzz\whisper-server.exe
-Remove-Item -Recurse -Force whisper.cpp\build
-Remove-Item -Recurse -Force dist\*
-rm -rf buzz/whisper_cpp
-rm -fr buzz/whisper-server.exe
-rm -rf whisper.cpp/build
-rm -rf dist/*
else
rm -f buzz/$(LIBWHISPER)
rm -f buzz/whisper_cpp.py
rm -f buzz/libwhisper-coreml.dylib || true
rm -f buzz/whisper_cpp_coreml.py || true
rm -rf buzz/whisper_cpp || true
rm -fr buzz/whisper_cpp_vulkan || true
rm -rf whisper.cpp/build || true
rm -rf dist/* || true
endif
@ -60,53 +48,74 @@ version:
poetry version ${version}
echo "VERSION = \"${version}\"" > buzz/__version__.py
CMAKE_FLAGS=
ifeq ($(UNAME_S),Darwin)
AVX1_M := $(shell sysctl machdep.cpu.features)
ifeq (,$(findstring AVX1.0,$(AVX1_M)))
CMAKE_FLAGS += -DWHISPER_NO_AVX=ON
endif
ifeq (,$(findstring FMA,$(AVX1_M)))
CMAKE_FLAGS += -DWHISPER_NO_FMA=ON
endif
AVX2_M := $(shell sysctl machdep.cpu.leaf7_features)
ifeq (,$(findstring AVX2,$(AVX2_M)))
CMAKE_FLAGS += -DWHISPER_NO_AVX2=ON
endif
CMAKE_FLAGS += -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64"
else
ifeq ($(OS), Windows_NT)
CMAKE_FLAGS += -DBUILD_SHARED_LIBS=ON -DCMAKE_BUILD_TYPE=Release
endif
endif
buzz/$(LIBWHISPER):
buzz/whisper_cpp:
ifeq ($(OS), Windows_NT)
cp dll_backup/whisper.dll buzz || copy dll_backup\whisper.dll buzz\whisper.dll
cp dll_backup/SDL2.dll buzz || copy dll_backup\SDL2.dll buzz\SDL2.dll
else
cmake -S whisper.cpp -B whisper.cpp/build/ $(CMAKE_FLAGS)
cmake --build whisper.cpp/build --verbose
cp whisper.cpp/build/bin/Debug/$(LIBWHISPER) buzz || true
cp whisper.cpp/build/$(LIBWHISPER) buzz || true
endif
# Build CoreML support on ARM Macs
ifeq ($(shell uname -m), arm64)
ifeq ($(shell uname -s), Darwin)
rm -rf whisper.cpp/build || true
cmake -S whisper.cpp -B whisper.cpp/build/ $(CMAKE_FLAGS) -DWHISPER_COREML=1
cmake --build whisper.cpp/build --verbose
cp whisper.cpp/build/bin/Debug/$(LIBWHISPER) buzz/libwhisper-coreml.dylib || true
cp whisper.cpp/build/$(LIBWHISPER) buzz/libwhisper-coreml.dylib || true
endif
# Build Whisper for CPU
# The _DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR is needed to prevent mutex lock issues on Windows
# https://github.com/actions/runner-images/issues/10004#issuecomment-2156109231
# -DCMAKE_[C|CXX]_COMPILER_WORKS=TRUE is used to prevent issue in building test program that fails on CI
cmake -S whisper.cpp -B whisper.cpp/build/ -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DCMAKE_INSTALL_RPATH='$$ORIGIN' -DCMAKE_BUILD_WITH_INSTALL_RPATH=ON -DCMAKE_C_FLAGS="-D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR" -DCMAKE_CXX_FLAGS="-D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR" -DCMAKE_C_COMPILER_WORKS=TRUE -DCMAKE_CXX_COMPILER_WORKS=TRUE
cmake --build whisper.cpp/build -j --config Release --verbose
-mkdir buzz/whisper_cpp
cp dll_backup/SDL2.dll buzz/whisper_cpp
cp whisper.cpp/build/bin/Release/whisper.dll buzz/whisper_cpp
cp whisper.cpp/build/bin/Release/ggml.dll buzz/whisper_cpp
cp whisper.cpp/build/bin/Release/ggml-base.dll buzz/whisper_cpp
cp whisper.cpp/build/bin/Release/ggml-cpu.dll buzz/whisper_cpp
# Build Whisper with Vulkan support. On Windows whisper-server.exe wil lbe used as dll approach is unreliable,
# it often does not see the GPU
cmake -S whisper.cpp -B whisper.cpp/build/ -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DCMAKE_INSTALL_RPATH='$$ORIGIN' -DCMAKE_BUILD_WITH_INSTALL_RPATH=ON -DCMAKE_C_FLAGS="-D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR" -DCMAKE_CXX_FLAGS="-D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR" -DCMAKE_C_COMPILER_WORKS=TRUE -DCMAKE_CXX_COMPILER_WORKS=TRUE -DGGML_VULKAN=1
cmake --build whisper.cpp/build -j --config Release --verbose
cp whisper.cpp/build/bin/Release/whisper-server.exe buzz/
endif
buzz/whisper_cpp.py: buzz/$(LIBWHISPER) translation_mo
cd buzz && ctypesgen ../whisper.cpp/whisper.h -lwhisper -o whisper_cpp.py
ifeq ($(shell uname -m), arm64)
ifeq ($(shell uname -s), Darwin)
cd buzz && ctypesgen ../whisper.cpp/whisper.h -lwhisper-coreml -o whisper_cpp_coreml.py
ifeq ($(shell uname -s), Linux)
# Build Whisper for CPU
-rm -rf whisper.cpp/build || true
-mkdir -p buzz/whisper_cpp
cmake -S whisper.cpp -B whisper.cpp/build/ -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DCMAKE_INSTALL_RPATH='$$ORIGIN' -DCMAKE_BUILD_WITH_INSTALL_RPATH=ON
cmake --build whisper.cpp/build -j --config Release --verbose
cp whisper.cpp/build/src/libwhisper.so buzz/whisper_cpp/libwhisper.so || true
cp whisper.cpp/build/ggml/src/libggml.so buzz/whisper_cpp || true
cp whisper.cpp/build/ggml/src/libggml-base.so buzz/whisper_cpp || true
cp whisper.cpp/build/ggml/src/libggml-cpu.so buzz/whisper_cpp || true
# Build Whisper for Vulkan
rm -rf whisper.cpp/build || true
-mkdir -p buzz/whisper_cpp_vulkan
cmake -S whisper.cpp -B whisper.cpp/build/ -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DCMAKE_INSTALL_RPATH='$$ORIGIN' -DCMAKE_BUILD_WITH_INSTALL_RPATH=ON -DGGML_VULKAN=1
cmake --build whisper.cpp/build -j --config Release --verbose
cp whisper.cpp/build/src/libwhisper.so buzz/whisper_cpp_vulkan/whisper-vulkan.so || true
cp whisper.cpp/build/ggml/src/libggml.so buzz/whisper_cpp_vulkan || true
cp whisper.cpp/build/ggml/src/libggml-base.so buzz/whisper_cpp_vulkan || true
cp whisper.cpp/build/ggml/src/libggml-cpu.so buzz/whisper_cpp_vulkan || true
cp whisper.cpp/build/ggml/src/ggml-vulkan/libggml-vulkan.so buzz/whisper_cpp_vulkan || true
endif
# Build on Macs
ifeq ($(shell uname -s), Darwin)
-rm -rf whisper.cpp/build || true
-mkdir -p buzz/whisper_cpp
ifeq ($(shell uname -m), arm64)
cmake -S whisper.cpp -B whisper.cpp/build/ -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DWHISPER_COREML=1
else
cmake -S whisper.cpp -B whisper.cpp/build/ -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON
endif
cmake --build whisper.cpp/build -j --config Release --verbose
cp whisper.cpp/build/src/libwhisper.dylib buzz/whisper_cpp/ || true
cp whisper.cpp/build/ggml/src/libggml* buzz/whisper_cpp/ || true
endif
buzz/whisper_cpp.py: buzz/whisper_cpp translation_mo
cd buzz && ctypesgen ../whisper.cpp/include/whisper.h -I../whisper.cpp/ggml/include -lwhisper -o ./whisper_cpp/whisper_cpp.py
ifeq ($(shell uname -s), Linux)
cd buzz && ctypesgen ../whisper.cpp/include/whisper.h -I../whisper.cpp/ggml/include -lwhisper-vulkan -o ./whisper_cpp_vulkan/whisper_cpp_vulkan.py
endif
# Prints all the Mac developer identities used for code signing
@ -238,7 +247,7 @@ ifeq ($(OS), Windows_NT)
done
else
for dir in buzz/locale/*/ ; do \
python msgfmt.py -o $$dir/LC_MESSAGES/buzz.mo $$dir/LC_MESSAGES/buzz.po; \
python3 msgfmt.py -o $$dir/LC_MESSAGES/buzz.mo $$dir/LC_MESSAGES/buzz.po; \
done
endif

View file

@ -26,7 +26,10 @@ os.environ["PATH"] += os.pathsep + APP_BASE_DIR
# Add the app directory to the DLL list: https://stackoverflow.com/a/64303856
if platform.system() == "Windows":
os.add_dll_directory(APP_BASE_DIR)
os.add_dll_directory(os.path.join(APP_BASE_DIR, "dll_backup"))
dll_backup_dir = os.path.join(APP_BASE_DIR, "dll_backup")
if os.path.isdir(dll_backup_dir):
os.add_dll_directory(dll_backup_dir)
def main():

View file

@ -37,11 +37,16 @@ class FileTranscriberQueueWorker(QObject):
super().__init__(parent)
self.tasks_queue = queue.Queue()
self.canceled_tasks: Set[UUID] = set()
self.current_transcriber = None
@pyqtSlot()
def run(self):
logging.debug("Waiting for next transcription task")
# Clean up of previous run.
if self.current_transcriber is not None:
self.current_transcriber.stop()
# Get next non-canceled task from queue
while True:
self.current_task: Optional[FileTranscriptionTask] = self.tasks_queue.get()
@ -131,9 +136,14 @@ class FileTranscriberQueueWorker(QObject):
def cancel_task(self, task_id: UUID):
self.canceled_tasks.add(task_id)
if self.current_task.uid == task_id:
if self.current_task is not None and self.current_task.uid == task_id:
if self.current_transcriber is not None:
self.current_transcriber.stop()
if self.current_transcriber_thread is not None:
if not self.current_transcriber_thread.wait(3000):
logging.warning("Transcriber thread did not terminate gracefully")
self.current_transcriber_thread.terminate()
def on_task_error(self, error: str):
if (

View file

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: buzz\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-06 20:16+0300\n"
"POT-Creation-Date: 2025-07-18 09:13+0300\n"
"PO-Revision-Date: 2025-05-17 18:25+0200\n"
"Last-Translator: Éric Duarte <contacto@ericdq.com>\n"
"Language-Team: Catalan <jmas@softcatala.org>\n"
@ -295,8 +295,8 @@ msgid "Download failed"
msgstr "Descàrrega fallida"
#: buzz/widgets/preferences_dialog/models_preferences_widget.py:273
#: buzz/widgets/main_window.py:295 buzz/model_loader.py:515
#: buzz/model_loader.py:529
#: buzz/widgets/main_window.py:295 buzz/model_loader.py:505
#: buzz/model_loader.py:519
msgid "Error"
msgstr "Error"
@ -486,11 +486,11 @@ msgstr "Esperant la traducció de la IA..."
msgid "Microphone:"
msgstr "Micròfon:"
#: buzz/widgets/recording_transcriber_widget.py:577
#: buzz/widgets/recording_transcriber_widget.py:579
msgid "An error occurred while starting a new recording:"
msgstr "S'ha produït un error en iniciar un enregistrament nou:"
#: buzz/widgets/recording_transcriber_widget.py:581
#: buzz/widgets/recording_transcriber_widget.py:583
msgid ""
"Please check your audio devices or check the application logs for more "
"information."
@ -540,7 +540,6 @@ msgid "Export"
msgstr "Exporta"
#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py:174
#: buzz/transcriber/transcriber.py:24
msgid "Translate"
msgstr "Traduir"
@ -664,6 +663,24 @@ msgstr "Selecciona un fitxer d'àudio"
msgid "Unable to save OpenAI API key to keyring"
msgstr "No s'ha pogut desar la clau OpenAI API a l'anell de claus"
#: buzz/transcriber/local_whisper_cpp_server_transcriber.py:51
#: buzz/transcriber/recording_transcriber.py:372
msgid "Whisper server failed to start. Check logs for details."
msgstr ""
#: buzz/transcriber/local_whisper_cpp_server_transcriber.py:54
#: buzz/transcriber/recording_transcriber.py:375
msgid ""
"Whisper server failed to start due to insufficient memory. Please try again "
"with a smaller model. To force CPU mode use BUZZ_FORCE_CPU=TRUE environment "
"variable."
msgstr ""
#: buzz/transcriber/transcriber.py:24
#, fuzzy
msgid "Translate to English"
msgstr "Configuració de la traducció"
#: buzz/transcriber/transcriber.py:25
msgid "Transcribe"
msgstr "Transcriure"
@ -1024,7 +1041,11 @@ msgstr "Sundanès"
msgid "Cantonese"
msgstr "Cantonès"
#: buzz/model_loader.py:548
#: buzz/transcriber/recording_transcriber.py:338
msgid "Starting Whisper.cpp..."
msgstr ""
#: buzz/model_loader.py:538
msgid "A connection error occurred"
msgstr "S'ha produït un error de connexió"

View file

@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-06 20:16+0300\n"
"POT-Creation-Date: 2025-07-18 09:13+0300\n"
"PO-Revision-Date: \n"
"Last-Translator: Ole Guldberg2 <xalt7x.service@gmail.com>\n"
"Language-Team: \n"
@ -295,8 +295,8 @@ msgid "Download failed"
msgstr "Download mislykkedes"
#: buzz/widgets/preferences_dialog/models_preferences_widget.py:273
#: buzz/widgets/main_window.py:295 buzz/model_loader.py:515
#: buzz/model_loader.py:529
#: buzz/widgets/main_window.py:295 buzz/model_loader.py:505
#: buzz/model_loader.py:519
msgid "Error"
msgstr "Fejl"
@ -487,11 +487,11 @@ msgstr "Venter på AI oversættelse..."
msgid "Microphone:"
msgstr "Mikrofon:"
#: buzz/widgets/recording_transcriber_widget.py:577
#: buzz/widgets/recording_transcriber_widget.py:579
msgid "An error occurred while starting a new recording:"
msgstr "Der skete en fejl ved opstart af en ny optagelse:"
#: buzz/widgets/recording_transcriber_widget.py:581
#: buzz/widgets/recording_transcriber_widget.py:583
msgid ""
"Please check your audio devices or check the application logs for more "
"information."
@ -541,7 +541,6 @@ msgid "Export"
msgstr "Eksporter"
#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py:174
#: buzz/transcriber/transcriber.py:24
msgid "Translate"
msgstr "Oversæt"
@ -665,6 +664,24 @@ msgstr "Vælg audio-fil"
msgid "Unable to save OpenAI API key to keyring"
msgstr "Kan ikke gemme OpenAI API-nøgle i nøgleringen"
#: buzz/transcriber/local_whisper_cpp_server_transcriber.py:51
#: buzz/transcriber/recording_transcriber.py:372
msgid "Whisper server failed to start. Check logs for details."
msgstr ""
#: buzz/transcriber/local_whisper_cpp_server_transcriber.py:54
#: buzz/transcriber/recording_transcriber.py:375
msgid ""
"Whisper server failed to start due to insufficient memory. Please try again "
"with a smaller model. To force CPU mode use BUZZ_FORCE_CPU=TRUE environment "
"variable."
msgstr ""
#: buzz/transcriber/transcriber.py:24
#, fuzzy
msgid "Translate to English"
msgstr "Oversættelsesindstillinger"
#: buzz/transcriber/transcriber.py:25
msgid "Transcribe"
msgstr "Transkriber"
@ -1025,7 +1042,11 @@ msgstr ""
msgid "Cantonese"
msgstr ""
#: buzz/model_loader.py:548
#: buzz/transcriber/recording_transcriber.py:338
msgid "Starting Whisper.cpp..."
msgstr ""
#: buzz/model_loader.py:538
msgid "A connection error occurred"
msgstr "Der er opstået en forbindelsesfejl"

View file

@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-06 20:16+0300\n"
"POT-Creation-Date: 2025-07-18 09:13+0300\n"
"PO-Revision-Date: 2025-03-05 14:41+0100\n"
"Last-Translator: \n"
"Language-Team: \n"
@ -295,8 +295,8 @@ msgid "Download failed"
msgstr "Der Download ist fehlgeschlagen"
#: buzz/widgets/preferences_dialog/models_preferences_widget.py:273
#: buzz/widgets/main_window.py:295 buzz/model_loader.py:515
#: buzz/model_loader.py:529
#: buzz/widgets/main_window.py:295 buzz/model_loader.py:505
#: buzz/model_loader.py:519
msgid "Error"
msgstr "Fehler"
@ -487,11 +487,11 @@ msgstr "Warten auf KI-Übersetzung..."
msgid "Microphone:"
msgstr "Mikrofon:"
#: buzz/widgets/recording_transcriber_widget.py:577
#: buzz/widgets/recording_transcriber_widget.py:579
msgid "An error occurred while starting a new recording:"
msgstr "Beim Starten einer neuen Aufnahme ist ein Fehler aufgetreten:"
#: buzz/widgets/recording_transcriber_widget.py:581
#: buzz/widgets/recording_transcriber_widget.py:583
msgid ""
"Please check your audio devices or check the application logs for more "
"information."
@ -541,7 +541,6 @@ msgid "Export"
msgstr "Export"
#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py:174
#: buzz/transcriber/transcriber.py:24
msgid "Translate"
msgstr "Übersetzen"
@ -666,6 +665,24 @@ msgid "Unable to save OpenAI API key to keyring"
msgstr ""
"Der OpenAI-API-Schlüssel kann nicht im Schlüsselbund gespeichert werden"
#: buzz/transcriber/local_whisper_cpp_server_transcriber.py:51
#: buzz/transcriber/recording_transcriber.py:372
msgid "Whisper server failed to start. Check logs for details."
msgstr ""
#: buzz/transcriber/local_whisper_cpp_server_transcriber.py:54
#: buzz/transcriber/recording_transcriber.py:375
msgid ""
"Whisper server failed to start due to insufficient memory. Please try again "
"with a smaller model. To force CPU mode use BUZZ_FORCE_CPU=TRUE environment "
"variable."
msgstr ""
#: buzz/transcriber/transcriber.py:24
#, fuzzy
msgid "Translate to English"
msgstr "Übersetzungseinstellungen"
#: buzz/transcriber/transcriber.py:25
msgid "Transcribe"
msgstr "Transkribieren"
@ -1026,7 +1043,11 @@ msgstr "Sundanesisch"
msgid "Cantonese"
msgstr "Kantonesisch"
#: buzz/model_loader.py:548
#: buzz/transcriber/recording_transcriber.py:338
msgid "Starting Whisper.cpp..."
msgstr ""
#: buzz/model_loader.py:538
msgid "A connection error occurred"
msgstr "Ein Verbindungsfehler ist aufgetreten"

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-06 20:16+0300\n"
"POT-Creation-Date: 2025-07-18 09:13+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -287,8 +287,8 @@ msgid "Download failed"
msgstr ""
#: buzz/widgets/preferences_dialog/models_preferences_widget.py:273
#: buzz/widgets/main_window.py:295 buzz/model_loader.py:515
#: buzz/model_loader.py:529
#: buzz/widgets/main_window.py:295 buzz/model_loader.py:505
#: buzz/model_loader.py:519
msgid "Error"
msgstr ""
@ -477,11 +477,11 @@ msgstr ""
msgid "Microphone:"
msgstr ""
#: buzz/widgets/recording_transcriber_widget.py:577
#: buzz/widgets/recording_transcriber_widget.py:579
msgid "An error occurred while starting a new recording:"
msgstr ""
#: buzz/widgets/recording_transcriber_widget.py:581
#: buzz/widgets/recording_transcriber_widget.py:583
msgid ""
"Please check your audio devices or check the application logs for more "
"information."
@ -529,7 +529,6 @@ msgid "Export"
msgstr ""
#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py:174
#: buzz/transcriber/transcriber.py:24
msgid "Translate"
msgstr ""
@ -647,6 +646,23 @@ msgstr ""
msgid "Unable to save OpenAI API key to keyring"
msgstr ""
#: buzz/transcriber/local_whisper_cpp_server_transcriber.py:51
#: buzz/transcriber/recording_transcriber.py:372
msgid "Whisper server failed to start. Check logs for details."
msgstr ""
#: buzz/transcriber/local_whisper_cpp_server_transcriber.py:54
#: buzz/transcriber/recording_transcriber.py:375
msgid ""
"Whisper server failed to start due to insufficient memory. Please try again "
"with a smaller model. To force CPU mode use BUZZ_FORCE_CPU=TRUE environment "
"variable."
msgstr ""
#: buzz/transcriber/transcriber.py:24
msgid "Translate to English"
msgstr ""
#: buzz/transcriber/transcriber.py:25
msgid "Transcribe"
msgstr ""
@ -1007,7 +1023,11 @@ msgstr ""
msgid "Cantonese"
msgstr ""
#: buzz/model_loader.py:548
#: buzz/transcriber/recording_transcriber.py:338
msgid "Starting Whisper.cpp..."
msgstr ""
#: buzz/model_loader.py:538
msgid "A connection error occurred"
msgstr ""

View file

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-06 20:16+0300\n"
"POT-Creation-Date: 2025-07-18 09:13+0300\n"
"PO-Revision-Date: 2025-05-17 18:27+0200\n"
"Last-Translator: Éric Duarte <contacto@ericdq.com>\n"
"Language-Team: \n"
@ -301,8 +301,8 @@ msgid "Download failed"
msgstr "Descarga fallida"
#: buzz/widgets/preferences_dialog/models_preferences_widget.py:273
#: buzz/widgets/main_window.py:295 buzz/model_loader.py:515
#: buzz/model_loader.py:529
#: buzz/widgets/main_window.py:295 buzz/model_loader.py:505
#: buzz/model_loader.py:519
msgid "Error"
msgstr "Error"
@ -517,12 +517,12 @@ msgid "Microphone:"
msgstr "Micrófono:"
# automatic translation
#: buzz/widgets/recording_transcriber_widget.py:577
#: buzz/widgets/recording_transcriber_widget.py:579
msgid "An error occurred while starting a new recording:"
msgstr "Se produjo un error al iniciar una grabación nueva:"
# automatic translation
#: buzz/widgets/recording_transcriber_widget.py:581
#: buzz/widgets/recording_transcriber_widget.py:583
msgid ""
"Please check your audio devices or check the application logs for more "
"information."
@ -575,7 +575,6 @@ msgid "Export"
msgstr "Exportar"
#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py:174
#: buzz/transcriber/transcriber.py:24
msgid "Translate"
msgstr "Traducir"
@ -707,6 +706,24 @@ msgstr "Seleccionar archivo de audio"
msgid "Unable to save OpenAI API key to keyring"
msgstr "No se puede guardar la clave de la API de OpenAI en el llavero"
#: buzz/transcriber/local_whisper_cpp_server_transcriber.py:51
#: buzz/transcriber/recording_transcriber.py:372
msgid "Whisper server failed to start. Check logs for details."
msgstr ""
#: buzz/transcriber/local_whisper_cpp_server_transcriber.py:54
#: buzz/transcriber/recording_transcriber.py:375
msgid ""
"Whisper server failed to start due to insufficient memory. Please try again "
"with a smaller model. To force CPU mode use BUZZ_FORCE_CPU=TRUE environment "
"variable."
msgstr ""
#: buzz/transcriber/transcriber.py:24
#, fuzzy
msgid "Translate to English"
msgstr "Ajustes de traducción"
# automatic translation
#: buzz/transcriber/transcriber.py:25
msgid "Transcribe"
@ -1068,7 +1085,11 @@ msgstr "Sundanés"
msgid "Cantonese"
msgstr "Cantonés"
#: buzz/model_loader.py:548
#: buzz/transcriber/recording_transcriber.py:338
msgid "Starting Whisper.cpp..."
msgstr ""
#: buzz/model_loader.py:538
msgid "A connection error occurred"
msgstr "Se ha producido un error de conexión"

View file

@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: buzz\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-06 20:16+0300\n"
"POT-Creation-Date: 2025-07-18 09:13+0300\n"
"PO-Revision-Date: 2025-05-30 15:22+0100\n"
"Language-Team: (Italiano) Albano Battistella <albanobattistella@gmail.com>\n"
"Language: it_IT\n"
@ -294,8 +294,8 @@ msgid "Download failed"
msgstr "Download non riuscito"
#: buzz/widgets/preferences_dialog/models_preferences_widget.py:273
#: buzz/widgets/main_window.py:295 buzz/model_loader.py:515
#: buzz/model_loader.py:529
#: buzz/widgets/main_window.py:295 buzz/model_loader.py:505
#: buzz/model_loader.py:519
msgid "Error"
msgstr "Errore"
@ -486,11 +486,11 @@ msgstr "In attesa della traduzione AI..."
msgid "Microphone:"
msgstr "Microfono:"
#: buzz/widgets/recording_transcriber_widget.py:577
#: buzz/widgets/recording_transcriber_widget.py:579
msgid "An error occurred while starting a new recording:"
msgstr "Si è verificato un errore durante l'avvio della nuova registrazione:"
#: buzz/widgets/recording_transcriber_widget.py:581
#: buzz/widgets/recording_transcriber_widget.py:583
msgid ""
"Please check your audio devices or check the application logs for more "
"information."
@ -540,7 +540,6 @@ msgid "Export"
msgstr "Esporta"
#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py:174
#: buzz/transcriber/transcriber.py:24
msgid "Translate"
msgstr "Tradurre"
@ -664,6 +663,24 @@ msgstr "Seleziona file audio"
msgid "Unable to save OpenAI API key to keyring"
msgstr "Impossibile salvare la chiave API OpenAI nel portachiavi"
#: buzz/transcriber/local_whisper_cpp_server_transcriber.py:51
#: buzz/transcriber/recording_transcriber.py:372
msgid "Whisper server failed to start. Check logs for details."
msgstr ""
#: buzz/transcriber/local_whisper_cpp_server_transcriber.py:54
#: buzz/transcriber/recording_transcriber.py:375
msgid ""
"Whisper server failed to start due to insufficient memory. Please try again "
"with a smaller model. To force CPU mode use BUZZ_FORCE_CPU=TRUE environment "
"variable."
msgstr ""
#: buzz/transcriber/transcriber.py:24
#, fuzzy
msgid "Translate to English"
msgstr "Impostazioni di traduzione"
#: buzz/transcriber/transcriber.py:25
msgid "Transcribe"
msgstr "Trascrivere"
@ -1024,7 +1041,11 @@ msgstr "Sundanese"
msgid "Cantonese"
msgstr "Cantonese"
#: buzz/model_loader.py:548
#: buzz/transcriber/recording_transcriber.py:338
msgid "Starting Whisper.cpp..."
msgstr ""
#: buzz/model_loader.py:538
msgid "A connection error occurred"
msgstr "Si è verificato un errore di connessione"

View file

@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-06 20:16+0300\n"
"POT-Creation-Date: 2025-07-18 09:13+0300\n"
"PO-Revision-Date: \n"
"Last-Translator: nunawa <71294849+nunawa@users.noreply.github.com>\n"
"Language-Team: \n"
@ -291,8 +291,8 @@ msgid "Download failed"
msgstr "ダウンロード失敗"
#: buzz/widgets/preferences_dialog/models_preferences_widget.py:273
#: buzz/widgets/main_window.py:295 buzz/model_loader.py:515
#: buzz/model_loader.py:529
#: buzz/widgets/main_window.py:295 buzz/model_loader.py:505
#: buzz/model_loader.py:519
msgid "Error"
msgstr "エラー"
@ -483,11 +483,11 @@ msgstr "AI翻訳を待っています..."
msgid "Microphone:"
msgstr "マイク:"
#: buzz/widgets/recording_transcriber_widget.py:577
#: buzz/widgets/recording_transcriber_widget.py:579
msgid "An error occurred while starting a new recording:"
msgstr "新規録音開始時にエラーが発生しました:"
#: buzz/widgets/recording_transcriber_widget.py:581
#: buzz/widgets/recording_transcriber_widget.py:583
msgid ""
"Please check your audio devices or check the application logs for more "
"information."
@ -537,7 +537,6 @@ msgid "Export"
msgstr "出力"
#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py:174
#: buzz/transcriber/transcriber.py:24
msgid "Translate"
msgstr "翻訳"
@ -660,6 +659,24 @@ msgstr "音声ファイルを選択"
msgid "Unable to save OpenAI API key to keyring"
msgstr "OpenAI API キーをkeyringに保存できません"
#: buzz/transcriber/local_whisper_cpp_server_transcriber.py:51
#: buzz/transcriber/recording_transcriber.py:372
msgid "Whisper server failed to start. Check logs for details."
msgstr ""
#: buzz/transcriber/local_whisper_cpp_server_transcriber.py:54
#: buzz/transcriber/recording_transcriber.py:375
msgid ""
"Whisper server failed to start due to insufficient memory. Please try again "
"with a smaller model. To force CPU mode use BUZZ_FORCE_CPU=TRUE environment "
"variable."
msgstr ""
#: buzz/transcriber/transcriber.py:24
#, fuzzy
msgid "Translate to English"
msgstr "翻訳設定"
#: buzz/transcriber/transcriber.py:25
msgid "Transcribe"
msgstr "文字起こし"
@ -1020,7 +1037,11 @@ msgstr ""
msgid "Cantonese"
msgstr ""
#: buzz/model_loader.py:548
#: buzz/transcriber/recording_transcriber.py:338
msgid "Starting Whisper.cpp..."
msgstr ""
#: buzz/model_loader.py:538
msgid "A connection error occurred"
msgstr "接続エラーが発生しました"

View file

@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-06 20:16+0300\n"
"PO-Revision-Date: 2025-07-06 20:11+0300\n"
"POT-Creation-Date: 2025-07-18 09:13+0300\n"
"PO-Revision-Date: 2025-07-18 09:14+0300\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: lv_LV\n"
@ -297,8 +297,8 @@ msgid "Download failed"
msgstr "Lejupielāde neizdevās"
#: buzz/widgets/preferences_dialog/models_preferences_widget.py:273
#: buzz/widgets/main_window.py:295 buzz/model_loader.py:515
#: buzz/model_loader.py:529
#: buzz/widgets/main_window.py:295 buzz/model_loader.py:505
#: buzz/model_loader.py:519
msgid "Error"
msgstr "Kļūda"
@ -383,6 +383,8 @@ msgid ""
"Enter instructions for AI on how to translate, for example 'Please translate "
"each text sent to you from English to Spanish.'"
msgstr ""
"Ievadiet instrukcijas mākslīgajam intelektam, piemēram, 'Lūdzu, iztulko "
"katru tev atsūtīto tekstu no angļu valodas latviski'"
#: buzz/widgets/transcriber/advanced_settings_dialog.py:92
msgid "Instructions for AI:"
@ -489,11 +491,11 @@ msgstr "Gaida MI tulkojumu..."
msgid "Microphone:"
msgstr "Mikrofons:"
#: buzz/widgets/recording_transcriber_widget.py:577
#: buzz/widgets/recording_transcriber_widget.py:579
msgid "An error occurred while starting a new recording:"
msgstr "Sākot jaunu ierakstu notikusi kļūda:"
#: buzz/widgets/recording_transcriber_widget.py:581
#: buzz/widgets/recording_transcriber_widget.py:583
msgid ""
"Please check your audio devices or check the application logs for more "
"information."
@ -543,7 +545,6 @@ msgid "Export"
msgstr "Eksportēt"
#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py:174
#: buzz/transcriber/transcriber.py:24
msgid "Translate"
msgstr "Tulkot"
@ -665,6 +666,28 @@ msgstr "Izvēlieties audio failu"
msgid "Unable to save OpenAI API key to keyring"
msgstr "Neizdevās saglabāt OpenAI API atslēgu atslēgu saišķī"
#: buzz/transcriber/local_whisper_cpp_server_transcriber.py:51
#: buzz/transcriber/recording_transcriber.py:372
msgid "Whisper server failed to start. Check logs for details."
msgstr ""
"Whisper serverim neizdevās ieslēgties. Lūdzu pārbaudiet lietotnes žurnāla "
"ierakstus."
#: buzz/transcriber/local_whisper_cpp_server_transcriber.py:54
#: buzz/transcriber/recording_transcriber.py:375
msgid ""
"Whisper server failed to start due to insufficient memory. Please try again "
"with a smaller model. To force CPU mode use BUZZ_FORCE_CPU=TRUE environment "
"variable."
msgstr ""
"Whisper serverim neizdevās ieslēgties, jo nepietika atmiņas. Lūdzu mēģiniet "
"vēlreiz ar mazāku modeli. Lai izmantotu tikai CPU iestatiet "
"BUZZ_FORCE_CPU=TRUE vides mainīgo."
#: buzz/transcriber/transcriber.py:24
msgid "Translate to English"
msgstr "Tulkot angliski"
#: buzz/transcriber/transcriber.py:25
msgid "Transcribe"
msgstr "Atpazīt"
@ -1025,7 +1048,11 @@ msgstr "Sundāņu"
msgid "Cantonese"
msgstr "Kantonas"
#: buzz/model_loader.py:548
#: buzz/transcriber/recording_transcriber.py:338
msgid "Starting Whisper.cpp..."
msgstr "Palaiž Whisper.cpp..."
#: buzz/model_loader.py:538
msgid "A connection error occurred"
msgstr "Notika savienojuma kļūda"

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-06 20:16+0300\n"
"POT-Creation-Date: 2025-07-18 09:13+0300\n"
"PO-Revision-Date: 2025-03-20 18:30+0100\n"
"Last-Translator: Heimen Stoffels <vistausss@fastmail.com>\n"
"Language-Team: none\n"
@ -297,8 +297,8 @@ msgid "Download failed"
msgstr "Het downloaden is mislukt"
#: buzz/widgets/preferences_dialog/models_preferences_widget.py:273
#: buzz/widgets/main_window.py:295 buzz/model_loader.py:515
#: buzz/model_loader.py:529
#: buzz/widgets/main_window.py:295 buzz/model_loader.py:505
#: buzz/model_loader.py:519
msgid "Error"
msgstr "Foutmelding"
@ -489,11 +489,11 @@ msgstr "Bezig met wachten op AI-vertaling…"
msgid "Microphone:"
msgstr "Microfoon:"
#: buzz/widgets/recording_transcriber_widget.py:577
#: buzz/widgets/recording_transcriber_widget.py:579
msgid "An error occurred while starting a new recording:"
msgstr "Er is een fout opgetreden tijdens het starten van de opname:"
#: buzz/widgets/recording_transcriber_widget.py:581
#: buzz/widgets/recording_transcriber_widget.py:583
msgid ""
"Please check your audio devices or check the application logs for more "
"information."
@ -541,7 +541,6 @@ msgid "Export"
msgstr "Exporteren"
#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py:174
#: buzz/transcriber/transcriber.py:24
msgid "Translate"
msgstr "Vertalen"
@ -664,6 +663,24 @@ msgstr "Kies een audiobestand"
msgid "Unable to save OpenAI API key to keyring"
msgstr "De OpenAI-api-sleutel kan niet worden bewaard in de sleutelbos"
#: buzz/transcriber/local_whisper_cpp_server_transcriber.py:51
#: buzz/transcriber/recording_transcriber.py:372
msgid "Whisper server failed to start. Check logs for details."
msgstr ""
#: buzz/transcriber/local_whisper_cpp_server_transcriber.py:54
#: buzz/transcriber/recording_transcriber.py:375
msgid ""
"Whisper server failed to start due to insufficient memory. Please try again "
"with a smaller model. To force CPU mode use BUZZ_FORCE_CPU=TRUE environment "
"variable."
msgstr ""
#: buzz/transcriber/transcriber.py:24
#, fuzzy
msgid "Translate to English"
msgstr "Vertaalinstellingen"
#: buzz/transcriber/transcriber.py:25
msgid "Transcribe"
msgstr "Transcriberen"
@ -1024,7 +1041,11 @@ msgstr "Soedanees"
msgid "Cantonese"
msgstr "Kantonees"
#: buzz/model_loader.py:548
#: buzz/transcriber/recording_transcriber.py:338
msgid "Starting Whisper.cpp..."
msgstr ""
#: buzz/model_loader.py:538
msgid "A connection error occurred"
msgstr "Er is een verbindingsfout opgetreden"

View file

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-06 20:16+0300\n"
"POT-Creation-Date: 2025-07-18 09:13+0300\n"
"PO-Revision-Date: 2024-03-17 20:50+0200\n"
"Last-Translator: \n"
"Language-Team: \n"
@ -298,8 +298,8 @@ msgid "Download failed"
msgstr "Pobrany"
#: buzz/widgets/preferences_dialog/models_preferences_widget.py:273
#: buzz/widgets/main_window.py:295 buzz/model_loader.py:515
#: buzz/model_loader.py:529
#: buzz/widgets/main_window.py:295 buzz/model_loader.py:505
#: buzz/model_loader.py:519
msgid "Error"
msgstr "Błąd"
@ -495,11 +495,11 @@ msgstr ""
msgid "Microphone:"
msgstr "Mikrofon:"
#: buzz/widgets/recording_transcriber_widget.py:577
#: buzz/widgets/recording_transcriber_widget.py:579
msgid "An error occurred while starting a new recording:"
msgstr "Wystąpił błąd podczas rozpoczęcia nowego nagrania:"
#: buzz/widgets/recording_transcriber_widget.py:581
#: buzz/widgets/recording_transcriber_widget.py:583
msgid ""
"Please check your audio devices or check the application logs for more "
"information."
@ -550,7 +550,6 @@ msgid "Export"
msgstr ""
#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py:174
#: buzz/transcriber/transcriber.py:24
msgid "Translate"
msgstr ""
@ -674,6 +673,24 @@ msgstr "Wybierz plik audio"
msgid "Unable to save OpenAI API key to keyring"
msgstr ""
#: buzz/transcriber/local_whisper_cpp_server_transcriber.py:51
#: buzz/transcriber/recording_transcriber.py:372
msgid "Whisper server failed to start. Check logs for details."
msgstr ""
#: buzz/transcriber/local_whisper_cpp_server_transcriber.py:54
#: buzz/transcriber/recording_transcriber.py:375
msgid ""
"Whisper server failed to start due to insufficient memory. Please try again "
"with a smaller model. To force CPU mode use BUZZ_FORCE_CPU=TRUE environment "
"variable."
msgstr ""
#: buzz/transcriber/transcriber.py:24
#, fuzzy
msgid "Translate to English"
msgstr "Nowa transkrypcja"
#: buzz/transcriber/transcriber.py:25
#, fuzzy
msgid "Transcribe"
@ -1035,7 +1052,11 @@ msgstr ""
msgid "Cantonese"
msgstr ""
#: buzz/model_loader.py:548
#: buzz/transcriber/recording_transcriber.py:338
msgid "Starting Whisper.cpp..."
msgstr ""
#: buzz/model_loader.py:538
msgid "A connection error occurred"
msgstr ""

View file

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Buzz\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-06 20:16+0300\n"
"POT-Creation-Date: 2025-07-18 09:13+0300\n"
"PO-Revision-Date: 2025-06-29 22:23-0300\n"
"Last-Translator: Paulo Schopf <pschopf@gmail.com>\n"
"Language-Team: none\n"
@ -295,8 +295,8 @@ msgid "Download failed"
msgstr "Falha ao baixar"
#: buzz/widgets/preferences_dialog/models_preferences_widget.py:273
#: buzz/widgets/main_window.py:295 buzz/model_loader.py:515
#: buzz/model_loader.py:529
#: buzz/widgets/main_window.py:295 buzz/model_loader.py:505
#: buzz/model_loader.py:519
msgid "Error"
msgstr "Erro"
@ -485,11 +485,11 @@ msgstr "Aguardando tradução da IA..."
msgid "Microphone:"
msgstr "Microfone:"
#: buzz/widgets/recording_transcriber_widget.py:577
#: buzz/widgets/recording_transcriber_widget.py:579
msgid "An error occurred while starting a new recording:"
msgstr "Ocorreu um erro ao iniciar uma nova gravação:"
#: buzz/widgets/recording_transcriber_widget.py:581
#: buzz/widgets/recording_transcriber_widget.py:583
msgid ""
"Please check your audio devices or check the application logs for more "
"information."
@ -539,7 +539,6 @@ msgid "Export"
msgstr "Exportar"
#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py:174
#: buzz/transcriber/transcriber.py:24
msgid "Translate"
msgstr "Traduzir"
@ -663,6 +662,24 @@ msgstr "Selecionar arquivo de áudio"
msgid "Unable to save OpenAI API key to keyring"
msgstr "Não foi possível salvar a chave da API OpenAI no cofre de chaves"
#: buzz/transcriber/local_whisper_cpp_server_transcriber.py:51
#: buzz/transcriber/recording_transcriber.py:372
msgid "Whisper server failed to start. Check logs for details."
msgstr ""
#: buzz/transcriber/local_whisper_cpp_server_transcriber.py:54
#: buzz/transcriber/recording_transcriber.py:375
msgid ""
"Whisper server failed to start due to insufficient memory. Please try again "
"with a smaller model. To force CPU mode use BUZZ_FORCE_CPU=TRUE environment "
"variable."
msgstr ""
#: buzz/transcriber/transcriber.py:24
#, fuzzy
msgid "Translate to English"
msgstr "Configurações de tradução"
#: buzz/transcriber/transcriber.py:25
msgid "Transcribe"
msgstr "Transcrever"
@ -1023,7 +1040,11 @@ msgstr "Sundanês"
msgid "Cantonese"
msgstr "Cantonês"
#: buzz/model_loader.py:548
#: buzz/transcriber/recording_transcriber.py:338
msgid "Starting Whisper.cpp..."
msgstr ""
#: buzz/model_loader.py:538
msgid "A connection error occurred"
msgstr "Ocorreu um erro de conexão"

View file

@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-06 20:16+0300\n"
"POT-Creation-Date: 2025-07-18 09:13+0300\n"
"PO-Revision-Date: \n"
"Last-Translator: Yevhen Popok <xalt7x.service@gmail.com>\n"
"Language-Team: \n"
@ -293,8 +293,8 @@ msgid "Download failed"
msgstr "Невдале завантаження"
#: buzz/widgets/preferences_dialog/models_preferences_widget.py:273
#: buzz/widgets/main_window.py:295 buzz/model_loader.py:515
#: buzz/model_loader.py:529
#: buzz/widgets/main_window.py:295 buzz/model_loader.py:505
#: buzz/model_loader.py:519
msgid "Error"
msgstr "Помилка"
@ -485,11 +485,11 @@ msgstr "Очікування перекладу від ШІ..."
msgid "Microphone:"
msgstr "Мікрофон:"
#: buzz/widgets/recording_transcriber_widget.py:577
#: buzz/widgets/recording_transcriber_widget.py:579
msgid "An error occurred while starting a new recording:"
msgstr "При старті нового запису виникла помилка:"
#: buzz/widgets/recording_transcriber_widget.py:581
#: buzz/widgets/recording_transcriber_widget.py:583
msgid ""
"Please check your audio devices or check the application logs for more "
"information."
@ -539,7 +539,6 @@ msgid "Export"
msgstr "Експорт"
#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py:174
#: buzz/transcriber/transcriber.py:24
msgid "Translate"
msgstr "Перекласти"
@ -661,6 +660,24 @@ msgstr "Вибрати аудіофайл"
msgid "Unable to save OpenAI API key to keyring"
msgstr "Не вдається додати до звʼязки ключів API-ключ OpenAI"
#: buzz/transcriber/local_whisper_cpp_server_transcriber.py:51
#: buzz/transcriber/recording_transcriber.py:372
msgid "Whisper server failed to start. Check logs for details."
msgstr ""
#: buzz/transcriber/local_whisper_cpp_server_transcriber.py:54
#: buzz/transcriber/recording_transcriber.py:375
msgid ""
"Whisper server failed to start due to insufficient memory. Please try again "
"with a smaller model. To force CPU mode use BUZZ_FORCE_CPU=TRUE environment "
"variable."
msgstr ""
#: buzz/transcriber/transcriber.py:24
#, fuzzy
msgid "Translate to English"
msgstr "Налаштування перекладу"
#: buzz/transcriber/transcriber.py:25
msgid "Transcribe"
msgstr "Розпізнати"
@ -1021,7 +1038,11 @@ msgstr ""
msgid "Cantonese"
msgstr ""
#: buzz/model_loader.py:548
#: buzz/transcriber/recording_transcriber.py:338
msgid "Starting Whisper.cpp..."
msgstr ""
#: buzz/model_loader.py:538
msgid "A connection error occurred"
msgstr "Виникла помилка зʼєднання"

View file

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-06 20:16+0300\n"
"POT-Creation-Date: 2025-07-18 09:13+0300\n"
"PO-Revision-Date: 2023-05-01 15:45+0800\n"
"Last-Translator: \n"
"Language-Team: lamb \n"
@ -301,8 +301,8 @@ msgid "Download failed"
msgstr "下载模型失败"
#: buzz/widgets/preferences_dialog/models_preferences_widget.py:273
#: buzz/widgets/main_window.py:295 buzz/model_loader.py:515
#: buzz/model_loader.py:529
#: buzz/widgets/main_window.py:295 buzz/model_loader.py:505
#: buzz/model_loader.py:519
msgid "Error"
msgstr "错误"
@ -498,11 +498,11 @@ msgstr "等待AI翻译..."
msgid "Microphone:"
msgstr "麦克风:"
#: buzz/widgets/recording_transcriber_widget.py:577
#: buzz/widgets/recording_transcriber_widget.py:579
msgid "An error occurred while starting a new recording:"
msgstr "开始新录制时出错"
#: buzz/widgets/recording_transcriber_widget.py:581
#: buzz/widgets/recording_transcriber_widget.py:583
msgid ""
"Please check your audio devices or check the application logs for more "
"information."
@ -551,7 +551,6 @@ msgid "Export"
msgstr "导出"
#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py:174
#: buzz/transcriber/transcriber.py:24
msgid "Translate"
msgstr "翻译"
@ -674,6 +673,24 @@ msgstr "选择音频文件"
msgid "Unable to save OpenAI API key to keyring"
msgstr "无法将OpenAI API密钥保存到密钥串"
#: buzz/transcriber/local_whisper_cpp_server_transcriber.py:51
#: buzz/transcriber/recording_transcriber.py:372
msgid "Whisper server failed to start. Check logs for details."
msgstr ""
#: buzz/transcriber/local_whisper_cpp_server_transcriber.py:54
#: buzz/transcriber/recording_transcriber.py:375
msgid ""
"Whisper server failed to start due to insufficient memory. Please try again "
"with a smaller model. To force CPU mode use BUZZ_FORCE_CPU=TRUE environment "
"variable."
msgstr ""
#: buzz/transcriber/transcriber.py:24
#, fuzzy
msgid "Translate to English"
msgstr "翻译设置"
#: buzz/transcriber/transcriber.py:25
#, fuzzy
msgid "Transcribe"
@ -1035,7 +1052,11 @@ msgstr ""
msgid "Cantonese"
msgstr ""
#: buzz/model_loader.py:548
#: buzz/transcriber/recording_transcriber.py:338
msgid "Starting Whisper.cpp..."
msgstr ""
#: buzz/model_loader.py:538
msgid "A connection error occurred"
msgstr "连接发生错误"

View file

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-06 20:16+0300\n"
"POT-Creation-Date: 2025-07-18 09:13+0300\n"
"PO-Revision-Date: 2023-05-01 15:45+0800\n"
"Last-Translator: \n"
"Language-Team: Lamb\n"
@ -296,8 +296,8 @@ msgid "Download failed"
msgstr "下載模型"
#: buzz/widgets/preferences_dialog/models_preferences_widget.py:273
#: buzz/widgets/main_window.py:295 buzz/model_loader.py:515
#: buzz/model_loader.py:529
#: buzz/widgets/main_window.py:295 buzz/model_loader.py:505
#: buzz/model_loader.py:519
msgid "Error"
msgstr ""
@ -493,11 +493,11 @@ msgstr ""
msgid "Microphone:"
msgstr "麥克風:"
#: buzz/widgets/recording_transcriber_widget.py:577
#: buzz/widgets/recording_transcriber_widget.py:579
msgid "An error occurred while starting a new recording:"
msgstr "開始新錄製出錯"
#: buzz/widgets/recording_transcriber_widget.py:581
#: buzz/widgets/recording_transcriber_widget.py:583
msgid ""
"Please check your audio devices or check the application logs for more "
"information."
@ -546,7 +546,6 @@ msgid "Export"
msgstr ""
#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py:174
#: buzz/transcriber/transcriber.py:24
msgid "Translate"
msgstr ""
@ -668,6 +667,24 @@ msgstr "選擇聲音檔案"
msgid "Unable to save OpenAI API key to keyring"
msgstr ""
#: buzz/transcriber/local_whisper_cpp_server_transcriber.py:51
#: buzz/transcriber/recording_transcriber.py:372
msgid "Whisper server failed to start. Check logs for details."
msgstr ""
#: buzz/transcriber/local_whisper_cpp_server_transcriber.py:54
#: buzz/transcriber/recording_transcriber.py:375
msgid ""
"Whisper server failed to start due to insufficient memory. Please try again "
"with a smaller model. To force CPU mode use BUZZ_FORCE_CPU=TRUE environment "
"variable."
msgstr ""
#: buzz/transcriber/transcriber.py:24
#, fuzzy
msgid "Translate to English"
msgstr "新錄製"
#: buzz/transcriber/transcriber.py:25
#, fuzzy
msgid "Transcribe"
@ -1029,7 +1046,11 @@ msgstr ""
msgid "Cantonese"
msgstr ""
#: buzz/model_loader.py:548
#: buzz/transcriber/recording_transcriber.py:338
msgid "Starting Whisper.cpp..."
msgstr ""
#: buzz/model_loader.py:538
msgid "A connection error occurred"
msgstr ""

View file

@ -7,7 +7,6 @@ import threading
import shutil
import subprocess
import sys
import tempfile
import warnings
import platform
import requests
@ -23,16 +22,6 @@ from huggingface_hub.errors import LocalEntryNotFoundError
from buzz.locale import _
# Catch exception from whisper.dll not getting loaded.
# TODO: Remove flag and try-except when issue with loading
# the DLL in some envs is fixed.
LOADED_WHISPER_CPP_BINARY = False
try:
import buzz.whisper_cpp as whisper_cpp # noqa: F401
LOADED_WHISPER_CPP_BINARY = True
except ImportError:
logging.exception("")
model_root_dir = user_cache_dir("Buzz")
model_root_dir = os.path.join(model_root_dir, "models")
@ -89,6 +78,7 @@ class ModelType(enum.Enum):
)
def is_available(self):
from buzz.transcriber.whisper_cpp import LOADED_WHISPER_CPP_BINARY
if (
# Hide Whisper.cpp option if whisper.dll did not load correctly.
# See: https://github.com/chidiwilliams/buzz/issues/274,

View file

@ -6,11 +6,13 @@ import shutil
import tempfile
from abc import abstractmethod
from typing import Optional, List
from pathlib import Path
from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot
from yt_dlp import YoutubeDL
from buzz.whisper_audio import SAMPLE_RATE
from buzz import whisper_audio
from buzz.assets import APP_BASE_DIR
from buzz.transcriber.transcriber import (
FileTranscriptionTask,
get_output_file_path,
@ -18,6 +20,9 @@ from buzz.transcriber.transcriber import (
OutputFormat,
)
app_env = os.environ.copy()
app_env['PATH'] = os.pathsep.join([os.path.join(APP_BASE_DIR, "_internal")] + [app_env['PATH']])
class FileTranscriber(QObject):
transcription_task: FileTranscriptionTask
@ -35,6 +40,7 @@ class FileTranscriber(QObject):
if self.transcription_task.source == FileTranscriptionTask.Source.URL_IMPORT:
temp_output_path = tempfile.mktemp()
wav_file = temp_output_path + ".wav"
wav_file = str(Path(wav_file).resolve())
cookiefile = os.getenv("BUZZ_DOWNLOAD_COOKIEFILE")
@ -64,16 +70,17 @@ class FileTranscriber(QObject):
"-threads", "0",
"-i", temp_output_path,
"-ac", "1",
"-ar", str(SAMPLE_RATE),
"-ar", str(whisper_audio.SAMPLE_RATE),
"-acodec", "pcm_s16le",
"-loglevel", "panic",
wav_file]
wav_file
]
if sys.platform == "win32":
si = subprocess.STARTUPINFO()
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
si.wShowWindow = subprocess.SW_HIDE
result = subprocess.run(cmd, capture_output=True, startupinfo=si)
result = subprocess.run(cmd, capture_output=True, startupinfo=si, env=app_env)
else:
result = subprocess.run(cmd, capture_output=True)

View file

@ -0,0 +1,87 @@
import logging
import os
import time
import subprocess
from typing import Optional, List
from PyQt6.QtCore import QObject
from openai import OpenAI
from buzz.locale import _
from buzz.assets import APP_BASE_DIR
from buzz.transcriber.openai_whisper_api_file_transcriber import OpenAIWhisperAPIFileTranscriber
from buzz.transcriber.transcriber import FileTranscriptionTask, Segment
class LocalWhisperCppServerTranscriber(OpenAIWhisperAPIFileTranscriber):
# To be used on Windows only
def __init__(self, task: FileTranscriptionTask, parent: Optional["QObject"] = None) -> None:
super().__init__(task=task, parent=parent)
self.process = None
self.initialization_error = None
command = [
os.path.join(APP_BASE_DIR, "whisper-server.exe"),
"--port", "3000",
"--inference-path", "/audio/transcriptions",
"--threads", str(os.getenv("BUZZ_WHISPERCPP_N_THREADS", (os.cpu_count() or 8)//2)),
"--language", task.transcription_options.language,
"--model", task.model_path
]
logging.debug(f"Starting Whisper server with command: {' '.join(command)}")
self.process = subprocess.Popen(
command,
stdout=subprocess.DEVNULL, # For debug set to subprocess.PIPE, but it will freeze on Windows after ~30 seconds
stderr=subprocess.PIPE,
shell=False,
creationflags=subprocess.CREATE_NO_WINDOW
)
# Wait for server to start and load model
time.sleep(10)
if self.process is not None and self.process.poll() is None:
logging.debug(f"Whisper server started successfully.")
logging.debug(f"Model: {task.model_path}")
else:
stderr_output = self.process.stderr.read().decode()
logging.error(f"Whisper server failed to start. Error: {stderr_output}")
self.initialization_error = _("Whisper server failed to start. Check logs for details.")
if "ErrorOutOfDeviceMemory" in stderr_output:
self.initialization_error = _("Whisper server failed to start due to insufficient memory. "
"Please try again with a smaller model. "
"To force CPU mode use BUZZ_FORCE_CPU=TRUE environment variable.")
return
self.openai_client = OpenAI(
api_key="not-used",
base_url="http://127.0.0.1:3000"
)
def transcribe(self) -> List[Segment]:
if self.initialization_error:
raise Exception(self.initialization_error)
return super().transcribe()
def stop(self):
if self.process and self.process.poll() is None:
try:
self.process.terminate()
self.process.wait(timeout=5)
except subprocess.TimeoutExpired:
# Force kill if terminate doesn't work within 5 seconds
logging.warning("Whisper server didn't terminate gracefully, force killing")
self.process.kill()
try:
self.process.wait(timeout=2)
except subprocess.TimeoutExpired:
logging.error("Failed to kill whisper server process")
except Exception as e:
logging.error(f"Error stopping whisper server: {e}")
def __del__(self):
self.stop()

View file

@ -4,6 +4,8 @@ import os
import sys
import subprocess
import tempfile
from pathlib import Path
from typing import Optional, List
from PyQt6.QtCore import QObject
@ -11,7 +13,7 @@ from openai import OpenAI
from buzz.settings.settings import Settings
from buzz.model_loader import get_custom_api_whisper_model
from buzz.transcriber.file_transcriber import FileTranscriber
from buzz.transcriber.file_transcriber import FileTranscriber, app_env
from buzz.transcriber.transcriber import FileTranscriptionTask, Segment, Task
from buzz.transcriber.whisper_cpp import append_segment
@ -41,6 +43,7 @@ class OpenAIWhisperAPIFileTranscriber(FileTranscriber):
)
mp3_file = tempfile.mktemp() + ".mp3"
mp3_file = str(Path(mp3_file).resolve())
cmd = [
"ffmpeg",
@ -53,7 +56,7 @@ class OpenAIWhisperAPIFileTranscriber(FileTranscriber):
si = subprocess.STARTUPINFO()
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
si.wShowWindow = subprocess.SW_HIDE
result = subprocess.run(cmd, capture_output=True, startupinfo=si)
result = subprocess.run(cmd, capture_output=True, startupinfo=si, env=app_env)
else:
result = subprocess.run(cmd, capture_output=True)
@ -80,7 +83,7 @@ class OpenAIWhisperAPIFileTranscriber(FileTranscriber):
si.wShowWindow = subprocess.SW_HIDE
duration_secs = float(
subprocess.run(cmd, capture_output=True, check=True, startupinfo=si).stdout.decode("utf-8")
subprocess.run(cmd, capture_output=True, check=True, startupinfo=si, env=app_env).stdout.decode("utf-8")
)
else:
duration_secs = float(
@ -107,6 +110,7 @@ class OpenAIWhisperAPIFileTranscriber(FileTranscriber):
chunk_end = min((i + 1) * chunk_duration, duration_secs)
chunk_file = tempfile.mktemp() + ".mp3"
chunk_file = str(Path(chunk_file).resolve())
# fmt: off
cmd = [
@ -122,7 +126,7 @@ class OpenAIWhisperAPIFileTranscriber(FileTranscriber):
si = subprocess.STARTUPINFO()
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
si.wShowWindow = subprocess.SW_HIDE
subprocess.run(cmd, capture_output=True, check=True, startupinfo=si)
subprocess.run(cmd, capture_output=True, check=True, startupinfo=si, env=app_env)
else:
subprocess.run(cmd, capture_output=True, check=True)
@ -168,7 +172,7 @@ class OpenAIWhisperAPIFileTranscriber(FileTranscriber):
segments = getattr(transcript, "segments", None)
words = getattr(transcript, "words", None)
if "words" is None and "words" in transcript.model_extra:
if words is None and "words" in transcript.model_extra:
words = transcript.model_extra["words"]
if segments is None:

View file

@ -2,10 +2,12 @@ import datetime
import logging
import platform
import os
import sys
import wave
import time
import tempfile
import threading
import subprocess
from typing import Optional
from platformdirs import user_cache_dir
@ -17,7 +19,9 @@ from openai import OpenAI
from PyQt6.QtCore import QObject, pyqtSignal
from buzz import whisper_audio
from buzz.model_loader import WhisperModelSize, ModelType, get_custom_api_whisper_model
from buzz.locale import _
from buzz.assets import APP_BASE_DIR
from buzz.model_loader import ModelType, get_custom_api_whisper_model
from buzz.settings.settings import Settings
from buzz.transcriber.transcriber import TranscriptionOptions, Task
from buzz.transcriber.whisper_cpp import WhisperCpp
@ -65,8 +69,11 @@ class RecordingTranscriber(QObject):
self.sounddevice = sounddevice
self.openai_client = None
self.whisper_api_model = get_custom_api_whisper_model("")
self.is_windows = sys.platform == "win32"
self.process = None
def start(self):
model = None
model_path = self.model_path
keep_samples = int(self.keep_sample_seconds * self.sample_rate)
@ -80,7 +87,11 @@ class RecordingTranscriber(QObject):
device = "cuda" if use_cuda else "cpu"
model = whisper.load_model(model_path, device=device)
elif self.transcription_options.model.model_type == ModelType.WHISPER_CPP:
model = WhisperCpp(model_path)
# As DLL mode on Windows is somewhat unreliable, will use local whisper-server
if self.is_windows:
self.start_local_whisper_server()
else:
model = WhisperCpp(model_path)
elif self.transcription_options.model.model_type == ModelType.FASTER_WHISPER:
model_root_dir = user_cache_dir("Buzz")
model_root_dir = os.path.join(model_root_dir, "models")
@ -91,6 +102,10 @@ class RecordingTranscriber(QObject):
logging.debug("Unsupported CUDA version (<12), using CPU")
device = "cpu"
if not torch.cuda.is_available():
logging.debug("CUDA is not available, using CPU")
device = "cpu"
if force_cpu != "false":
device = "cpu"
@ -134,7 +149,6 @@ class RecordingTranscriber(QObject):
)
self.is_running = True
amplitude = 0.0
try:
with self.sounddevice.InputStream(
samplerate=self.sample_rate,
@ -159,7 +173,7 @@ class RecordingTranscriber(QObject):
amplitude,
)
if amplitude < 0.01:
if amplitude < 0.025:
time.sleep(0.5)
continue
@ -181,6 +195,8 @@ class RecordingTranscriber(QObject):
elif (
self.transcription_options.model.model_type
== ModelType.WHISPER_CPP
# On Windows we use the local whisper server via OpenAI API
and not self.is_windows
):
assert isinstance(model, WhisperCpp)
result = model.transcribe(
@ -220,7 +236,11 @@ class RecordingTranscriber(QObject):
task=self.transcription_options.task.value,
)
else: # OPEN_AI_WHISPER_API
assert self.openai_client is not None
if self.openai_client is None:
self.transcription.emit(_("A connection error occurred"))
self.stop_recording()
return
# scale samples to 16-bit PCM
pcm_data = (samples * 32767).astype(np.int16).tobytes()
@ -237,7 +257,7 @@ class RecordingTranscriber(QObject):
options = {
"model": self.whisper_api_model,
"file": temp_file,
"response_format": "verbose_json",
"response_format": "json",
"prompt": self.transcription_options.initial_prompt,
}
@ -251,10 +271,17 @@ class RecordingTranscriber(QObject):
else self.openai_client.audio.translations.create(**options)
)
result = {"text": " ".join(
[segment["text"] for segment in transcript.model_extra["segments"]])}
if "segments" in transcript.model_extra:
result = {"text": " ".join(
[segment["text"] for segment in transcript.model_extra["segments"]])}
else:
result = {"text": transcript.text}
except Exception as e:
result = {"text": f"Error: {str(e)}"}
if self.is_running:
result = {"text": f"Error: {str(e)}"}
else:
result = {"text": ""}
os.unlink(temp_filename)
@ -279,6 +306,12 @@ class RecordingTranscriber(QObject):
self.finished.emit()
# Cleanup
if model:
del model
if torch.cuda.is_available():
torch.cuda.empty_cache()
@staticmethod
def get_device_sample_rate(device_id: Optional[int]) -> int:
"""Returns the sample rate to be used for recording. It uses the default sample rate
@ -308,3 +341,65 @@ class RecordingTranscriber(QObject):
def stop_recording(self):
self.is_running = False
if self.process and self.process.poll() is None:
self.process.terminate()
self.process.wait()
def start_local_whisper_server(self):
self.transcription.emit(_("Starting Whisper.cpp..."))
self.process = None
command = [
os.path.join(APP_BASE_DIR, "whisper-server.exe"),
"--port", "3004",
"--inference-path", "/audio/transcriptions",
"--threads", str(os.getenv("BUZZ_WHISPERCPP_N_THREADS", (os.cpu_count() or 8)//2)),
"--language", self.transcription_options.language,
"--model", self.model_path,
"--no-timestamps",
"--no-context", # on Windows context causes duplications of last message
]
logging.debug(f"Starting Whisper server with command: {' '.join(command)}")
self.process = subprocess.Popen(
command,
stdout=subprocess.DEVNULL, # For debug set to subprocess.PIPE, but it will freeze on Windows after ~30 seconds
stderr=subprocess.PIPE,
shell=False,
creationflags=subprocess.CREATE_NO_WINDOW
)
# Wait for server to start and load model
time.sleep(10)
if self.process is not None and self.process.poll() is None:
logging.debug(f"Whisper server started successfully.")
logging.debug(f"Model: {self.model_path}")
else:
stderr_output = self.process.stderr.read().decode()
logging.error(f"Whisper server failed to start. Error: {stderr_output}")
self.transcription.emit(_("Whisper server failed to start. Check logs for details."))
if "ErrorOutOfDeviceMemory" in stderr_output:
message = _("Whisper server failed to start due to insufficient memory. "
"Please try again with a smaller model. "
"To force CPU mode use BUZZ_FORCE_CPU=TRUE environment variable.")
logging.error(message)
self.transcription.emit(message)
self.transcription.emit(_("Whisper server failed to start. Check logs for details."))
return
self.openai_client = OpenAI(
api_key="not-used",
base_url="http://127.0.0.1:3004",
timeout=10.0,
max_retries=0
)
def __del__(self):
if self.process and self.process.poll() is None:
self.process.terminate()
self.process.wait()

View file

@ -21,7 +21,7 @@ class Task(enum.Enum):
TASK_LABEL_TRANSLATIONS = {
Task.TRANSLATE: _("Translate"),
Task.TRANSLATE: _("Translate to English"),
Task.TRANSCRIBE: _("Transcribe"),
}

View file

@ -1,5 +1,6 @@
import platform
import os
import sys
import ctypes
import logging
from typing import Union, Any, List
@ -7,21 +8,43 @@ from typing import Union, Any, List
import numpy as np
from buzz import whisper_audio
from buzz.model_loader import LOADED_WHISPER_CPP_BINARY
from buzz.transcriber.transcriber import Segment, Task, TranscriptionOptions
if LOADED_WHISPER_CPP_BINARY:
from buzz import whisper_cpp
LOADED_WHISPER_CPP_BINARY = False
IS_VULKAN_SUPPORTED = False
try:
import vulkan
IS_COREML_SUPPORTED = False
if platform.system() == "Darwin" and platform.machine() == "arm64":
instance = vulkan.vkCreateInstance(vulkan.VkInstanceCreateInfo(), None)
vulkan.vkDestroyInstance(instance, None)
vulkan_version = vulkan.vkEnumerateInstanceVersion()
major = (vulkan_version >> 22) & 0x3FF
minor = (vulkan_version >> 12) & 0x3FF
logging.debug("Vulkan version = %s.%s", major, minor)
# On macOS, default whisper_cpp is compiled with CoreML (Apple Silicon) or Vulkan (Intel).
# On Windows whisper-server.exe subprocess will be used
if (platform.system() == "Linux") and ((major > 1) or (major == 1 and minor >= 2)):
from buzz.whisper_cpp_vulkan import whisper_cpp_vulkan
IS_VULKAN_SUPPORTED = True
LOADED_WHISPER_CPP_BINARY = True
except (ImportError, Exception) as e:
logging.debug(f"Vulkan import error: {e}")
IS_VULKAN_SUPPORTED = False
if not IS_VULKAN_SUPPORTED:
try:
from buzz import whisper_cpp_coreml # noqa: F401
from buzz.whisper_cpp import whisper_cpp # noqa: F401
IS_COREML_SUPPORTED = True
except ImportError:
logging.exception("")
LOADED_WHISPER_CPP_BINARY = True
except ImportError as e:
logging.exception("whisper_cpp load error: %s", e)
def append_segment(result, txt: bytes, start: int, end: int):
if txt == b'':
@ -45,14 +68,9 @@ def append_segment(result, txt: bytes, start: int, end: int):
class WhisperCpp:
def __init__(self, model: str) -> None:
self.is_coreml_supported = IS_COREML_SUPPORTED
self.is_vulkan_supported = IS_VULKAN_SUPPORTED
if self.is_coreml_supported:
coreml_model = model.replace(".bin", "-encoder.mlmodelc")
if not os.path.exists(coreml_model):
self.is_coreml_supported = False
logging.debug(f"WhisperCpp model {model}, (Core ML: {self.is_coreml_supported})")
logging.debug(f"WhisperCpp model {model}, Vulkan: {self.is_vulkan_supported})")
self.instance = self.get_instance()
self.ctx = self.instance.init_from_file(model)
@ -120,8 +138,8 @@ class WhisperCpp:
}
def get_instance(self):
if self.is_coreml_supported:
return WhisperCppCoreML()
if self.is_vulkan_supported:
return WhisperCppVulkan()
return WhisperCppCpu()
def get_params(
@ -130,7 +148,7 @@ class WhisperCpp:
print_realtime=False,
print_progress=False,
):
params = self.instance.full_default_params(whisper_cpp.WHISPER_SAMPLING_GREEDY)
params = self.instance.full_default_params(0) # WHISPER_SAMPLING_GREEDY
params.n_threads = int(os.getenv("BUZZ_WHISPERCPP_N_THREADS", (os.cpu_count() or 8)//2))
params.print_realtime = print_realtime
params.print_progress = print_progress
@ -216,38 +234,41 @@ class WhisperCppCpu(WhisperCppInterface):
def free(self, ctx):
if ctx and whisper_cpp is not None:
return whisper_cpp.whisper_free(ctx)
return None
class WhisperCppCoreML(WhisperCppInterface):
class WhisperCppVulkan(WhisperCppInterface):
def full_default_params(self, sampling: int):
return whisper_cpp_coreml.whisper_full_default_params(sampling)
return whisper_cpp_vulkan.whisper_full_default_params(sampling)
def get_string(self, string: str):
return whisper_cpp_coreml.String(string.encode())
return whisper_cpp_vulkan.String(string.encode())
def get_encoder_begin_callback(self, callback):
return whisper_cpp_coreml.whisper_encoder_begin_callback(callback)
return whisper_cpp_vulkan.whisper_encoder_begin_callback(callback)
def get_new_segment_callback(self, callback):
return whisper_cpp_coreml.whisper_new_segment_callback(callback)
return whisper_cpp_vulkan.whisper_new_segment_callback(callback)
def init_from_file(self, model: str):
return whisper_cpp_coreml.whisper_init_from_file(model.encode())
return whisper_cpp_vulkan.whisper_init_from_file(model.encode())
def full(self, ctx, params, audio, length):
return whisper_cpp_coreml.whisper_full(ctx, params, audio, length)
return whisper_cpp_vulkan.whisper_full(ctx, params, audio, length)
def full_n_segments(self, ctx):
return whisper_cpp_coreml.whisper_full_n_segments(ctx)
return whisper_cpp_vulkan.whisper_full_n_segments(ctx)
def full_get_segment_text(self, ctx, i):
return whisper_cpp_coreml.whisper_full_get_segment_text(ctx, i)
return whisper_cpp_vulkan.whisper_full_get_segment_text(ctx, i)
def full_get_segment_t0(self, ctx, i):
return whisper_cpp_coreml.whisper_full_get_segment_t0(ctx, i)
return whisper_cpp_vulkan.whisper_full_get_segment_t0(ctx, i)
def full_get_segment_t1(self, ctx, i):
return whisper_cpp_coreml.whisper_full_get_segment_t1(ctx, i)
return whisper_cpp_vulkan.whisper_full_get_segment_t1(ctx, i)
def free(self, ctx):
return whisper_cpp_coreml.whisper_free(ctx)
if ctx and whisper_cpp_vulkan is not None:
return whisper_cpp_vulkan.whisper_free(ctx)
return None

View file

@ -1,6 +1,7 @@
import ctypes
import logging
import sys
import os
from typing import Optional, List
from PyQt6.QtCore import QObject
@ -9,6 +10,7 @@ from buzz import whisper_audio
from buzz.transcriber.file_transcriber import FileTranscriber
from buzz.transcriber.transcriber import FileTranscriptionTask, Segment, Stopped
from buzz.transcriber.whisper_cpp import WhisperCpp
from buzz.transcriber.local_whisper_cpp_server_transcriber import LocalWhisperCppServerTranscriber
class WhisperCppFileTranscriber(FileTranscriber):
@ -25,7 +27,16 @@ class WhisperCppFileTranscriber(FileTranscriber):
self.transcription_options = task.transcription_options
self.model_path = task.model_path
self.model = WhisperCpp(model=self.model_path)
self.transcriber = None
self.model = None
is_windows = sys.platform == "win32"
force_cpu = os.getenv("BUZZ_FORCE_CPU", "false")
# As DLL mode on Windows is somewhat unreliable, will use local whisper-server
if is_windows and force_cpu == "false":
self.transcriber = LocalWhisperCppServerTranscriber(task, parent)
else:
self.model = WhisperCpp(model=self.model_path)
self.state = self.State()
def transcribe(self) -> List[Segment]:
@ -41,32 +52,42 @@ class WhisperCppFileTranscriber(FileTranscriber):
self.transcription_options.word_level_timings,
)
audio = whisper_audio.load_audio(self.transcription_task.file_path)
self.duration_audio_ms = len(audio) * 1000 / whisper_audio.SAMPLE_RATE
if self.model:
audio = whisper_audio.load_audio(self.transcription_task.file_path)
self.duration_audio_ms = len(audio) * 1000 / whisper_audio.SAMPLE_RATE
whisper_params = self.model.get_params(
transcription_options=self.transcription_options
)
whisper_params.encoder_begin_callback_user_data = ctypes.c_void_p(
id(self.state)
)
whisper_params.encoder_begin_callback = (
self.model.get_instance().get_encoder_begin_callback(self.encoder_begin_callback)
)
whisper_params.new_segment_callback_user_data = ctypes.c_void_p(id(self.state))
whisper_params.new_segment_callback = self.model.get_instance().get_new_segment_callback(
self.new_segment_callback
)
whisper_params = self.model.get_params(
transcription_options=self.transcription_options
)
whisper_params.encoder_begin_callback_user_data = ctypes.c_void_p(
id(self.state)
)
whisper_params.encoder_begin_callback = (
self.model.get_instance().get_encoder_begin_callback(self.encoder_begin_callback)
)
whisper_params.new_segment_callback_user_data = ctypes.c_void_p(id(self.state))
whisper_params.new_segment_callback = self.model.get_instance().get_new_segment_callback(
self.new_segment_callback
)
result = self.model.transcribe(
audio=self.transcription_task.file_path, params=whisper_params
)
result = self.model.transcribe(
audio=self.transcription_task.file_path, params=whisper_params
)
if not self.state.running:
raise Stopped
self.state.running = False
return result["segments"]
# On Windows we use the local whisper server
if self.transcriber is not None:
return self.transcriber.transcribe()
if not self.state.running:
raise Stopped
self.state.running = False
return result["segments"]
return []
def new_segment_callback(self, ctx, _state, _n_new, user_data):
n_segments = self.model.get_instance().full_n_segments(ctx)
@ -89,3 +110,9 @@ class WhisperCppFileTranscriber(FileTranscriber):
def stop(self):
self.state.running = False
if self.transcriber is not None:
self.transcriber.stop()
def __del__(self):
self.stop()

View file

@ -46,6 +46,8 @@ class WhisperFileTranscriber(FileTranscriber):
self.segments = []
self.started_process = False
self.stopped = False
self.recv_pipe = None
self.send_pipe = None
def transcribe(self) -> List[Segment]:
time_started = datetime.datetime.now()
@ -56,24 +58,30 @@ class WhisperFileTranscriber(FileTranscriber):
if torch.cuda.is_available():
logging.debug(f"CUDA version detected: {torch.version.cuda}")
recv_pipe, send_pipe = multiprocessing.Pipe(duplex=False)
self.recv_pipe, self.send_pipe = multiprocessing.Pipe(duplex=False)
self.current_process = multiprocessing.Process(
target=self.transcribe_whisper, args=(send_pipe, self.transcription_task)
target=self.transcribe_whisper, args=(self.send_pipe, self.transcription_task)
)
if not self.stopped:
self.current_process.start()
self.started_process = True
self.read_line_thread = Thread(target=self.read_line, args=(recv_pipe,))
self.read_line_thread = Thread(target=self.read_line, args=(self.recv_pipe,))
self.read_line_thread.start()
self.current_process.join()
if self.current_process.exitcode != 0:
send_pipe.close()
self.send_pipe.close()
self.read_line_thread.join()
# Join read_line_thread with timeout to prevent hanging
if self.read_line_thread and self.read_line_thread.is_alive():
self.read_line_thread.join(timeout=3)
if self.read_line_thread.is_alive():
logging.warning("Read line thread didn't terminate gracefully in transcribe()")
self.started_process = False
logging.debug(
"whisper process completed with code = %s, time taken = %s,"
@ -153,6 +161,10 @@ class WhisperFileTranscriber(FileTranscriber):
logging.debug("Unsupported CUDA version (<12), using CPU")
device = "cpu"
if not torch.cuda.is_available():
logging.debug("CUDA is not available, using CPU")
device = "cpu"
if force_cpu != "false":
device = "cpu"
@ -168,7 +180,8 @@ class WhisperFileTranscriber(FileTranscriber):
audio=task.file_path,
language=task.transcription_options.language,
task=task.transcription_options.task.value,
temperature=task.transcription_options.temperature,
# Prevent crash on Windows https://github.com/SYSTRAN/faster-whisper/issues/71#issuecomment-1526263764
temperature = 0 if platform.system() == "Windows" else task.transcription_options.temperature,
initial_prompt=task.transcription_options.initial_prompt,
word_timestamps=task.transcription_options.word_level_timings,
no_speech_threshold=0.4,
@ -249,8 +262,30 @@ class WhisperFileTranscriber(FileTranscriber):
def stop(self):
self.stopped = True
if self.started_process:
self.current_process.terminate()
# Use timeout to avoid hanging indefinitely
self.current_process.join(timeout=5)
if self.current_process.is_alive():
logging.warning("Process didn't terminate gracefully, force killing")
self.current_process.kill()
self.current_process.join(timeout=2)
# Close pipes to unblock the read_line thread
try:
if hasattr(self, 'send_pipe'):
self.send_pipe.close()
if hasattr(self, 'recv_pipe'):
self.recv_pipe.close()
except Exception as e:
logging.debug(f"Error closing pipes: {e}")
# Join read_line_thread with timeout to prevent hanging
if self.read_line_thread and self.read_line_thread.is_alive():
self.read_line_thread.join(timeout=3)
if self.read_line_thread.is_alive():
logging.warning("Read line thread didn't terminate gracefully")
def read_line(self, pipe: Connection):
while True:
@ -260,7 +295,10 @@ class WhisperFileTranscriber(FileTranscriber):
# Uncomment to debug
# print(f"*** DEBUG ***: {line}")
except EOFError: # Connection closed
except (EOFError, BrokenPipeError, ConnectionResetError): # Connection closed or broken
break
except Exception as e:
logging.debug(f"Error reading from pipe: {e}")
break
if line == self.READ_LINE_THREAD_STOP_TOKEN:

View file

@ -17,7 +17,8 @@ class PipelineWithProgress(AutomaticSpeechRecognitionPipeline): # pragma: no co
inputs_len = inputs.shape[0]
step = chunk_len - stride_left - stride_right
for chunk_start_idx in range(0, inputs_len, step):
# Print progress to stderr
# Buzz will print progress to stderr
progress = int((chunk_start_idx / inputs_len) * 100)
sys.stderr.write(f"{progress}%\n")
@ -27,8 +28,7 @@ class PipelineWithProgress(AutomaticSpeechRecognitionPipeline): # pragma: no co
if dtype is not None:
processed = processed.to(dtype=dtype)
_stride_left = 0 if chunk_start_idx == 0 else stride_left
# all right strides must be full, otherwise it is the last item
is_last = chunk_end_idx > inputs_len if stride_right > 0 else chunk_end_idx >= inputs_len
is_last = chunk_end_idx >= inputs_len
_stride_right = 0 if is_last else stride_right
chunk_len = chunk.shape[0]
@ -98,7 +98,7 @@ class PipelineWithProgress(AutomaticSpeechRecognitionPipeline): # pragma: no co
# of the original length in the stride so we can cut properly.
stride = (inputs.shape[0], int(round(stride[0] * ratio)), int(round(stride[1] * ratio)))
if not isinstance(inputs, np.ndarray):
raise ValueError(f"We expect a numpy ndarray as input, got `{type(inputs)}`")
raise TypeError(f"We expect a numpy ndarray as input, got `{type(inputs)}`")
if len(inputs.shape) != 1:
raise ValueError("We expect a single channel audio input for AutomaticSpeechRecognitionPipeline")
@ -109,7 +109,7 @@ class PipelineWithProgress(AutomaticSpeechRecognitionPipeline): # pragma: no co
if isinstance(stride_length_s, (int, float)):
stride_length_s = [stride_length_s, stride_length_s]
# XXX: Carefuly, this variable will not exist in `seq2seq` setting.
# XXX: Carefully, this variable will not exist in `seq2seq` setting.
# Currently chunking is not possible at this level for `seq2seq` so
# it's ok.
align_to = getattr(self.model.config, "inputs_to_logits_ratio", 1)
@ -120,11 +120,11 @@ class PipelineWithProgress(AutomaticSpeechRecognitionPipeline): # pragma: no co
if chunk_len < stride_left + stride_right:
raise ValueError("Chunk length must be superior to stride length")
# Will use our custom chunk_iter with progress
# Buzz use our custom chunk_iter with progress
for item in self.chunk_iter(
inputs, self.feature_extractor, chunk_len, stride_left, stride_right, self.torch_dtype
):
yield item
yield {**item, **extra}
else:
if self.type == "seq2seq_whisper" and inputs.shape[0] > self.feature_extractor.n_samples:
processed = self.feature_extractor(
@ -133,12 +133,25 @@ class PipelineWithProgress(AutomaticSpeechRecognitionPipeline): # pragma: no co
truncation=False,
padding="longest",
return_tensors="pt",
return_attention_mask=True,
)
else:
processed = self.feature_extractor(
inputs, sampling_rate=self.feature_extractor.sampling_rate, return_tensors="pt"
)
if self.type == "seq2seq_whisper" and stride is None:
processed = self.feature_extractor(
inputs,
sampling_rate=self.feature_extractor.sampling_rate,
return_tensors="pt",
return_token_timestamps=True,
return_attention_mask=True,
)
extra["num_frames"] = processed.pop("num_frames")
else:
processed = self.feature_extractor(
inputs,
sampling_rate=self.feature_extractor.sampling_rate,
return_tensors="pt",
return_attention_mask=True,
)
if self.torch_dtype is not None:
processed = processed.to(dtype=self.torch_dtype)
if stride is not None:
@ -193,7 +206,8 @@ class TransformersWhisper:
chunk_length_s=30 if word_timestamps else None,
torch_dtype=torch_dtype,
device=device,
)
ignore_warning=True # Ignore warning about chunk_length_s being experimental for seq2seq models
)
transcript = pipe(
audio,

View file

@ -430,6 +430,8 @@ class MainWindow(QMainWindow):
if self.transcription_viewer_widget is not None:
self.transcription_viewer_widget.close()
logging.debug("Closing MainWindow")
super().closeEvent(event)
def save_geometry(self):

View file

@ -187,7 +187,7 @@ class GeneralPreferencesWidget(QWidget):
def on_click_test_openai_api_key_button(self):
self.test_openai_api_key_button.setEnabled(False)
job = TestOpenAIApiKeyJob(api_key=self.openai_api_key)
job = ValidateOpenAIApiKeyJob(api_key=self.openai_api_key)
job.signals.success.connect(self.on_test_openai_api_key_success)
job.signals.failed.connect(self.on_test_openai_api_key_failure)
job.setAutoDelete(True)
@ -265,7 +265,7 @@ class GeneralPreferencesWidget(QWidget):
def on_recording_transcriber_mode_changed(self, value):
self.settings.set_value(Settings.Key.RECORDING_TRANSCRIBER_MODE, value)
class TestOpenAIApiKeyJob(QRunnable):
class ValidateOpenAIApiKeyJob(QRunnable):
class Signals(QObject):
success = pyqtSignal()
failed = pyqtSignal(str)

View file

@ -306,7 +306,9 @@ class RecordingTranscriberWidget(QWidget):
self.reset_recording_controls()
self.model_loader = None
if model_path == "":
if model_path == "" and self.transcription_options.model.model_type != ModelType.OPEN_AI_WHISPER_API:
self.on_transcriber_error("")
logging.error("Model path is empty, cannot start recording.")
return
self.transcription_thread = QThread()

View file

@ -1,6 +0,0 @@
DLLs extracted from whisper.cpp release v1.6.2
https://github.com/ggerganov/whisper.cpp/actions/runs/9251036100
https://github.com/ggerganov/whisper.cpp/commit/c7b6988678779901d02ceba1a8212d2c9908956e
whisper.cpp submodule commit version should match the commit where the
whisper.dll are extracted from to ensure compatibility.

Binary file not shown.

Binary file not shown.

33
poetry.lock generated
View file

@ -1181,7 +1181,6 @@ description = "Fast transfer of large files with the Hugging Face Hub."
optional = false
python-versions = ">=3.8"
groups = ["main"]
markers = "platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\""
files = [
{file = "hf_xet-1.1.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f52c2fa3635b8c37c7764d8796dfa72706cc4eded19d638331161e82b0792e23"},
{file = "hf_xet-1.1.5-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:9fa6e3ee5d61912c4a113e0708eaaef987047616465ac7aa30f7121a48fc1af8"},
@ -3427,6 +3426,21 @@ pytest = "*"
dev = ["pre-commit", "tox"]
doc = ["sphinx", "sphinx-rtd-theme"]
[[package]]
name = "pytest-timeout"
version = "2.4.0"
description = "pytest plugin to abort hanging tests"
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2"},
{file = "pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a"},
]
[package.dependencies]
pytest = ">=7.0.0"
[[package]]
name = "pytest-xvfb"
version = "2.0.0"
@ -4790,6 +4804,21 @@ platformdirs = ">=3.9.1,<5"
docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""]
[[package]]
name = "vulkan"
version = "1.3.275.1"
description = "Ultimate Python binding for Vulkan API"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "vulkan-1.3.275.1-py3-none-any.whl", hash = "sha256:e1e0ddf57d3a7d19f79ebf1e192b20dbd378172b027cad4f495d961b51409586"},
{file = "vulkan-1.3.275.1.tar.gz", hash = "sha256:5cf7961cb4a4e60d063eb815147c2289244575b75a4512089cc6e8a959b996d2"},
]
[package.dependencies]
cffi = ">=1.10"
[[package]]
name = "wrapt"
version = "1.17.2"
@ -4925,4 +4954,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.9,<3.13"
content-hash = "6fd00715df18f91cd02b7524a34897f408f686eab80fd77b2d8eefd4f2ce99ca"
content-hash = "068de55b5f53b353c9ac9880a1d420f4187decbd547f8b2b871f3dc790a977c8"

View file

@ -20,12 +20,9 @@ packages = [
{ include = "buzz" },
]
include = [
{ path = "buzz/libwhisper.*", format = ["sdist", "wheel"] },
{ path = "buzz/libwhisper-coreml.*", format = ["sdist", "wheel"] },
{ path = "buzz/*.dll", format = ["sdist", "wheel"] },
{ path = "buzz/whisper_cpp.py", format = ["sdist", "wheel"] },
{ path = "buzz/whisper_cpp/*", format = ["sdist", "wheel"] },
{ path = "buzz/whisper_cpp_vulkan/*", format = ["sdist", "wheel"] },
{ path = "buzz/locale/*/LC_MESSAGES/buzz.mo", format = ["sdist", "wheel"] },
{ path = "buzz/dll_backup/*", format = ["sdist", "wheel"] },
]
[[tool.poetry.source]]
@ -80,6 +77,8 @@ urllib3 = "^2.3.0"
demucs = {url = "https://github.com/raivisdejus/demucs/releases/download/4.1.0a3/demucs-4.1.0a3-py3-none-any.whl"}
posthog = "^3.23.0"
onnxruntime = "^1.18.0"
vulkan = "^1.3.275.1"
hf-xet = "^1.1.5"
[tool.poetry.group.dev.dependencies]
autopep8 = "^1.7.0"
@ -91,6 +90,7 @@ pytest-cov = "^4.0.0"
pytest-qt = "^4.1.0"
pytest-xvfb = "^2.0.0"
pytest-mock = "^3.12.0"
pytest-timeout = "^2.4.0"
pylint = "^2.15.5"
pre-commit = "^2.20.0"
pytest-benchmark = "^4.0.0"

View file

@ -2,5 +2,8 @@
log_cli = 1
log_cli_level = DEBUG
qt_api=pyqt6
log_format = %(asctime)s %(levelname)s %(message)s
log_format = %(asctime)s %(levelname)s %(module)s::%(funcName)s %(message)s
log_date_format = %Y-%m-%d %H:%M:%S
addopts = -x
markers =
timeout: set a timeout on a test function.

View file

@ -1,5 +1,5 @@
# Development notes:
# - To build the snap run `snapcraft clean` and `snapcraft --debug --verbosity verbose`
# - To build the snap run `snapcraft clean` and `snapcraft --verbose`
# - To install local snap `snap install ./buzz_*.snap --dangerous`
name: buzz
base: core22
@ -83,7 +83,12 @@ parts:
plugin: python
source: .
build-packages:
- wget
- portaudio19-dev
- qt6-declarative-dev
- qt6-multimedia-dev
- libvulkan-dev
- cmake
stage-packages:
# Audio
- ffmpeg
@ -100,8 +105,6 @@ parts:
- libgstreamer1.0-0
- libgstreamer-plugins-base1.0-0
- libgstreamer-plugins-good1.0-0
- qt6-declarative-dev
- qt6-multimedia-dev
# Display
- libxkbcommon-x11-0
- libxcb-icccm4
@ -114,7 +117,20 @@ parts:
- libxcb-cursor0
# GPU
- libglu1-mesa
- libvulkan1
- mesa-vulkan-drivers
python-packages:
- ctypesgen
- setuptools
- cmake
- polib
override-build: |
# https://vulkan.lunarg.com/doc/view/latest/linux/getting_started_ubuntu.html
wget -qO- https://packages.lunarg.com/lunarg-signing-key-pub.asc | tee /etc/apt/trusted.gpg.d/lunarg.asc
wget -qO /etc/apt/sources.list.d/lunarg-vulkan-jammy.list http://packages.lunarg.com/vulkan/lunarg-vulkan-jammy.list
apt update
apt install -y vulkan-sdk
# Clear cache to save space on CI
apt clean
@ -122,39 +138,48 @@ parts:
pip install poetry
make translation_mo
pip install .
pip uninstall -y ctranslate2
pip uninstall -y torch torchaudio nvidia-cublas-cu12 nvidia-cuda-cupti-cu12 nvidia-cuda-nvrtc-cu12 nvidia-cuda-runtime-cu12 nvidia-cudnn-cu12 nvidia-cufft-cu12 nvidia-cufile-cu12 nvidia-curand-cu12 nvidia-cusolver-cu12 nvidia-cusparse-cu12 nvidia-cusparselt-cu12 nvidia-nccl-cu12 nvidia-nvjitlink-cu12 nvidia-nvtx-cu12
pip cache purge
pip install https://github.com/raivisdejus/CTranslate2-no-execstack/releases/download/v4.2.1/ctranslate2-4.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
pip install -U torch==2.7.1+cu128 torchaudio==2.7.1+cu128 --index-url https://download.pytorch.org/whl/cu128
pip cache purge
pip install nvidia-cublas-cu12==12.8.3.14 nvidia-cuda-cupti-cu12==12.8.57 nvidia-cuda-nvrtc-cu12==12.8.61 --extra-index-url https://pypi.ngc.nvidia.com
pip cache purge
pip install nvidia-cuda-runtime-cu12==12.8.57 nvidia-cudnn-cu12==9.7.1.26 nvidia-cufft-cu12==11.3.3.41 --extra-index-url https://pypi.ngc.nvidia.com
pip cache purge
pip install nvidia-curand-cu12==10.3.9.55 nvidia-cusolver-cu12==11.7.2.55 nvidia-cusparse-cu12==12.5.4.2 --extra-index-url https://pypi.ngc.nvidia.com
pip install nvidia-curand-cu12==10.3.9.55 nvidia-cusolver-cu12==11.7.2.55 nvidia-cusparse-cu12==12.5.7.53 --extra-index-url https://pypi.ngc.nvidia.com
pip cache purge
pip install nvidia-cusparselt-cu12==0.6.3 nvidia-nvjitlink-cu12==12.8.61 nvidia-nvtx-cu12==12.8.55 --extra-index-url https://pypi.ngc.nvidia.com
pip cache purge
python3 build.py
mkdir $CRAFT_PART_INSTALL/buzz
cp $CRAFT_PART_BUILD/buzz/whisper_cpp.py $CRAFT_PART_INSTALL/buzz/
cp $CRAFT_PART_BUILD/buzz/libwhisper.so $CRAFT_PART_INSTALL/buzz/
cp -r $CRAFT_PART_BUILD/buzz/whisper_cpp $CRAFT_PART_INSTALL/buzz/
cp -r $CRAFT_PART_BUILD/buzz/whisper_cpp_vulkan $CRAFT_PART_INSTALL/buzz/
# Create desktop file
mkdir -p $CRAFT_PART_INSTALL/usr/share/applications
cp $CRAFT_PART_BUILD/buzz.desktop $CRAFT_PART_INSTALL/usr/share/applications/
after: [ desktop-qt5 ]
desktop-file:
graphics-core22:
after: [ buzz ]
source: https://github.com/canonical/gpu-snap.git
plugin: dump
source: .
organize:
buzz.desktop: usr/share/applications/buzz.desktop
# To prevent error of the same file having different permissions
override-prime: |
craftctl default
${CRAFT_PART_SRC}/bin/graphics-core22-cleanup mesa-core22 nvidia-core22
stage:
- -buzz/whisper_cpp.py
- ./*
- -venv/**
prime:
- ./*
- -venv/**
- bin/graphics-core22-wrapper
apps:
buzz:
command-chain:
- bin/graphics-core22-wrapper
command: bin/desktop-launch python3 -m buzz
desktop: usr/share/applications/buzz.desktop
environment:
@ -202,7 +227,19 @@ plugs:
interface: content
target: $SNAP/share/sounds
default-provider: gtk-common-themes
graphics-core22:
interface: content
target: $SNAP/graphics
default-provider: mesa-core22
layout:
/usr/lib/$SNAPCRAFT_ARCH_TRIPLET/alsa-lib:
bind: $SNAP/usr/lib/$SNAPCRAFT_ARCH_TRIPLET/alsa-lib
/usr/share/libdrm:
bind: $SNAP/graphics/libdrm
/usr/share/drirc.d:
symlink: $SNAP/graphics/drirc.d
/usr/share/X11/XErrorDB:
symlink: $SNAP/graphics/X11/XErrorDB
/usr/share/X11/locale:
symlink: $SNAP/graphics/X11/locale

View file

@ -2,12 +2,13 @@ import pytest
import unittest.mock
from PyQt6.QtCore import QCoreApplication, QThread
from buzz.file_transcriber_queue_worker import FileTranscriberQueueWorker
from buzz.model_loader import ModelType, TranscriptionModel
from buzz.model_loader import ModelType, TranscriptionModel, WhisperModelSize
from buzz.transcriber.transcriber import FileTranscriptionTask, TranscriptionOptions, FileTranscriptionOptions
from buzz.transcriber.whisper_cpp_file_transcriber import WhisperCppFileTranscriber
from tests.audio import test_multibyte_utf8_audio_path
import time
@pytest.fixture(scope="session")
def qapp():
app = QCoreApplication.instance()
@ -16,6 +17,7 @@ def qapp():
yield app
app.quit()
@pytest.fixture
def worker(qapp):
worker = FileTranscriberQueueWorker()
@ -28,29 +30,27 @@ def worker(qapp):
thread.quit()
thread.wait()
@pytest.fixture
def audio_file():
# Use a small, existing audio file for testing
return test_multibyte_utf8_audio_path
def test_transcription_with_whisper_cpp_tiny_no_speech_extraction(worker, audio_file):
def test_transcription_with_whisper_cpp_tiny_no_speech_extraction(worker):
options = TranscriptionOptions(
model=TranscriptionModel(model_type=ModelType.WHISPER_CPP, whisper_model_size="tiny"),
model=TranscriptionModel(model_type=ModelType.WHISPER_CPP, whisper_model_size=WhisperModelSize.TINY),
extract_speech=False
)
task = FileTranscriptionTask(file_path=str(audio_file), transcription_options=options, file_transcription_options=FileTranscriptionOptions(), model_path="mock_path")
task = FileTranscriptionTask(file_path=str(test_multibyte_utf8_audio_path), transcription_options=options,
file_transcription_options=FileTranscriptionOptions(), model_path="mock_path")
with unittest.mock.patch.object(WhisperCppFileTranscriber, 'run') as mock_run:
with unittest.mock.patch('buzz.transcriber.whisper_cpp_file_transcriber.LocalWhisperCppServerTranscriber'), \
unittest.mock.patch.object(WhisperCppFileTranscriber, 'run') as mock_run:
mock_run.side_effect = lambda: worker.current_transcriber.completed.emit([
{"start": 0, "end": 1000, "text": "Test transcription."}
])
completed_spy = unittest.mock.Mock()
worker.task_completed.connect(completed_spy)
worker.add_task(task)
# Wait for the signal to be emitted
timeout = 5 # seconds
timeout = 10 # seconds
start_time = time.time()
while not completed_spy.called and (time.time() - start_time) < timeout:
QCoreApplication.processEvents()
@ -62,17 +62,19 @@ def test_transcription_with_whisper_cpp_tiny_no_speech_extraction(worker, audio_
assert len(args[1]) > 0
assert args[1][0]["text"] == "Test transcription."
def test_transcription_with_whisper_cpp_tiny_with_speech_extraction(worker, audio_file):
def test_transcription_with_whisper_cpp_tiny_with_speech_extraction(worker):
options = TranscriptionOptions(
model=TranscriptionModel(model_type=ModelType.WHISPER_CPP, whisper_model_size="tiny"),
model=TranscriptionModel(model_type=ModelType.WHISPER_CPP, whisper_model_size=WhisperModelSize.TINY),
extract_speech=True
)
task = FileTranscriptionTask(file_path=str(audio_file), transcription_options=options, file_transcription_options=FileTranscriptionOptions(), model_path="mock_path")
with unittest.mock.patch('demucs.api.Separator') as mock_separator_class, \
unittest.mock.patch('demucs.api.save_audio') as mock_save_audio, \
unittest.mock.patch.object(WhisperCppFileTranscriber, 'run') as mock_run:
task = FileTranscriptionTask(file_path=str(test_multibyte_utf8_audio_path), transcription_options=options,
file_transcription_options=FileTranscriptionOptions(), model_path="mock_path")
with unittest.mock.patch('buzz.transcriber.whisper_cpp_file_transcriber.LocalWhisperCppServerTranscriber'), \
unittest.mock.patch('demucs.api.Separator') as mock_separator_class, \
unittest.mock.patch('demucs.api.save_audio') as mock_save_audio, \
unittest.mock.patch.object(WhisperCppFileTranscriber, 'run') as mock_run:
# Mock demucs.api.Separator and save_audio
mock_separator_instance = unittest.mock.Mock()
mock_separator_instance.separate_audio_file.return_value = (None, {"vocals": "mock_vocals_data"})
@ -88,7 +90,7 @@ def test_transcription_with_whisper_cpp_tiny_with_speech_extraction(worker, audi
worker.add_task(task)
# Wait for the signal to be emitted
timeout = 5 # seconds
timeout = 10 # seconds
start_time = time.time()
while not completed_spy.called and (time.time() - start_time) < timeout:
QCoreApplication.processEvents()
@ -100,4 +102,4 @@ def test_transcription_with_whisper_cpp_tiny_with_speech_extraction(worker, audi
args, kwargs = completed_spy.call_args
assert args[0] == task
assert len(args[1]) > 0
assert args[1][0]["text"] == "Test transcription with speech extraction."
assert args[1][0]["text"] == "Test transcription with speech extraction."

View file

@ -1,7 +1,12 @@
import os
import sys
import time
from unittest.mock import Mock, patch
from PyQt6.QtCore import QThread
from buzz.locale import _
from buzz.assets import APP_BASE_DIR
from buzz.model_loader import TranscriptionModel, ModelType, WhisperModelSize
from buzz.transcriber.recording_transcriber import RecordingTranscriber
from buzz.transcriber.transcriber import TranscriptionOptions, Task
@ -21,6 +26,10 @@ class TestRecordingTranscriber:
model_path = get_model_path(transcription_model)
model_exe_path = os.path.join(APP_BASE_DIR, "whisper-server.exe")
if sys.platform.startswith("win"):
assert os.path.exists(model_exe_path), f"{model_exe_path} does not exist"
transcriber = RecordingTranscriber(
transcription_options=TranscriptionOptions(
model=transcription_model, language="fr", task=Task.TRANSCRIBE
@ -34,17 +43,24 @@ class TestRecordingTranscriber:
thread.started.connect(transcriber.start)
mock_transcription = Mock()
transcriber.transcription.connect(mock_transcription)
transcriptions = []
with qtbot.wait_signal(transcriber.transcription, timeout=60 * 1000):
thread.start()
def on_transcription(text):
transcriptions.append(text)
transcriber.stop_recording()
transcriber.transcription.connect(on_transcription)
text = mock_transcription.call_args[0][0]
assert "Bienvenue dans Passe" in text
thread.start()
qtbot.waitUntil(lambda: len(transcriptions) == 3, timeout=60_000)
# any string in any transcription
strings_to_check = [_("Starting Whisper.cpp..."), "Bienvenue dans Passe"]
assert any(s in t for s in strings_to_check for t in transcriptions)
# Wait for the thread to finish
transcriber.stop_recording()
time.sleep(10)
thread.quit()
thread.wait()
time.sleep(3)

View file

@ -1,5 +1,8 @@
import os
import time
from typing import List
from unittest.mock import Mock
from pathlib import Path
import pytest
from pytestqt.qtbot import QtBot
@ -31,8 +34,9 @@ class TestWhisperCppFileTranscriber:
def test_transcribe(
self, qtbot: QtBot, word_level_timings: bool, expected_segments: List[Segment]
):
os.environ["BUZZ_FORCE_CPU"] = "true"
file_transcription_options = FileTranscriptionOptions(
file_paths=[test_audio_path]
file_paths=[str(Path(test_audio_path).resolve())]
)
transcription_options = TranscriptionOptions(
language="fr",
@ -47,7 +51,7 @@ class TestWhisperCppFileTranscriber:
model_path = get_model_path(transcription_options.model)
transcriber = WhisperCppFileTranscriber(
task=FileTranscriptionTask(
file_path=test_audio_path,
file_path=str(Path(test_audio_path).resolve()),
transcription_options=transcription_options,
file_transcription_options=file_transcription_options,
model_path=model_path,
@ -76,6 +80,9 @@ class TestWhisperCppFileTranscriber:
assert expected_segment.end == segments[i].end
assert expected_segment.text in segments[i].text
transcriber.stop()
time.sleep(3)
@pytest.mark.parametrize(
"word_level_timings,expected_segments",
[
@ -91,8 +98,9 @@ class TestWhisperCppFileTranscriber:
def test_transcribe_latvian(
self, qtbot: QtBot, word_level_timings: bool, expected_segments: List[Segment]
):
os.environ["BUZZ_FORCE_CPU"] = "true"
file_transcription_options = FileTranscriptionOptions(
file_paths=[test_multibyte_utf8_audio_path]
file_paths=[str(Path(test_multibyte_utf8_audio_path).resolve())]
)
transcription_options = TranscriptionOptions(
language="lv",
@ -107,7 +115,7 @@ class TestWhisperCppFileTranscriber:
model_path = get_model_path(transcription_options.model)
transcriber = WhisperCppFileTranscriber(
task=FileTranscriptionTask(
file_path=test_multibyte_utf8_audio_path,
file_path=str(Path(test_multibyte_utf8_audio_path).resolve()),
transcription_options=transcription_options,
file_transcription_options=file_transcription_options,
model_path=model_path,
@ -135,3 +143,6 @@ class TestWhisperCppFileTranscriber:
assert expected_segment.start == segments[i].start
assert expected_segment.end == segments[i].end
assert expected_segment.text in segments[i].text
transcriber.stop()
time.sleep(3)

View file

@ -230,6 +230,9 @@ class TestWhisperFileTranscriber:
assert len(segments[i].text) > 0
logging.debug(f"{segments[i].start} {segments[i].end} {segments[i].text}")
transcriber.stop()
time.sleep(3)
def test_transcribe_from_url(self, qtbot):
url = (
"https://github.com/chidiwilliams/buzz/raw/main/testdata/whisper-french.mp3"
@ -270,6 +273,9 @@ class TestWhisperFileTranscriber:
assert len(segments[i].text) > 0
logging.debug(f"{segments[i].start} {segments[i].end} {segments[i].text}")
transcriber.stop()
time.sleep(3)
def test_transcribe_from_folder_watch_source(self, qtbot):
file_path = tempfile.mktemp(suffix=".mp3")
shutil.copy(test_audio_path, file_path)
@ -301,6 +307,9 @@ class TestWhisperFileTranscriber:
)
assert len(glob.glob("*.txt", root_dir=output_directory)) > 0
transcriber.stop()
time.sleep(3)
@pytest.mark.skip()
def test_transcribe_stop(self):
output_file_path = os.path.join(tempfile.gettempdir(), "whisper.txt")
@ -335,3 +344,5 @@ class TestWhisperFileTranscriber:
# Assert that file was not created
assert os.path.isfile(output_file_path) is False
time.sleep(3)

View file

@ -1,3 +1,4 @@
import logging
import os
from typing import List
from unittest.mock import patch
@ -95,6 +96,7 @@ class TestMainWindow:
window.close()
@pytest.mark.timeout(300)
def test_should_run_and_cancel_transcription_task(
self, qtbot, db, transcription_service
):
@ -105,19 +107,44 @@ class TestMainWindow:
table_widget = self._get_tasks_table(window)
qtbot.wait_until(
self._get_assert_task_status_callback(table_widget, 0, "in_progress"),
timeout=2 * 60 * 1000,
)
try:
qtbot.wait_until(
self._get_assert_task_status_callback(table_widget, 0, "in_progress"),
timeout=60 * 1000,
)
except Exception:
logging.error("Task never reached 'in_progress' status")
assert False, "Task did not start as expected"
logging.debug("Will cancel transcription task")
# Stop task in progress
table_widget.selectRow(0)
# Force immediate processing of pending events before triggering cancellation
qtbot.wait(100)
window.toolbar.stop_transcription_action.trigger()
# Give some time for the cancellation to be processed
qtbot.wait(500)
qtbot.wait_until(
self._get_assert_task_status_callback(table_widget, 0, "canceled"),
timeout=60 * 1000,
)
logging.debug("Will wait for task to reach 'canceled' status")
try:
qtbot.wait_until(
self._get_assert_task_status_callback(table_widget, 0, "canceled"),
timeout=30 * 1000,
)
except Exception:
# On Windows, the cancellation might be slower, check final state
final_status = self._get_status(table_widget, 0)
logging.error(f"Task status after timeout: {final_status}")
if "canceled" not in final_status.lower():
assert False, f"Task did not cancel as expected. Final status: {final_status}"
logging.debug("Task canceled")
qtbot.wait(200)
table_widget.selectRow(0)
assert window.toolbar.stop_transcription_action.isEnabled() is False

View file

@ -4,7 +4,7 @@ from PyQt6.QtWidgets import QPushButton, QMessageBox, QLineEdit, QCheckBox
from buzz.locale import _
from buzz.settings.settings import Settings
from buzz.widgets.preferences_dialog.general_preferences_widget import (
GeneralPreferencesWidget, TestOpenAIApiKeyJob
GeneralPreferencesWidget, ValidateOpenAIApiKeyJob
)
@ -118,7 +118,7 @@ class TestTestOpenAIApiKeyJob:
mocker.patch('buzz.widgets.preferences_dialog.general_preferences_widget.OpenAI', return_value=mock_client)
mocker.patch('buzz.settings.settings.Settings.value', return_value="") # No custom base URL
job = TestOpenAIApiKeyJob(api_key="test_key")
job = ValidateOpenAIApiKeyJob(api_key="test_key")
mock_success = mocker.Mock()
mock_failed = mocker.Mock()
job.signals.success.connect(mock_success)
@ -140,7 +140,7 @@ class TestTestOpenAIApiKeyJob:
mocker.patch('buzz.widgets.preferences_dialog.general_preferences_widget.OpenAI', return_value=mock_client)
mocker.patch('buzz.settings.settings.Settings.value', return_value="") # No custom base URL
job = TestOpenAIApiKeyJob(api_key="wrong_key")
job = ValidateOpenAIApiKeyJob(api_key="wrong_key")
mock_success = mocker.Mock()
mock_failed = mocker.Mock()
job.signals.success.connect(mock_success)

View file

@ -111,6 +111,7 @@ class TestRecordingTranscriberWidget:
widget.close()
@pytest.mark.timeout(60)
def test_on_next_transcription_append_above(self, qtbot: QtBot):
with (patch("sounddevice.InputStream", side_effect=MockInputStream),
patch("buzz.transcriber.recording_transcriber.RecordingTranscriber.get_device_sample_rate",
@ -129,7 +130,7 @@ class TestRecordingTranscriberWidget:
widget.on_next_transcription('test2')
assert widget.transcription_text_box.toPlainText() == 'test2\n\ntest1\n\n'
qtbot.wait(200)
qtbot.wait(500)
widget.close()
@ -161,6 +162,7 @@ class TestRecordingTranscriberWidget:
assert RecordingTranscriberWidget.find_common_part("hello world", "") == ""
assert RecordingTranscriberWidget.find_common_part("", "") == ""
@pytest.mark.timeout(60)
def test_on_next_transcription_append_and_correct(self, qtbot: QtBot):
with (patch("sounddevice.InputStream", side_effect=MockInputStream),
patch("buzz.transcriber.recording_transcriber.RecordingTranscriber.get_device_sample_rate",
@ -182,6 +184,6 @@ class TestRecordingTranscriberWidget:
widget.on_next_transcription('Ceci est la deuxième phrase. Et voici la troisième.')
assert widget.transcription_text_box.toPlainText() == 'Bienvenue dans la transcription en direct de Buzz. Ceci est la deuxième phrase. Et voici la troisième.'
qtbot.wait(200)
qtbot.wait(500)
widget.close()

@ -1 +1 @@
Subproject commit c7b6988678779901d02ceba1a8212d2c9908956e
Subproject commit a8d002cfd879315632a579e73f0148d06959de36