diff --git a/.coveragerc b/.coveragerc
deleted file mode 100644
index c682e6aa..00000000
--- a/.coveragerc
+++ /dev/null
@@ -1,19 +0,0 @@
-[run]
-omit =
- buzz/whisper_cpp/*
- buzz/transcriber/local_whisper_cpp_server_transcriber.py
- *_test.py
- demucs/*
- whisper_diarization/*
- deepmultilingualpunctuation/*
- ctc_forced_aligner/*
-
-[report]
-exclude_also =
- if sys.platform == "win32":
- if platform.system\(\) == "Windows":
- if platform.system\(\) == "Linux":
- if platform.system\(\) == "Darwin":
-
-[html]
-directory = coverage/html
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 19e33ae7..54849bdd 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,402 +1,64 @@
+---
name: CI
-on:
- push:
- branches:
- - main
- tags:
- - "*"
- pull_request:
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
- cancel-in-progress: true
+'on': push
jobs:
- test:
- runs-on: ${{ matrix.os }}
- env:
- BUZZ_DISABLE_TELEMETRY: true
- strategy:
- fail-fast: false
- matrix:
- include:
- - os: macos-15-intel
- - os: macos-latest
- - os: windows-latest
- - os: ubuntu-22.04
- - os: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- with:
- submodules: recursive
-
- # Should be removed with next update to whisper.cpp
- - name: Downgrade Xcode
- uses: maxim-lobanov/setup-xcode@v1
- with:
- xcode-version: '16.0.0'
- if: matrix.os == 'macos-latest'
-
- - name: Set up Python
- uses: actions/setup-python@v5
- with:
- python-version: "3.12"
-
- - name: Install Vulkan SDK
- if: "startsWith(matrix.os, 'ubuntu-') || matrix.os == 'windows-latest'"
- uses: humbletim/install-vulkan-sdk@v1.2
- with:
- version: 1.4.309.0
- cache: true
-
- - name: Install uv
- uses: astral-sh/setup-uv@v6
-
- - name: Load cached venv
- id: cached-uv-dependencies
- uses: actions/cache@v4
- with:
- path: .venv
- key: venv-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/uv.lock') }}
-
- - uses: AnimMouse/setup-ffmpeg@v1
- id: setup-ffmpeg
- with:
- version: ${{ matrix.os == 'macos-15-intel' && '7.1.1' || matrix.os == 'macos-latest' && '80' || '8.0' }}
-
- - name: Test ffmpeg
- run: ffmpeg -i ./testdata/audio-long.mp3 ./testdata/audio-long.wav
-
- - name: Add msbuild to PATH
- uses: microsoft/setup-msbuild@v2
- if: runner.os == 'Windows'
-
- - name: Install apt dependencies
- run: |
- sudo apt-get update
-
- if [ "$(lsb_release -rs)" == "22.04" ]; then
- sudo apt-get install libegl1-mesa
-
- # Add ubuntu-toolchain-r PPA for newer libstdc++6 with GLIBCXX_3.4.32
- sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
- sudo apt-get update
- sudo apt-get install -y libstdc++6
- fi
-
- 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 libvulkan-dev ccache
- if: "startsWith(matrix.os, 'ubuntu-')"
-
- - name: Install dependencies
- run: uv sync
-
- - name: Test
- run: |
- uv run make test
- shell: bash
- env:
- PYTHONFAULTHANDLER: "1"
-
- - name: Upload coverage reports to Codecov with GitHub Action
- uses: codecov/codecov-action@v4
- with:
- flags: ${{ runner.os }}
- token: ${{ secrets.CODECOV_TOKEN }}
- env:
- CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
-
build:
runs-on: ${{ matrix.os }}
- timeout-minutes: 90
- env:
- BUZZ_DISABLE_TELEMETRY: true
strategy:
- fail-fast: false
matrix:
include:
- - os: macos-15-intel
- os: macos-latest
+ CMD_BUILD: |
+ brew install create-dmg
+ poetry run make bundle_mac version=$GITHUB_REF_NAME
+ PATH: |
+ dist/buzz*.dmg
+ dist/buzz*.tar.gz
- os: windows-latest
+ CMD_BUILD: |
+ poetry run make bundle_windows version=$env:GITHUB_REF_NAME
+ PATH: |
+ dist/buzz*.tar.gz
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v3
+ - uses: actions/setup-python@v4
with:
- submodules: recursive
+ python-version: '3.10.6'
- # Should be removed with next update to whisper.cpp
- - name: Downgrade Xcode
- uses: maxim-lobanov/setup-xcode@v1
+ - name: Install Poetry Action
+ uses: snok/install-poetry@v1.3.1
with:
- xcode-version: '16.0.0'
- if: matrix.os == 'macos-latest'
-
- - name: Set up Python
- uses: actions/setup-python@v5
- with:
- python-version: "3.12"
-
- - name: Install Vulkan SDK
- if: "startsWith(matrix.os, 'ubuntu-') || matrix.os == 'windows-latest'"
- uses: humbletim/install-vulkan-sdk@v1.2
- with:
- version: 1.4.309.0
- cache: true
-
- - name: Install uv
- uses: astral-sh/setup-uv@v6
+ virtualenvs-create: true
+ virtualenvs-in-project: true
- name: Load cached venv
- id: cached-uv-dependencies
- uses: actions/cache@v4
+ id: cached-poetry-dependencies
+ uses: actions/cache@v3
with:
path: .venv
- key: venv-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/uv.lock') }}
+ key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }}-2
- - name: Install Inno Setup on Windows
- uses: crazy-max/ghaction-chocolatey@v3
+ - run: poetry install
+ - name: Test
+ run: poetry run pytest --cov --cov-fail-under=73
+ - run: ${{ matrix.CMD_BUILD }}
+ - uses: actions/upload-artifact@v3
with:
- args: install innosetup --yes
- if: runner.os == 'Windows'
-
- - name: Install apt dependencies
- run: |
- sudo apt-get update
-
- if [ "$(lsb_release -rs)" == "22.04" ]; then
- sudo apt-get install libegl1-mesa
-
- # Add ubuntu-toolchain-r PPA for newer libstdc++6 with GLIBCXX_3.4.32
- sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
- sudo apt-get update
- sudo apt-get install -y libstdc++6
- fi
-
- 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 libvulkan-dev ccache
- if: "startsWith(matrix.os, 'ubuntu-')"
-
- - name: Install dependencies
- run: uv sync
-
- - uses: AnimMouse/setup-ffmpeg@v1
- id: setup-ffmpeg
- with:
- version: ${{ matrix.os == 'macos-15-intel' && '7.1.1' || matrix.os == 'macos-latest' && '80' || '8.0' }}
-
- - name: Install MSVC for Windows
- run: |
- if [ "$RUNNER_OS" == "Windows" ]; then
- uv add msvc-runtime
- uv pip install -U torch==2.8.0+cu129 torchaudio==2.8.0+cu129 --index-url https://download.pytorch.org/whl/cu129
- uv pip install nvidia-cublas-cu12==12.9.1.4 nvidia-cuda-cupti-cu12==12.9.79 nvidia-cuda-runtime-cu12==12.9.79 --extra-index-url https://pypi.ngc.nvidia.com
-
- uv cache clean
- uv run pip cache purge
- fi
- shell: bash
-
- - name: Add msbuild to PATH
- uses: microsoft/setup-msbuild@v2
- if: runner.os == 'Windows'
-
- - uses: ruby/setup-ruby@v1
- with:
- ruby-version: "3.0"
- bundler-cache: true
- if: "startsWith(matrix.os, 'ubuntu-')"
-
- - name: Install FPM
- run: gem install fpm
- if: "startsWith(matrix.os, 'ubuntu-')"
-
- - name: Clear space on Windows
- if: runner.os == 'Windows'
- run: |
- rm 'C:\Android\android-sdk\' -r -force
- rm 'C:\Program Files (x86)\Google\' -r -force
- rm 'C:\tools\kotlinc\' -r -force
- rm 'C:\tools\php\' -r -force
- rm 'C:\selenium\' -r -force
- shell: pwsh
-
- - name: Bundle
- run: |
- if [ "$RUNNER_OS" == "macOS" ]; then
-
- brew install create-dmg
-
- sudo pkill -9 XProtect >/dev/null || true;
- while pgrep XProtect; do sleep 3; done;
-
- CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
- KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
-
- echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
-
- security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
- security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
- security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
-
- security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
- security list-keychain -d user -s $KEYCHAIN_PATH
-
- xcrun notarytool store-credentials --apple-id "$APPLE_ID" --password "$APPLE_APP_PASSWORD" --team-id "$APPLE_TEAM_ID" notarytool --validate
-
- uv run make bundle_mac
-
- elif [ "$RUNNER_OS" == "Windows" ]; then
-
- cp -r ./dll_backup ./buzz/
- uv run make bundle_windows
-
- fi
- env:
- BUZZ_CODESIGN_IDENTITY: ${{ secrets.BUZZ_CODESIGN_IDENTITY }}
- BUZZ_KEYCHAIN_NOTARY_PROFILE: ${{ secrets.BUZZ_KEYCHAIN_NOTARY_PROFILE }}
- BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
- KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
- P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
- APPLE_ID: ${{ secrets.APPLE_ID }}
- APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
- APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
- shell: bash
-
- - uses: actions/upload-artifact@v4
- with:
- name: Buzz-${{ runner.os }}-${{ runner.arch }}
+ name: Buzz
path: |
- dist/Buzz*-windows.exe
- dist/Buzz*-windows-*.bin
- dist/Buzz*-mac.dmg
-
- build_wheels:
- runs-on: ${{ matrix.os }}
- env:
- BUZZ_DISABLE_TELEMETRY: true
- strategy:
- fail-fast: false
- matrix:
- os: [ubuntu-latest, windows-latest, macos-15-intel, macos-latest]
-
- steps:
- - uses: actions/checkout@v4
- with:
- submodules: recursive
-
- # Should be removed with next update to whisper.cpp
- - name: Downgrade Xcode
- uses: maxim-lobanov/setup-xcode@v1
- with:
- xcode-version: '16.0.0'
- if: matrix.os == 'macos-latest'
-
- - name: Install Vulkan SDK
- if: "startsWith(matrix.os, 'ubuntu-') || matrix.os == 'windows-latest'"
- uses: humbletim/install-vulkan-sdk@v1.2
- with:
- version: 1.4.309.0
- cache: true
-
- - name: Install uv
- uses: astral-sh/setup-uv@v6
-
- - name: Build wheels
- run: uv build --wheel
- shell: bash
-
- - uses: actions/upload-artifact@v4
- with:
- name: buzz-wheel-${{ runner.os }}-${{ runner.arch }}
- path: ./dist/*.whl
-
- publish_pypi:
- needs: [build_wheels, test]
- runs-on: ubuntu-latest
- env:
- BUZZ_DISABLE_TELEMETRY: true
- environment: pypi
- permissions:
- id-token: write
- if: startsWith(github.ref, 'refs/tags/')
- steps:
- - uses: actions/download-artifact@v4
- with:
- pattern: buzz-wheel-*
- path: dist
- merge-multiple: true
-
- - uses: pypa/gh-action-pypi-publish@release/v1
- with:
- verbose: true
- password: ${{ secrets.PYPI_TOKEN }}
-
+ ${{ matrix.PATH }}
release:
- runs-on: ${{ matrix.os }}
- env:
- BUZZ_DISABLE_TELEMETRY: true
- strategy:
- fail-fast: false
- matrix:
- include:
- - os: macos-15-intel
- - os: macos-latest
- - os: windows-latest
- needs: [build, test]
+ runs-on: ubuntu-latest
+ needs: build
if: startsWith(github.ref, 'refs/tags/')
steps:
- - uses: actions/checkout@v4
+ - uses: actions/download-artifact@v3
with:
- submodules: recursive
-
- - uses: actions/download-artifact@v4
- with:
- name: Buzz-${{ runner.os }}-${{ runner.arch }}
-
- - name: Rename .dmg files
- if: runner.os == 'macOS'
- run: |
- for file in Buzz*.dmg; do
- mv "$file" "${file%.dmg}-${{ runner.arch }}.dmg"
- done
-
+ name: Buzz
- name: Release
- uses: softprops/action-gh-release@v2
+ uses: softprops/action-gh-release@v1
with:
files: |
- Buzz*-unix.tar.gz
- Buzz*.exe
- Buzz*.bin
- Buzz*.dmg
-
-# Brew Cask deployment fails and the app is deprecated on Brew.
-# deploy_brew_cask:
-# runs-on: macos-latest
-# env:
-# BUZZ_DISABLE_TELEMETRY: true
-# needs: [release]
-# if: startsWith(github.ref, 'refs/tags/')
-# steps:
-# - uses: actions/checkout@v4
-# with:
-# submodules: recursive
-#
-# # Should be removed with next update to whisper.cpp
-# - name: Downgrade Xcode
-# uses: maxim-lobanov/setup-xcode@v1
-# with:
-# xcode-version: '16.0.0'
-# if: matrix.os == 'macos-latest'
-#
-# - name: Install uv
-# uses: astral-sh/setup-uv@v6
-#
-# - name: Set up Python
-# uses: actions/setup-python@v5
-# with:
-# python-version: "3.12"
-#
-# - name: Install dependencies
-# run: uv sync
-#
-# - name: Upload to Brew
-# run: uv run make upload_brew
-# env:
-# HOMEBREW_GITHUB_API_TOKEN: ${{ secrets.HOMEBREW_GITHUB_API_TOKEN }}
+ buzz*.tar.gz
+ buzz*.dmg
diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml
deleted file mode 100644
index d919633b..00000000
--- a/.github/workflows/gh-pages.yml
+++ /dev/null
@@ -1,32 +0,0 @@
----
-name: GitHub Pages
-on:
- push:
- branches:
- - main
-
-jobs:
- deploy:
- name: Deploy
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-node@v4
- with:
- node-version: 18
- cache: npm
- cache-dependency-path: docs/package-lock.json
-
- - name: Install dependencies
- run: npm ci
- working-directory: docs
-
- - name: Build
- run: npm run build
- working-directory: docs
-
- - name: Deploy to GitHub Pages
- uses: peaceiris/actions-gh-pages@v4
- with:
- github_token: ${{ secrets.GITHUB_TOKEN }}
- publish_dir: ./docs/build
diff --git a/.github/workflows/manual-build.yml b/.github/workflows/manual-build.yml
deleted file mode 100644
index ecbae668..00000000
--- a/.github/workflows/manual-build.yml
+++ /dev/null
@@ -1,94 +0,0 @@
----
-name: Manual Build
-on: workflow_dispatch
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
- cancel-in-progress: true
-
-jobs:
- build:
- runs-on: ${{ matrix.os }}
- env:
- BUZZ_DISABLE_TELEMETRY: true
- strategy:
- fail-fast: false
- matrix:
- include:
- - os: macos-latest
- - os: windows-latest
- steps:
- - uses: actions/checkout@v4
- with:
- submodules: recursive
- - uses: actions/setup-python@v5
- with:
- python-version: "3.11.9"
-
- - name: Install Poetry Action
- uses: snok/install-poetry@v1.3.1
- with:
- virtualenvs-create: true
- virtualenvs-in-project: true
-
- - name: Load cached venv
- id: cached-poetry-dependencies
- uses: actions/cache@v4
- with:
- path: .venv
- key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }}-2
-
- - uses: FedericoCarboni/setup-ffmpeg@v3.1
- id: setup-ffmpeg
- with:
- ffmpeg-version: release
- architecture: 'x64'
- github-token: ${{ github.server_url == 'https://github.com' && github.token || '' }}
-
- - name: Install dependencies
- run: poetry install
-
- - name: Bundle
- run: |
- if [ "$RUNNER_OS" == "macOS" ]; then
-
- brew install create-dmg
- poetry run make bundle_mac_unsigned
-
- elif [ "$RUNNER_OS" == "Windows" ]; then
-
- poetry run make bundle_windows
-
- fi
- shell: bash
-
- - uses: actions/upload-artifact@v4
- with:
- name: Buzz-${{ runner.os }}
- path: |
- dist/Buzz*-windows.exe
- dist/Buzz*-mac.dmg
-
- build-snap:
- runs-on: ubuntu-latest
- env:
- BUZZ_DISABLE_TELEMETRY: true
- outputs:
- snap: ${{ steps.snapcraft.outputs.snap }}
- steps:
- - uses: actions/checkout@v4
- with:
- submodules: recursive
- - uses: snapcore/action-build@v1
- id: snapcraft
- - run: |
- sudo apt-get update
- sudo apt-get install libportaudio2
- - run: sudo snap install --devmode *.snap
- - run: |
- cd $HOME
- xvfb-run buzz --version
- - uses: actions/upload-artifact@v4
- with:
- name: snap
- path: ${{ steps.snapcraft.outputs.snap }}
\ No newline at end of file
diff --git a/.github/workflows/snapcraft.yml b/.github/workflows/snapcraft.yml
deleted file mode 100644
index d4f95dc7..00000000
--- a/.github/workflows/snapcraft.yml
+++ /dev/null
@@ -1,106 +0,0 @@
----
-name: Snapcraft
-on:
- push:
- branches:
- - main
- tags:
- - "*"
- pull_request:
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
- cancel-in-progress: true
-
-jobs:
- build:
- runs-on: ubuntu-24.04
- timeout-minutes: 90
- env:
- BUZZ_DISABLE_TELEMETRY: true
- outputs:
- snap: ${{ steps.snapcraft.outputs.snap }}
- steps:
- # Ideas from https://github.com/orgs/community/discussions/25678
- - name: Remove unused build tools
- run: |
- sudo apt-get remove -y azure-cli google-cloud-sdk hhvm google-chrome-stable firefox powershell mono-devel || true
- sudo apt-get autoremove -y
- sudo apt-get clean
- python -m pip cache purge
- rm -rf /opt/hostedtoolcache || true
- - name: Check available disk space
- run: |
- echo "=== Disk space ==="
- df -h
- echo "=== Memory ==="
- free -h
- - uses: actions/checkout@v4
- with:
- submodules: recursive
- - name: Install Snapcraft and dependencies
- run: |
- set -x
- # Ensure snapd is ready
- sudo systemctl start snapd.socket
- sudo snap wait system seed.loaded
-
- echo "=== Installing snapcraft ==="
- sudo snap install --classic snapcraft
-
- echo "=== Installing gnome extension dependencies ==="
- sudo snap install gnome-46-2404 || { echo "Failed to install gnome-46-2404"; sudo journalctl -u snapd --no-pager -n 50; exit 1; }
- sudo snap install gnome-46-2404-sdk || { echo "Failed to install gnome-46-2404-sdk"; sudo journalctl -u snapd --no-pager -n 50; exit 1; }
-
- echo "=== Installing build-snaps ==="
- sudo snap install --classic astral-uv || { echo "Failed to install astral-uv"; sudo journalctl -u snapd --no-pager -n 50; exit 1; }
-
- echo "=== Installed snaps ==="
- snap list
- - name: Check disk space before build
- run: df -h
- - name: Build snap
- id: snapcraft
- env:
- SNAPCRAFT_BUILD_ENVIRONMENT: host
- run: |
- sudo -E snapcraft pack --verbose --destructive-mode
- echo "snap=$(ls *.snap)" >> $GITHUB_OUTPUT
- - run: sudo snap install --devmode *.snap
- - run: |
- cd $HOME
- xvfb-run buzz --version
- - uses: actions/upload-artifact@v4
- with:
- name: snap
- path: ${{ steps.snapcraft.outputs.snap }}
-
- upload-edge:
- runs-on: ubuntu-latest
- needs: [ build ]
- if: github.ref == 'refs/heads/main'
- steps:
- - uses: actions/download-artifact@v4
- with:
- name: snap
- - uses: snapcore/action-publish@v1
- env:
- SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }}
- with:
- snap: ${{ needs.build.outputs.snap }}
- release: edge
-
- upload-stable:
- runs-on: ubuntu-latest
- needs: [ build ]
- if: startsWith(github.ref, 'refs/tags/')
- steps:
- - uses: actions/download-artifact@v4
- with:
- name: snap
- - uses: snapcore/action-publish@v1
- env:
- SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }}
- with:
- snap: ${{ needs.build.outputs.snap }}
- release: stable
diff --git a/.gitignore b/.gitignore
index 291ecb53..23af258f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,35 +2,4 @@ dist/
__pycache__/
build/
.pytest_cache/
-.coverage*
-!.coveragerc
-.env
-.DS_Store
-htmlcov/
-coverage.xml
-.idea/
-.venv/
-venv/
-.claude/
-
-# whisper_cpp
-whisper_cpp
-*.exe
-*.dll
-*.dylib
-*.so
-buzz/whisper_cpp/*
-
-# Internationalization - compiled binaries
-*.mo
-*.po~
-
-benchmarks.json
-
-.eggs
-*.egg-info
-/coverage/
-/wheelhouse/
-/.flatpak-builder
-/repo
-/nemo_msdd_configs
+.coverage
diff --git a/.gitmodules b/.gitmodules
deleted file mode 100644
index 5ce5bc73..00000000
--- a/.gitmodules
+++ /dev/null
@@ -1,15 +0,0 @@
-[submodule "whisper.cpp"]
- path = whisper.cpp
- url = https://github.com/ggerganov/whisper.cpp
-[submodule "whisper_diarization"]
- path = whisper_diarization
- url = https://github.com/MahmoudAshraf97/whisper-diarization
-[submodule "demucs_repo"]
- path = demucs_repo
- url = https://github.com/MahmoudAshraf97/demucs.git
-[submodule "deepmultilingualpunctuation"]
- path = deepmultilingualpunctuation
- url = https://github.com/oliverguhr/deepmultilingualpunctuation.git
-[submodule "ctc_forced_aligner"]
- path = ctc_forced_aligner
- url = https://github.com/MahmoudAshraf97/ctc-forced-aligner.git
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
deleted file mode 100644
index caf645bd..00000000
--- a/.pre-commit-config.yaml
+++ /dev/null
@@ -1,16 +0,0 @@
-repos:
- - repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.5.0
- hooks:
- - id: trailing-whitespace
- - id: end-of-file-fixer
- - id: check-yaml
- - id: check-added-large-files
-
- - repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.1.3
- hooks:
- - id: ruff
- args: [ --fix, --exit-non-zero-on-fix ]
-
- - id: ruff-format
diff --git a/.pylintrc b/.pylintrc
deleted file mode 100644
index 4b6768a5..00000000
--- a/.pylintrc
+++ /dev/null
@@ -1,4 +0,0 @@
-[MASTER]
-disable=
- C0114, # missing-module-docstring
- C0116, # missing-function-docstring
diff --git a/.python-version b/.python-version
deleted file mode 100644
index e4fba218..00000000
--- a/.python-version
+++ /dev/null
@@ -1 +0,0 @@
-3.12
diff --git a/.run/pytest.run.xml b/.run/pytest.run.xml
deleted file mode 100644
index 0876f136..00000000
--- a/.run/pytest.run.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 6a4d2a28..535b6b00 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -9,7 +9,7 @@
"type": "python",
"request": "launch",
"module": "main",
- "justMyCode": false
+ "justMyCode": true
}
]
-}
+}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
index b7492058..2cf13ac6 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,8 +1,6 @@
{
"files.associations": {
- ".coveragerc": "ini",
- "Buzz.spec": "python",
- "iosfwd": "cpp"
+ "Buzz.spec": "python"
},
"files.exclude": {
"**/.git": true,
@@ -11,11 +9,5 @@
"**/CVS": true,
"**/.DS_Store": true,
"**/Thumbs.db": true
- },
- "python.testing.pytestArgs": ["."],
- "python.testing.unittestEnabled": false,
- "python.testing.pytestEnabled": true,
- "python.linting.pylintEnabled": true,
- "python.linting.enabled": true,
- "cmake.sourceDirectory": "${workspaceFolder}/whisper.cpp"
+ }
}
diff --git a/Buzz.spec b/Buzz.spec
index f590f0f5..50fe4936 100644
--- a/Buzz.spec
+++ b/Buzz.spec
@@ -1,126 +1,29 @@
# -*- mode: python ; coding: utf-8 -*-
-import os
-import os.path
-import platform
-import shutil
-
from PyInstaller.utils.hooks import collect_data_files, copy_metadata
-from buzz.__version__ import VERSION
-
datas = []
-datas += collect_data_files("torch")
-datas += collect_data_files("demucs")
-datas += copy_metadata("tqdm")
-datas += copy_metadata("torch")
-datas += copy_metadata("regex")
-datas += copy_metadata("requests")
-datas += copy_metadata("packaging")
-datas += copy_metadata("filelock")
-datas += copy_metadata("numpy")
-datas += copy_metadata("tokenizers")
-datas += copy_metadata("huggingface-hub")
-datas += copy_metadata("safetensors")
-datas += copy_metadata("pyyaml")
-datas += copy_metadata("julius")
-datas += copy_metadata("openunmix")
-datas += copy_metadata("lameenc")
-datas += copy_metadata("diffq")
-datas += copy_metadata("einops")
-datas += copy_metadata("hydra-core")
-datas += copy_metadata("hydra-colorlog")
-datas += copy_metadata("museval")
-datas += copy_metadata("submitit")
-datas += copy_metadata("treetable")
-datas += copy_metadata("soundfile")
-datas += copy_metadata("dora-search")
-datas += copy_metadata("lhotse")
+datas += collect_data_files('torch')
+datas += copy_metadata('tqdm')
+datas += copy_metadata('torch')
+datas += copy_metadata('regex')
+datas += copy_metadata('requests')
+datas += copy_metadata('packaging')
+datas += copy_metadata('filelock')
+datas += copy_metadata('numpy')
+datas += copy_metadata('tokenizers')
+datas += collect_data_files('whisper')
-# Allow transformers package to load __init__.py file dynamically:
-# https://github.com/chidiwilliams/buzz/issues/272
-datas += collect_data_files("transformers", include_py_files=True)
-
-datas += collect_data_files("faster_whisper", include_py_files=True)
-datas += collect_data_files("stable_whisper", include_py_files=True)
-datas += collect_data_files("whisper")
-datas += collect_data_files("demucs", include_py_files=True)
-datas += collect_data_files("whisper_diarization", include_py_files=True)
-datas += collect_data_files("deepmultilingualpunctuation", include_py_files=True)
-datas += collect_data_files("ctc_forced_aligner", include_py_files=True, excludes=["build"])
-datas += collect_data_files("nemo", include_py_files=True)
-datas += collect_data_files("lightning_fabric", include_py_files=True)
-datas += collect_data_files("pytorch_lightning", include_py_files=True)
-datas += [("buzz/assets/*", "assets")]
-datas += [("buzz/locale", "locale")]
-datas += [("buzz/schema.sql", ".")]
block_cipher = None
-DEBUG = os.environ.get("PYINSTALLER_DEBUG", "").lower() in ["1", "true"]
-if DEBUG:
- options = [("v", None, "OPTION")]
-else:
- options = []
-
-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)
-
- # 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"), "."),
- ]
-
-binaries.append(("buzz/whisper_cpp/*", "buzz/whisper_cpp"))
-
-if platform.system() == "Windows":
- datas += [("dll_backup", "dll_backup")]
- datas += collect_data_files("msvc-runtime")
-
- binaries.append(("dll_backup/SDL2.dll", "dll_backup"))
a = Analysis(
- ["main.py"],
+ ['main.py'],
pathex=[],
- binaries=binaries,
+ binaries=[],
datas=datas,
- hiddenimports=[
- "dora", "dora.log",
- "julius", "julius.core", "julius.resample",
- "openunmix", "openunmix.filtering",
- "lameenc",
- "diffq",
- "einops",
- "hydra", "hydra.core", "hydra.core.global_hydra",
- "hydra_colorlog",
- "museval",
- "submitit",
- "treetable",
- "soundfile",
- "_soundfile_data",
- "lhotse",
- ],
+ hiddenimports=['apiclient', 'sounddevice', 'pytorch', '“sklearn.utils._cython_blas”', '“sklearn.neighbors.typedefs”',
+ '“sklearn.neighbors.quad_tree”', '“sklearn.tree”', '“sklearn.tree._utils”'],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
@@ -135,20 +38,19 @@ pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
- options,
- icon="./assets/buzz.ico",
+ [],
exclude_binaries=True,
- name="Buzz",
- debug=DEBUG,
+ name='Buzz',
+ debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
- console=DEBUG,
+ console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
- codesign_identity=os.environ.get("BUZZ_CODESIGN_IDENTITY"),
- entitlements_file="entitlements.plist" if platform.system() == "Darwin" else None,
+ codesign_identity=None,
+ entitlements_file=None,
)
coll = COLLECT(
exe,
@@ -156,19 +58,17 @@ coll = COLLECT(
a.zipfiles,
a.datas,
strip=False,
- upx=False,
+ upx=True,
upx_exclude=[],
- name="Buzz",
+ name='Buzz',
)
app = BUNDLE(
coll,
- name="Buzz.app",
- icon="./assets/buzz.icns",
- bundle_identifier="com.chidiwilliams.buzz",
- version=VERSION,
+ name='Buzz.app',
+ icon=None,
+ bundle_identifier=None,
+ version='0.0.1',
info_plist={
- "NSPrincipalClass": "NSApplication",
- "NSHighResolutionCapable": "True",
- "NSMicrophoneUsageDescription": "Allow Buzz to record audio from your microphone.",
- },
+ 'NSMicrophoneUsageDescription': 'Please provide microphone access to continue'
+ }
)
diff --git a/CLAUDE.md b/CLAUDE.md
deleted file mode 100644
index 1d471238..00000000
--- a/CLAUDE.md
+++ /dev/null
@@ -1 +0,0 @@
-- Use uv to run tests and any scripts
\ No newline at end of file
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
deleted file mode 100644
index 75e4ef2d..00000000
--- a/CODE_OF_CONDUCT.md
+++ /dev/null
@@ -1,134 +0,0 @@
-# Contributor Covenant Code of Conduct
-
-## Our Pledge
-
-We as members, contributors, and leaders pledge to make participation in our
-community a harassment-free experience for everyone, regardless of age, body
-size, visible or invisible disability, ethnicity, sex characteristics, gender
-identity and expression, level of experience, education, socio-economic status,
-nationality, personal appearance, race, caste, color, religion, or sexual
-identity and orientation.
-
-We pledge to act and interact in ways that contribute to an open, welcoming,
-diverse, inclusive, and healthy community.
-
-## Our Standards
-
-Examples of behavior that contributes to a positive environment for our
-community include:
-
-* Demonstrating empathy and kindness toward other people
-* Being respectful of differing opinions, viewpoints, and experiences
-* Giving and gracefully accepting constructive feedback
-* Accepting responsibility and apologizing to those affected by our mistakes,
- and learning from the experience
-* Focusing on what is best not just for us as individuals, but for the overall
- community
-
-Examples of unacceptable behavior include:
-
-* The use of sexualized language or imagery, and sexual attention or advances of
- any kind
-* Trolling, insulting or derogatory comments, and personal or political attacks
-* Public or private harassment
-* Publishing others' private information, such as a physical or email address,
- without their explicit permission
-* Other conduct which could reasonably be considered inappropriate in a
- professional setting
-
-## Enforcement Responsibilities
-
-Community leaders are responsible for clarifying and enforcing our standards of
-acceptable behavior and will take appropriate and fair corrective action in
-response to any behavior that they deem inappropriate, threatening, offensive,
-or harmful.
-
-Community leaders have the right and responsibility to remove, edit, or reject
-comments, commits, code, wiki edits, issues, and other contributions that are
-not aligned to this Code of Conduct, and will communicate reasons for moderation
-decisions when appropriate.
-
-## Scope
-
-This Code of Conduct applies within all community spaces, and also applies when
-an individual is officially representing the community in public spaces.
-Examples of representing our community include using an official email address,
-posting via an official social media account, or acting as an appointed
-representative at an online or offline event.
-
-## Enforcement
-
-Instances of abusive, harassing, or otherwise unacceptable behavior may be
-reported to the community leaders responsible for enforcement at
-williamschidi1@gmail.com.
-
-All complaints will be reviewed and investigated promptly and fairly.
-
-All community leaders are obligated to respect the privacy and security of the
-reporter of any incident.
-
-## Enforcement Guidelines
-
-Community leaders will follow these Community Impact Guidelines in determining
-the consequences for any action they deem in violation of this Code of Conduct:
-
-### 1. Correction
-
-**Community Impact**: Use of inappropriate language or other behavior deemed
-unprofessional or unwelcome in the community.
-
-**Consequence**: A private, written warning from community leaders, providing
-clarity around the nature of the violation and an explanation of why the
-behavior was inappropriate. A public apology may be requested.
-
-### 2. Warning
-
-**Community Impact**: A violation through a single incident or series of
-actions.
-
-**Consequence**: A warning with consequences for continued behavior. No
-interaction with the people involved, including unsolicited interaction with
-those enforcing the Code of Conduct, for a specified period of time. This
-includes avoiding interactions in community spaces as well as external channels
-like social media. Violating these terms may lead to a temporary or permanent
-ban.
-
-### 3. Temporary Ban
-
-**Community Impact**: A serious violation of community standards, including
-sustained inappropriate behavior.
-
-**Consequence**: A temporary ban from any sort of interaction or public
-communication with the community for a specified period of time. No public or
-private interaction with the people involved, including unsolicited interaction
-with those enforcing the Code of Conduct, is allowed during this period.
-Violating these terms may lead to a permanent ban.
-
-### 4. Permanent Ban
-
-**Community Impact**: Demonstrating a pattern of violation of community
-standards, including sustained inappropriate behavior, harassment of an
-individual, or aggression toward or disparagement of classes of individuals.
-
-**Consequence**: A permanent ban from any sort of public interaction within the
-community.
-
-## Attribution
-
-This Code of Conduct is adapted from the [Contributor Covenant][homepage],
-version 2.1, available at
-[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
-
-Community Impact Guidelines were inspired by
-[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
-
-For answers to common questions about this code of conduct, see the FAQ at
-[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
-[https://www.contributor-covenant.org/translations][translations].
-
-[homepage]: https://www.contributor-covenant.org
-[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
-[Mozilla CoC]: https://github.com/mozilla/diversity
-[FAQ]: https://www.contributor-covenant.org/faq
-[translations]: https://www.contributor-covenant.org/translations
-
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
deleted file mode 100755
index abb1a73c..00000000
--- a/CONTRIBUTING.md
+++ /dev/null
@@ -1,123 +0,0 @@
-# Buzz Contribution Guide
-
-## Internationalization
-
-To contribute a new language translation to Buzz:
-
-1. Run `make translation_po locale=[locale]`. `[locale]` is a string with the format "language\[_script\]\[_country\]",
- where:
-
- - "language" is a lowercase, two-letter ISO 639 language code,
- - "script" is a titlecase, four-letter, ISO 15924 script code, and
- - "country" is an uppercase, two-letter, ISO 3166 country code.
-
- For example: `make translation_po locale=en_US`.
-
-2. Fill in the translations in the `.po` file generated in `locale/[locale]/LC_MESSAGES`.
-3. Run `make translation_mo` to compile the translations, then test your changes.
-4. Create a new pull request with your changes.
-
-## Troubleshooting
-
-If you encounter any issues, please open an issue on the Buzz GitHub repository. Here are a few tips to gather data about the issue, so it is easier for us to fix.
-
-**Provide details**
-
-What version of the Buzz are you using? On what OS? What are steps to reproduce it? What settings were selected, like what model type and size was used.
-
-**Logs**
-
-Log files contain valuable information about what the Buzz was doing before the issue occurred. You can get the logs like this:
-* Linux run the app from the terminal and check the output.
-* Mac get logs from `~/Library/Logs/Buzz`.
-* Windows paste this into the Windows Explorer address bar `%USERPROFILE%\AppData\Local\Buzz\Buzz\Logs` and check the logs file.
-
-**Test on latest version**
-
-To see if your issue has already been fixed, try running the latest version of the Buzz. To get it log in to the GitHub and go to [Actions section](https://github.com/chidiwilliams/buzz/actions/workflows/ci.yml?query=branch%3Amain). Latest development versions attached to Artifacts section of successful builds.
-
-Linux versions get also pushed to the snap. To install latest development version use `snap install buzz --channel latest/edge`
-
-
-
-## Running Buzz locally
-
-### Linux (Ubuntu)
-
-1. Clone the repository `git clone --recursive https://github.com/chidiwilliams/buzz.git`
-2. Enter repo folder `cd buzz`
-3. Install uv `curl -LsSf https://astral.sh/uv/install.sh | sh` (or see [uv installation docs](https://docs.astral.sh/uv/getting-started/installation/))
-4. Install system dependencies you may be missing
-```
-sudo apt-get install --no-install-recommends libyaml-dev libtbb-dev libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-shape0 libxcb-cursor0 libportaudio2 gettext libpulse0 ffmpeg
-```
-On versions prior to Ubuntu 24.04 install `sudo apt-get install --no-install-recommends libegl1-mesa`
-
-5. Install the dependencies `uv sync`
-6. Run Buzz `uv run buzz`
-
-#### Necessary dependencies for Faster Whisper on GPU
-
- All the dependencies for GPU support should be included in the dependency packages already installed,
- but if you get issues running Faster Whisper on GPU, install [CUDA 12](https://developer.nvidia.com/cuda-downloads), [cuBLASS](https://developer.nvidia.com/cublas) and [cuDNN](https://developer.nvidia.com/cudnn).
-
-#### Error for Faster Whisper on GPU `Could not load library libcudnn_ops_infer.so.8`
-
- You need to add path to the library to the `LD_LIBRARY_PATH` environment variable.
- Check exact path to your uv virtual environment, it may be different for you.
-
-```
- export LD_LIBRARY_PATH=/path/to/buzz/.venv/lib/python3.12/site-packages/nvidia/cudnn/lib/:$LD_LIBRARY_PATH
-```
-
-#### For Whisper.cpp you will need to install Vulkan SDK
-
- Follow the instructions for your distribution https://vulkan.lunarg.com/doc/sdk/latest/linux/getting_started.html
-
-### Mac
-
-1. Clone the repository `git clone --recursive https://github.com/chidiwilliams/buzz.git`
-2. Enter repo folder `cd buzz`
-3. Install uv `curl -LsSf https://astral.sh/uv/install.sh | sh` (or `brew install uv`)
-4. Install system dependencies you may be missing `brew install ffmpeg`
-5. Install the dependencies `uv sync`
-6. Run Buzz `uv run buzz`
-
-
-
-### Windows
-
-Assumes you have [Git](https://git-scm.com/downloads) and [python](https://www.python.org/downloads) installed and added to PATH.
-
-1. Install the chocolatey package manager for Windows. [More info](https://docs.chocolatey.org/en-us/choco/setup)
-```
-Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
-```
-2. Install the build tools. `choco install make cmake`
-3. Install the ffmpeg. `choco install ffmpeg`
-4. Download [Build Tools for Visual Studio 2022](https://visualstudio.microsoft.com/vs/older-downloads/) and install "Desktop development with C++" workload.
-5. Add location of `namke` to your PATH environment variable. Usually it is `C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Tools\MSVC\14.44.35207\bin\Hostx64\x86`
-6. Install Vulkan SDK from https://vulkan.lunarg.com/sdk/home
-7. Clone the repository `git clone --recursive https://github.com/chidiwilliams/buzz.git`
-8. Enter repo folder `cd buzz`
-9. Install uv `powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"`
-10. Install the dependencies `uv sync`
-11. Build Whisper.cpp `uv run make buzz/whisper_cpp`
-12. `cp -r .\dll_backup\ .\buzz\`
-13. Run Buzz `uv run buzz`
-
-Note: It should be safe to ignore any "syntax errors" you see during the build. Buzz will work. Also you can ignore any errors for FFmpeg. Buzz tries to load FFmpeg by several different means and some of them throw errors, but FFmpeg should eventually be found and work.
-
-#### GPU Support
-
-GPU support on Windows with Nvidia GPUs is included out of the box in the `.exe` installer.
-
-To add GPU support for source or `pip` installed version switch torch library to GPU version. For more info see https://pytorch.org/get-started/locally/ .
-```
-uv add --index https://download.pytorch.org/whl/cu128 torch==2.7.1+cu128 torchaudio==2.7.1+cu128
-uv add --index https://pypi.ngc.nvidia.com nvidia-cublas-cu12==12.8.3.14 nvidia-cuda-cupti-cu12==12.8.57 nvidia-cuda-nvrtc-cu12==12.8.61 nvidia-cuda-runtime-cu12==12.8.57 nvidia-cudnn-cu12==9.7.1.26 nvidia-cufft-cu12==11.3.3.41 nvidia-curand-cu12==10.3.9.55 nvidia-cusolver-cu12==11.7.2.55 nvidia-cusparse-cu12==12.5.4.2 nvidia-cusparselt-cu12==0.6.3 nvidia-nvjitlink-cu12==12.8.61 nvidia-nvtx-cu12==12.8.55
-```
-
-To use Faster Whisper on GPU, install the following libraries:
-* [cuBLAS](https://developer.nvidia.com/cublas)
-* [cuDNN](https://developer.nvidia.com/cudnn)
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 4beb3323..1e02c1f1 100644
--- a/Makefile
+++ b/Makefile
@@ -1,242 +1,34 @@
-# Change also in pyproject.toml and buzz/__version__.py
-version := 1.4.4
-
-mac_app_path := ./dist/Buzz.app
-mac_zip_path := ./dist/Buzz-${version}-mac.zip
-mac_dmg_path := ./dist/Buzz-${version}-mac.dmg
-
-bundle_windows: dist/Buzz
- iscc installer.iss
-
-bundle_mac: dist/Buzz.app codesign_all_mac zip_mac notarize_zip staple_app_mac dmg_mac
-
-bundle_mac_unsigned: dist/Buzz.app zip_mac dmg_mac_unsigned
-
-clean:
-ifeq ($(OS), Windows_NT)
- -rmdir /s /q buzz\whisper_cpp
- -rmdir /s /q whisper.cpp\build
- -rmdir /s /q dist
- -Remove-Item -Recurse -Force buzz\whisper_cpp
- -Remove-Item -Recurse -Force whisper.cpp\build
- -Remove-Item -Recurse -Force dist\*
- -rm -rf buzz/whisper_cpp
- -rm -rf whisper.cpp/build
- -rm -rf dist/*
- -rm -rf buzz/__pycache__ buzz/**/__pycache__ buzz/**/**/__pycache__ buzz/**/**/**/__pycache__
- -for /d /r buzz %%d in (__pycache__) do @if exist "%%d" rmdir /s /q "%%d"
-else
- rm -rf buzz/whisper_cpp || true
- rm -rf whisper.cpp/build || true
- rm -rf dist/* || true
- find buzz -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
-endif
-
-COVERAGE_THRESHOLD := 70
-
-test: buzz/whisper_cpp
-# A check to get updates of yt-dlp. Should run only on local as part of regular development operations
-# Sort of a local "update checker"
-ifndef CI
- uv lock --upgrade-package yt-dlp
-endif
- pytest -s -vv --cov=buzz --cov-report=xml --cov-report=html --benchmark-skip --cov-fail-under=${COVERAGE_THRESHOLD} --cov-config=.coveragerc
-
-benchmarks: buzz/whisper_cpp
- pytest -s -vv --benchmark-only --benchmark-json benchmarks.json
-
-dist/Buzz dist/Buzz.app: buzz/whisper_cpp
+buzz:
pyinstaller --noconfirm Buzz.spec
-version:
- echo "VERSION = \"${version}\"" > buzz/__version__.py
+clean:
+ rm -r dist/* || true
-buzz/whisper_cpp: translation_mo
-ifeq ($(OS), Windows_NT)
- # Build Whisper with Vulkan support.
- # 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
- # GGML_NATIVE=OFF ensures we don't use -march=native (which would target the build machine's CPU)
- 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 -DGGML_NATIVE=OFF
- cmake --build whisper.cpp/build -j --config Release --verbose
+bundle_windows:
+ make buzz
+ tar -czf dist/buzz-${version}-windows.tar.gz dist/Buzz
- -mkdir buzz/whisper_cpp
- cp whisper.cpp/build/bin/Release/whisper-cli.exe buzz/whisper_cpp/
- cp whisper.cpp/build/bin/Release/whisper-server.exe buzz/whisper_cpp/
- cp dll_backup/SDL2.dll buzz/whisper_cpp
- PowerShell -NoProfile -ExecutionPolicy Bypass -Command "if (-not (Test-Path 'buzz\whisper_cpp\ggml-silero-v6.2.0.bin')) { Start-BitsTransfer -Source https://huggingface.co/ggml-org/whisper-vad/resolve/main/ggml-silero-v6.2.0.bin -Destination 'buzz\whisper_cpp\ggml-silero-v6.2.0.bin' }"
-endif
+test:
+ pytest --cov --cov-fail-under=73
-ifeq ($(shell uname -s), Linux)
- # Build Whisper with Vulkan support
- # GGML_NATIVE=OFF ensures we don't use -march=native (which would target the build machine's CPU)
- # This enables portable SSE4.2/AVX/AVX2 optimizations that work on most x86_64 CPUs
- 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 -DGGML_VULKAN=1 -DGGML_NATIVE=OFF
- cmake --build whisper.cpp/build -j --config Release --verbose
- cp whisper.cpp/build/bin/whisper-cli buzz/whisper_cpp/ || true
- cp whisper.cpp/build/bin/whisper-server buzz/whisper_cpp/ || true
- cp -P whisper.cpp/build/src/libwhisper.so* buzz/whisper_cpp/ || true
- cp -P whisper.cpp/build/ggml/src/libggml.so* buzz/whisper_cpp/ || true
- cp -P whisper.cpp/build/ggml/src/libggml-base.so* buzz/whisper_cpp/ || true
- cp -P whisper.cpp/build/ggml/src/libggml-cpu.so* buzz/whisper_cpp/ || true
- cp -P whisper.cpp/build/ggml/src/ggml-vulkan/libggml-vulkan.so* buzz/whisper_cpp/ || true
- test -f buzz/whisper_cpp/ggml-silero-v6.2.0.bin || curl -L -o buzz/whisper_cpp/ggml-silero-v6.2.0.bin https://huggingface.co/ggml-org/whisper-vad/resolve/main/ggml-silero-v6.2.0.bin
-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
- # Intel
- cmake -S whisper.cpp -B whisper.cpp/build/ -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DGGML_VULKAN=0 -DGGML_METAL=0
-endif
-
- cmake --build whisper.cpp/build -j --config Release --verbose
- cp whisper.cpp/build/bin/whisper-cli buzz/whisper_cpp/ || true
- cp whisper.cpp/build/bin/whisper-server buzz/whisper_cpp/ || true
- cp whisper.cpp/build/src/libwhisper.dylib buzz/whisper_cpp/ || true
- cp whisper.cpp/build/ggml/src/libggml* buzz/whisper_cpp/ || true
- test -f buzz/whisper_cpp/ggml-silero-v6.2.0.bin || curl -L -o buzz/whisper_cpp/ggml-silero-v6.2.0.bin https://huggingface.co/ggml-org/whisper-vad/resolve/main/ggml-silero-v6.2.0.bin
-endif
-
-# Prints all the Mac developer identities used for code signing
-print_identities_mac:
- security find-identity -p basic -v
-
-dmg_mac:
- ditto -x -k "${mac_zip_path}" dist/dmg
+bundle_mac:
+ make buzz
+ tar -czf dist/buzz-${version}-unix.tar.gz dist/Buzz
+ mkdir -p dist/dmg && cp -r dist/Buzz.app dist/dmg
create-dmg \
--volname "Buzz" \
- --volicon "./assets/buzz.icns" \
+ --volicon "dist/Buzz.app/Contents/Resources/icon-windowed.icns" \
--window-pos 200 120 \
--window-size 600 300 \
--icon-size 100 \
- --icon "Buzz.app" 175 120 \
+ --icon "dist/Buzz.app/Contents/Resources/icon-windowed.icns" 175 120 \
--hide-extension "Buzz.app" \
--app-drop-link 425 120 \
- --codesign "$$BUZZ_CODESIGN_IDENTITY" \
- --notarize "$$BUZZ_KEYCHAIN_NOTARY_PROFILE" \
- --filesystem APFS \
- "${mac_dmg_path}" \
+ "dist/buzz-${version}-mac.dmg" \
"dist/dmg/"
-dmg_mac_unsigned:
- ditto -x -k "${mac_zip_path}" dist/dmg
- create-dmg \
- --volname "Buzz" \
- --volicon "./assets/buzz.icns" \
- --window-pos 200 120 \
- --window-size 600 300 \
- --icon-size 100 \
- --icon "Buzz.app" 175 120 \
- --hide-extension "Buzz.app" \
- --app-drop-link 425 120 \
- "${mac_dmg_path}" \
- "dist/dmg/"
-
-staple_app_mac:
- xcrun stapler staple ${mac_app_path}
-
-notarize_zip:
- xcrun notarytool submit ${mac_zip_path} --keychain-profile "$$BUZZ_KEYCHAIN_NOTARY_PROFILE" --wait
-
-zip_mac:
- ditto -c -k --keepParent "${mac_app_path}" "${mac_zip_path}"
-
-codesign_all_mac: dist/Buzz.app
- for i in $$(find dist/Buzz.app/Contents/Resources/torch/bin -name "*" -type f); \
- do \
- codesign --force --options=runtime --sign "$$BUZZ_CODESIGN_IDENTITY" --timestamp "$$i"; \
- done
- for i in $$(find dist/Buzz.app/Contents/Resources -name "*.dylib" -o -name "*.so" -type f); \
- do \
- codesign --force --options=runtime --sign "$$BUZZ_CODESIGN_IDENTITY" --timestamp "$$i"; \
- done
- for i in $$(find dist/Buzz.app/Contents/MacOS -name "*.dylib" -o -name "*.so" -o -name "Qt*" -o -name "Python" -type f); \
- do \
- codesign --force --options=runtime --sign "$$BUZZ_CODESIGN_IDENTITY" --timestamp "$$i"; \
- done
- codesign --force --options=runtime --sign "$$BUZZ_CODESIGN_IDENTITY" --timestamp dist/Buzz.app/Contents/MacOS/Buzz
- codesign --force --options=runtime --sign "$$BUZZ_CODESIGN_IDENTITY" --entitlements ./entitlements.plist --timestamp dist/Buzz.app
- codesign --verify --deep --strict --verbose=2 dist/Buzz.app
-
-# HELPERS
-
-# Get the build logs for a notary upload
-notarize_log:
- xcrun notarytool log ${id} --keychain-profile "$$BUZZ_KEYCHAIN_NOTARY_PROFILE"
-
-# Make GGML model from whisper. Example: make ggml model_path=/Users/chidiwilliams/.cache/whisper/medium.pt
-ggml:
- python3 ./whisper.cpp/models/convert-pt-to-ggml.py ${model_path} .venv/lib/python3.12/site-packages/whisper dist
-
-upload_brew:
- brew bump-cask-pr --version ${version} --verbose buzz
-
-UPGRADE_VERSION_BRANCH := upgrade-to-${version}
-gh_upgrade_pr:
- git checkout main && git pull
- git checkout -B ${UPGRADE_VERSION_BRANCH}
-
- make version version=${version}
-
- git commit -am "Upgrade to ${version}"
- git push --set-upstream origin ${UPGRADE_VERSION_BRANCH}
-
- gh pr create --fill
- gh pr merge ${UPGRADE_VERSION_BRANCH} --auto --squash
-
-# Internationalization
-
-translation_po_all:
- $(MAKE) translation_po locale=ca_ES
- $(MAKE) translation_po locale=da_DK
- $(MAKE) translation_po locale=de_DE
- $(MAKE) translation_po locale=en_US
- $(MAKE) translation_po locale=es_ES
- $(MAKE) translation_po locale=it_IT
- $(MAKE) translation_po locale=ja_JP
- $(MAKE) translation_po locale=lv_LV
- $(MAKE) translation_po locale=nl
- $(MAKE) translation_po locale=pl_PL
- $(MAKE) translation_po locale=pt_BR
- $(MAKE) translation_po locale=uk_UA
- $(MAKE) translation_po locale=zh_CN
- $(MAKE) translation_po locale=zh_TW
-
-TMP_POT_FILE_PATH := $(shell mktemp)
-PO_FILE_PATH := buzz/locale/${locale}/LC_MESSAGES/buzz.po
-translation_po:
- mkdir -p buzz/locale/${locale}/LC_MESSAGES
- xgettext --from-code=UTF-8 --add-location=file -o "${TMP_POT_FILE_PATH}" -l python $(shell find buzz -name '*.py')
- sed -i.bak 's/CHARSET/UTF-8/' ${TMP_POT_FILE_PATH}
- if [ ! -f ${PO_FILE_PATH} ]; then \
- msginit --no-translator --input=${TMP_POT_FILE_PATH} --output-file=${PO_FILE_PATH}; \
- fi
- rm ${TMP_POT_FILE_PATH}.bak
- msgmerge -U ${PO_FILE_PATH} ${TMP_POT_FILE_PATH}
-
-# On windows we can have two ways to compile locales, one for CI the other for local builds
-# Will try both and ignore errors if they fail
-translation_mo:
-ifeq ($(OS), Windows_NT)
- -forfiles /p buzz\locale /c "cmd /c python ..\..\msgfmt.py -o @path\LC_MESSAGES\buzz.mo @path\LC_MESSAGES\buzz.po"
- -for dir in buzz/locale/*/ ; do \
- python msgfmt.py -o $$dir/LC_MESSAGES/buzz.mo $$dir/LC_MESSAGES/buzz.po; \
- done
-else
- for dir in buzz/locale/*/ ; do \
- python3 msgfmt.py -o $$dir/LC_MESSAGES/buzz.mo $$dir/LC_MESSAGES/buzz.po; \
- done
-endif
-
-lint:
- ruff check . --fix
- ruff format .
+release:
+ make clean
+ make bundle_mac version=${version}
+ poetry version ${version}
+ git tag "v${version}"
diff --git a/README.ja_JP.md b/README.ja_JP.md
deleted file mode 100644
index 9990e5e6..00000000
--- a/README.ja_JP.md
+++ /dev/null
@@ -1,98 +0,0 @@
-# Buzz
-
-[ドキュメント](https://chidiwilliams.github.io/buzz/)
-
-パソコン上でオフラインで音声の文字起こしと翻訳を行います。OpenAIの[Whisper](https://github.com/openai/whisper)を使用しています。
-
-
-[](https://github.com/chidiwilliams/buzz/actions/workflows/ci.yml)
-[](https://codecov.io/github/chidiwilliams/buzz)
-
-[](https://GitHub.com/chidiwilliams/buzz/releases/)
-
-
-
-## 機能
-- 音声・動画ファイルまたはYouTubeリンクの文字起こし
-- マイクからのリアルタイム音声文字起こし
- - イベントやプレゼンテーション中に便利なプレゼンテーションウィンドウ
-- ノイズの多い音声でより高い精度を得るための、文字起こし前の話者分離
-- 文字起こしメディアでの話者識別
-- 複数のWhisperバックエンドをサポート
- - Nvidia GPU向けCUDAアクセラレーション対応
- - Mac向けApple Silicon対応
- - Whisper.cppでのVulkanアクセラレーション対応(統合GPUを含むほとんどのGPUで利用可能)
-- TXT、SRT、VTT形式での文字起こしエクスポート
-- 検索、再生コントロール、速度調整機能を備えた高度な文字起こしビューア
-- 効率的なナビゲーションのためのキーボードショートカット
-- 新しいファイルの自動文字起こしのための監視フォルダ
-- スクリプトや自動化のためのコマンドラインインターフェース
-
-## インストール
-
-### macOS
-
-[SourceForge](https://sourceforge.net/projects/buzz-captions/files/)から`.dmg`ファイルをダウンロードしてください。
-
-### Windows
-
-[SourceForge](https://sourceforge.net/projects/buzz-captions/files/)からインストールファイルを入手してください。
-
-アプリは署名されていないため、インストール時に警告が表示されます。`詳細情報` -> `実行`を選択してください。
-
-### Linux
-
-Buzzは[Flatpak](https://flathub.org/apps/io.github.chidiwilliams.Buzz)または[Snap](https://snapcraft.io/buzz)として利用可能です。
-
-Flatpakをインストールするには、以下を実行してください:
-```shell
-flatpak install flathub io.github.chidiwilliams.Buzz
-```
-
-[](https://flathub.org/en/apps/io.github.chidiwilliams.Buzz)
-
-Snapをインストールするには、以下を実行してください:
-```shell
-sudo apt-get install libportaudio2 libcanberra-gtk-module libcanberra-gtk3-module
-sudo snap install buzz
-```
-
-[](https://snapcraft.io/buzz)
-
-### PyPI
-
-[ffmpeg](https://www.ffmpeg.org/download.html)をインストールしてください。
-
-Python 3.12環境を使用していることを確認してください。
-
-Buzzをインストール
-
-```shell
-pip install buzz-captions
-python -m buzz
-```
-
-**PyPIでのGPUサポート**
-
-PyPIでインストールしたバージョンでWindows上のNvidia GPUのGPUサポートを有効にするには、[torch](https://pytorch.org/get-started/locally/)のCUDAサポートを確認してください。
-
-```
-pip3 install -U torch==2.8.0+cu129 torchaudio==2.8.0+cu129 --index-url https://download.pytorch.org/whl/cu129
-pip3 install nvidia-cublas-cu12==12.9.1.4 nvidia-cuda-cupti-cu12==12.9.79 nvidia-cuda-runtime-cu12==12.9.79 --extra-index-url https://pypi.ngc.nvidia.com
-```
-
-### 最新開発版
-
-最新の機能やバグ修正を含む最新開発版の入手方法については、[FAQ](https://chidiwilliams.github.io/buzz/docs/faq#9-where-can-i-get-latest-development-version)をご覧ください。
-
-### スクリーンショット
-
-
diff --git a/README.md b/README.md
index b8cb5e19..bce14cd1 100644
--- a/README.md
+++ b/README.md
@@ -1,106 +1,116 @@
-[[简体中文](readme/README.zh_CN.md)] <- 点击查看中文页面。
-
# Buzz
-[Documentation](https://chidiwilliams.github.io/buzz/)
-
-Transcribe and translate audio offline on your personal computer. Powered by
-OpenAI's [Whisper](https://github.com/openai/whisper).
+

[](https://github.com/chidiwilliams/buzz/actions/workflows/ci.yml)
-[](https://codecov.io/github/chidiwilliams/buzz)

-[](https://GitHub.com/chidiwilliams/buzz/releases/)
-
+Buzz transcribes audio from your computer's microphones to text in real-time using OpenAI's [Whisper](https://github.com/openai/whisper).
-## Features
-- Transcribe audio and video files or Youtube links
-- Live realtime audio transcription from microphone
- - Presentation window for easy accessibility during events and presentations
-- Speech separation before transcription for better accuracy on noisy audio
-- Speaker identification in transcribed media
-- Multiple whisper backend support
- - CUDA acceleration support for Nvidia GPUs
- - Apple Silicon support for Macs
- - Vulkan acceleration support for Whisper.cpp on most GPUs, including integrated GPUs
-- Export transcripts to TXT, SRT, and VTT
-- Advanced Transcription Viewer with search, playback controls, and speed adjustment
-- Keyboard shortcuts for efficient navigation
-- Watch folder for automatic transcription of new files
-- Command-Line Interface for scripting and automation
+
+ Buzz - Watch Video
+
+
+
+## Requirements
+
+To set up Buzz, first install ffmpeg ([needed to run Whisper](https://github.com/openai/whisper#setup)).
+
+```text
+# on Ubuntu or Debian
+sudo apt update && sudo apt install ffmpeg
+
+# on MacOS using Homebrew (https://brew.sh/)
+brew install ffmpeg
+
+# on Windows using Chocolatey (https://chocolatey.org/)
+choco install ffmpeg
+
+# on Windows using Scoop (https://scoop.sh/)
+scoop install ffmpeg
+```
## Installation
-### macOS
+To install Buzz, download the [latest version](https://github.com/chidiwilliams/buzz/releases/latest) for your Operating System.
-Download the `.dmg` from the [SourceForge](https://sourceforge.net/projects/buzz-captions/files/).
+## Mac
-### Windows
+- Download and open the `*-mac.dmg` file.
+- After the installation window opens, drag the Buzz icon into the folder to add Buzz to your Applications directory.
-Get the installation files from the [SourceForge](https://sourceforge.net/projects/buzz-captions/files/).
+## Windows
-App is not signed, you will get a warning when you install it. Select `More info` -> `Run anyway`.
+- Download and unzip the `*-windows.zip` file.
+- Open the Buzz.exe file
-### Linux
+## Linux
-Buzz is available as a [Flatpak](https://flathub.org/apps/io.github.chidiwilliams.Buzz) or a [Snap](https://snapcraft.io/buzz).
+- Download and unzip the `*-unix.zip` file.
+- Open the Buzz binary file.
-To install flatpak, run:
-```shell
-flatpak install flathub io.github.chidiwilliams.Buzz
-```
+## How to use
-[](https://flathub.org/en/apps/io.github.chidiwilliams.Buzz)
+To record from a system microphone, select a model, language, task, microphone, and delay, then click Record.
-To install snap, run:
-```shell
-sudo apt-get install libportaudio2 libcanberra-gtk-module libcanberra-gtk3-module
-sudo snap install buzz
-```
+**Model**: Default: Tiny.
-[](https://snapcraft.io/buzz)
+**Language**: Default: English.
-### PyPI
+**Task**: Transcribe/Translate. Default: Transcribe.
-Install [ffmpeg](https://www.ffmpeg.org/download.html)
+**Microphone**: Default: System default microphone.
-Ensure you use Python 3.12 environment.
+**Delay**: The length of time (in seconds) Buzz waits before transcribing a new batch of recorded audio. Increasing this value will make Buzz take longer to show new transcribed text. However, shorter delays cut the audio into smaller chunks which may reduce the accuracy of the transcription. Default: 10s.
-Install Buzz
+For more information about the available model types, languages, and tasks, see the [Whisper docs](https://github.com/openai/whisper).
+
+### Record audio playing from computer
+
+To record audio playing out from your computer, you'll need to install an audio loopback driver (a program that lets you create virtual audio devices). The rest of this guide will use [BlackHole](https://github.com/ExistentialAudio/BlackHole) on Mac, but you can use other alternatives for your operating system (see [LoopBeAudio](https://nerds.de/en/loopbeaudio.html), [LoopBack](https://rogueamoeba.com/loopback/), and [Virtual Audio Cable](https://vac.muzychenko.net/en/)).
+
+1. Install [BlackHole via Homebrew](https://github.com/ExistentialAudio/BlackHole#option-2-install-via-homebrew)
+
+ ```shell
+ brew install blackhole-2ch
+ ```
+
+2. Open Audio MIDI Setup from Spotlight or from `/Applications/Utilities/Audio Midi Setup.app`.
+
+ 
+
+3. Click the '+' icon at the lower left corner and select 'Create Multi-Output Device'.
+
+ 
+
+4. Add your default speaker and BlackHole to the multi-output device.
+
+ 
+
+5. Select this multi-output device as your speaker (application or system-wide) to play audio into BlackHole.
+
+6. Open Buzz, select BlackHole as your microphone, and record as before to see transcriptions from the audio playing through BlackHole.
+
+## Build/run locally
+
+To build/run Buzz locally from source, first install the dependencies:
+
+1. Install [Poetry](https://python-poetry.org/docs/#installing-with-the-official-installer).
+2. Install the project dependencies.
+
+ ```shell
+ poetry install
+ ```
+
+Then, to run:
```shell
-pip install buzz-captions
-python -m buzz
+poetry run python main.py
```
-**GPU support for PyPI**
-
-To have GPU support for Nvidia GPUS on Windows, for PyPI installed version ensure, CUDA support for [torch](https://pytorch.org/get-started/locally/)
+To build:
+```shell
+poetry run pyinstaller --noconfirm Buzz.spec
```
-pip3 install -U torch==2.8.0+cu129 torchaudio==2.8.0+cu129 --index-url https://download.pytorch.org/whl/cu129
-pip3 install nvidia-cublas-cu12==12.9.1.4 nvidia-cuda-cupti-cu12==12.9.79 nvidia-cuda-runtime-cu12==12.9.79 --extra-index-url https://pypi.ngc.nvidia.com
-```
-
-### Latest development version
-
-For info on how to get latest development version with latest features and bug fixes see [FAQ](https://chidiwilliams.github.io/buzz/docs/faq#9-where-can-i-get-latest-development-version).
-
-### Support Buzz
-
-You can help the Buzz by starring 🌟 the repo and sharing it with your friends.
-
-### Screenshots
-
-
-
diff --git a/assets/buzz.icns b/assets/buzz.icns
deleted file mode 100644
index 1dba188a..00000000
Binary files a/assets/buzz.icns and /dev/null differ
diff --git a/assets/buzz.ico b/assets/buzz.ico
deleted file mode 100644
index 91182d81..00000000
Binary files a/assets/buzz.ico and /dev/null differ
diff --git a/buzz.desktop b/buzz.desktop
deleted file mode 100644
index 1e8cf81d..00000000
--- a/buzz.desktop
+++ /dev/null
@@ -1,17 +0,0 @@
-[Desktop Entry]
-
-Type=Application
-
-Encoding=UTF-8
-
-Name=Buzz
-
-Comment=Buzz transcribes and translates audio offline on your personal computer.
-
-Path=/opt/buzz
-
-Exec=/opt/buzz/Buzz
-
-Icon=buzz
-
-Terminal=false
diff --git a/buzz/assets/buzz.png b/buzz.png
similarity index 100%
rename from buzz/assets/buzz.png
rename to buzz.png
diff --git a/buzz/__init__.py b/buzz/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/buzz/__main__.py b/buzz/__main__.py
deleted file mode 100644
index 36656cc6..00000000
--- a/buzz/__main__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import buzz.buzz
-
-if __name__ == "__main__":
- buzz.buzz.main()
diff --git a/buzz/__version__.py b/buzz/__version__.py
deleted file mode 100644
index 8f8e75e9..00000000
--- a/buzz/__version__.py
+++ /dev/null
@@ -1 +0,0 @@
-VERSION = "1.4.4"
diff --git a/buzz/action.py b/buzz/action.py
deleted file mode 100644
index 9a9d8675..00000000
--- a/buzz/action.py
+++ /dev/null
@@ -1,23 +0,0 @@
-import typing
-
-from PyQt6.QtGui import QAction, QKeySequence
-
-
-class Action(QAction):
- def setShortcut(
- self,
- shortcut: typing.Union["QKeySequence", "QKeySequence.StandardKey", str, int],
- ) -> None:
- super().setShortcut(shortcut)
- self.setToolTip(Action.get_tooltip(self))
-
- @classmethod
- def get_tooltip(cls, action: QAction):
- tooltip = action.toolTip()
- shortcut = action.shortcut()
-
- if shortcut.isEmpty():
- return tooltip
-
- shortcut_text = shortcut.toString(QKeySequence.SequenceFormat.NativeText)
- return f"{tooltip} {shortcut_text}
"
diff --git a/buzz/assets.py b/buzz/assets.py
deleted file mode 100644
index 1b752a18..00000000
--- a/buzz/assets.py
+++ /dev/null
@@ -1,12 +0,0 @@
-import os
-import sys
-
-APP_BASE_DIR = (
- getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__)))
- if getattr(sys, "frozen", False)
- else os.path.dirname(__file__)
-)
-
-
-def get_path(path: str):
- return os.path.join(APP_BASE_DIR, path)
diff --git a/buzz/assets/add_FILL0_wght700_GRAD0_opsz48.svg b/buzz/assets/add_FILL0_wght700_GRAD0_opsz48.svg
deleted file mode 100644
index b85dda70..00000000
--- a/buzz/assets/add_FILL0_wght700_GRAD0_opsz48.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/buzz/assets/buzz-banner.jpg b/buzz/assets/buzz-banner.jpg
deleted file mode 100644
index 0d6a7d15..00000000
Binary files a/buzz/assets/buzz-banner.jpg and /dev/null differ
diff --git a/buzz/assets/buzz-icon-1024.png b/buzz/assets/buzz-icon-1024.png
deleted file mode 100644
index 967c7e6a..00000000
Binary files a/buzz/assets/buzz-icon-1024.png and /dev/null differ
diff --git a/buzz/assets/buzz.icns b/buzz/assets/buzz.icns
deleted file mode 100644
index 1dba188a..00000000
Binary files a/buzz/assets/buzz.icns and /dev/null differ
diff --git a/buzz/assets/buzz.ico b/buzz/assets/buzz.ico
deleted file mode 100644
index 91182d81..00000000
Binary files a/buzz/assets/buzz.ico and /dev/null differ
diff --git a/buzz/assets/buzz.svg b/buzz/assets/buzz.svg
deleted file mode 100644
index d916913d..00000000
--- a/buzz/assets/buzz.svg
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/buzz/assets/cancel_FILL0_wght700_GRAD0_opsz48.svg b/buzz/assets/cancel_FILL0_wght700_GRAD0_opsz48.svg
deleted file mode 100644
index 4dbdda7e..00000000
--- a/buzz/assets/cancel_FILL0_wght700_GRAD0_opsz48.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/buzz/assets/delete_FILL0_wght700_GRAD0_opsz48.svg b/buzz/assets/delete_FILL0_wght700_GRAD0_opsz48.svg
deleted file mode 100644
index 2b8e678f..00000000
--- a/buzz/assets/delete_FILL0_wght700_GRAD0_opsz48.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/buzz/assets/file_download_black_24dp.svg b/buzz/assets/file_download_black_24dp.svg
deleted file mode 100644
index 7b8e8350..00000000
--- a/buzz/assets/file_download_black_24dp.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
diff --git a/buzz/assets/icons/color-background.svg b/buzz/assets/icons/color-background.svg
deleted file mode 100644
index c62912ed..00000000
--- a/buzz/assets/icons/color-background.svg
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/buzz/assets/icons/fullscreen.svg b/buzz/assets/icons/fullscreen.svg
deleted file mode 100644
index e17e748d..00000000
--- a/buzz/assets/icons/fullscreen.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/buzz/assets/icons/gui-text-color.svg b/buzz/assets/icons/gui-text-color.svg
deleted file mode 100644
index 929d172c..00000000
--- a/buzz/assets/icons/gui-text-color.svg
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
\ No newline at end of file
diff --git a/buzz/assets/icons/new-window.svg b/buzz/assets/icons/new-window.svg
deleted file mode 100644
index cfb59177..00000000
--- a/buzz/assets/icons/new-window.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/buzz/assets/info-circle.svg b/buzz/assets/info-circle.svg
deleted file mode 100644
index b1b4d2f3..00000000
--- a/buzz/assets/info-circle.svg
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/buzz/assets/mic_FILL0_wght700_GRAD0_opsz48.svg b/buzz/assets/mic_FILL0_wght700_GRAD0_opsz48.svg
deleted file mode 100644
index 1c4f8af8..00000000
--- a/buzz/assets/mic_FILL0_wght700_GRAD0_opsz48.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/buzz/assets/open_in_full_FILL0_wght700_GRAD0_opsz48.svg b/buzz/assets/open_in_full_FILL0_wght700_GRAD0_opsz48.svg
deleted file mode 100644
index a73936f7..00000000
--- a/buzz/assets/open_in_full_FILL0_wght700_GRAD0_opsz48.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/buzz/assets/pause_black_24dp.svg b/buzz/assets/pause_black_24dp.svg
deleted file mode 100644
index 05913cdf..00000000
--- a/buzz/assets/pause_black_24dp.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/buzz/assets/play_arrow_black_24dp.svg b/buzz/assets/play_arrow_black_24dp.svg
deleted file mode 100644
index 6134c50b..00000000
--- a/buzz/assets/play_arrow_black_24dp.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/buzz/assets/redo_FILL0_wght700_GRAD0_opsz48.svg b/buzz/assets/redo_FILL0_wght700_GRAD0_opsz48.svg
deleted file mode 100644
index 92a79fcc..00000000
--- a/buzz/assets/redo_FILL0_wght700_GRAD0_opsz48.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/buzz/assets/resize_black.svg b/buzz/assets/resize_black.svg
deleted file mode 100644
index ac973d87..00000000
--- a/buzz/assets/resize_black.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/buzz/assets/speaker-identification.svg b/buzz/assets/speaker-identification.svg
deleted file mode 100644
index cfea8b41..00000000
--- a/buzz/assets/speaker-identification.svg
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/buzz/assets/translate_black.svg b/buzz/assets/translate_black.svg
deleted file mode 100644
index 627853ca..00000000
--- a/buzz/assets/translate_black.svg
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/buzz/assets/undo_FILL0_wght700_GRAD0_opsz48.svg b/buzz/assets/undo_FILL0_wght700_GRAD0_opsz48.svg
deleted file mode 100644
index 2f495847..00000000
--- a/buzz/assets/undo_FILL0_wght700_GRAD0_opsz48.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/buzz/assets/update_FILL0_wght700_GRAD0_opsz48.svg b/buzz/assets/update_FILL0_wght700_GRAD0_opsz48.svg
deleted file mode 100644
index d01ac5a7..00000000
--- a/buzz/assets/update_FILL0_wght700_GRAD0_opsz48.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/buzz/assets/url.svg b/buzz/assets/url.svg
deleted file mode 100644
index 3b193417..00000000
--- a/buzz/assets/url.svg
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
- url [#1423]
- Created with Sketch.
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/buzz/assets/visibility_FILL0_wght700_GRAD0_opsz48.svg b/buzz/assets/visibility_FILL0_wght700_GRAD0_opsz48.svg
deleted file mode 100644
index eeb6692f..00000000
--- a/buzz/assets/visibility_FILL0_wght700_GRAD0_opsz48.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/buzz/assets/visibility_off_FILL0_wght700_GRAD0_opsz48.svg b/buzz/assets/visibility_off_FILL0_wght700_GRAD0_opsz48.svg
deleted file mode 100644
index 3f008781..00000000
--- a/buzz/assets/visibility_off_FILL0_wght700_GRAD0_opsz48.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/buzz/buzz.py b/buzz/buzz.py
deleted file mode 100644
index 6f434353..00000000
--- a/buzz/buzz.py
+++ /dev/null
@@ -1,102 +0,0 @@
-import faulthandler
-import logging
-import multiprocessing
-import os
-import platform
-import sys
-from pathlib import Path
-from typing import TextIO
-
-# Set up CUDA library paths before any torch imports
-# This must happen before platformdirs or any other imports that might indirectly load torch
-import buzz.cuda_setup # noqa: F401
-
-from platformdirs import user_log_dir, user_cache_dir, user_data_dir
-
-# Will download all Huggingface data to the app cache directory
-os.environ.setdefault("HF_HOME", user_cache_dir("Buzz"))
-
-from buzz.assets import APP_BASE_DIR
-
-# Check for segfaults if not running in frozen mode
-# Note: On Windows, faulthandler can print "Windows fatal exception" messages
-# for non-fatal RPC errors (0x800706be) during multiprocessing operations.
-# These are usually harmless but noisy, so we disable faulthandler on Windows.
-if getattr(sys, "frozen", False) is False and platform.system() != "Windows":
- faulthandler.enable()
-
-# Sets stdout/stderr to no-op TextIO when None (run as Windows GUI with --noconsole).
-# stdout fix: torch.hub uses sys.stdout.write() for download progress and crashes if None.
-# stderr fix: Resolves https://github.com/chidiwilliams/buzz/issues/221
-if sys.stdout is None:
- sys.stdout = TextIO()
-if sys.stderr is None:
- sys.stderr = TextIO()
-
-# Adds the current directory to the PATH, so the ffmpeg binary get picked up:
-# https://stackoverflow.com/a/44352931/9830227
-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)
-
- 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)
-
- onnx_dll_dir = os.path.join(APP_BASE_DIR, "onnxruntime", "capi")
- if os.path.isdir(onnx_dll_dir):
- os.add_dll_directory(onnx_dll_dir)
-
-
-def main():
- if platform.system() == "Linux":
- multiprocessing.set_start_method("spawn")
-
- # Fixes opening new window when app has been frozen on Windows:
- # https://stackoverflow.com/a/33979091
- multiprocessing.freeze_support()
-
- log_dir = user_log_dir(appname="Buzz")
- os.makedirs(log_dir, exist_ok=True)
-
- log_format = (
- "[%(asctime)s] %(module)s.%(funcName)s:%(lineno)d %(levelname)s -> %(message)s"
- )
- logging.basicConfig(
- filename=os.path.join(log_dir, "logs.txt"),
- level=logging.DEBUG,
- format=log_format,
- )
-
- # Silence noisy third-party library loggers
- logging.getLogger("matplotlib").setLevel(logging.WARNING)
- logging.getLogger("graphviz").setLevel(logging.WARNING)
- logging.getLogger("nemo_logger").setLevel(logging.ERROR)
- logging.getLogger("nemo_logging").setLevel(logging.ERROR)
- logging.getLogger("numba").setLevel(logging.WARNING)
- logging.getLogger("torio._extension.utils").setLevel(logging.WARNING)
- logging.getLogger("export_config_manager").setLevel(logging.WARNING)
- logging.getLogger("training_telemetry_provider").setLevel(logging.ERROR)
- logging.getLogger("default_recorder").setLevel(logging.WARNING)
- logging.getLogger("config").setLevel(logging.WARNING)
-
- if getattr(sys, "frozen", False) is False:
- stdout_handler = logging.StreamHandler(sys.stdout)
- stdout_handler.setLevel(logging.DEBUG)
- stdout_handler.setFormatter(logging.Formatter(log_format))
- logging.getLogger().addHandler(stdout_handler)
-
- from buzz.cli import parse_command_line
- from buzz.widgets.application import Application
-
- logging.debug("app_dir: %s", APP_BASE_DIR)
- logging.debug("log_dir: %s", log_dir)
- logging.debug("cache_dir: %s", user_cache_dir("Buzz"))
- logging.debug("data_dir: %s", user_data_dir("Buzz"))
-
- app = Application(sys.argv)
- parse_command_line(app)
- app.show_main_window()
- sys.exit(app.exec())
diff --git a/buzz/cache.py b/buzz/cache.py
deleted file mode 100644
index 0446a4ab..00000000
--- a/buzz/cache.py
+++ /dev/null
@@ -1,79 +0,0 @@
-import json
-import logging
-import os
-import pickle
-from typing import List
-
-from platformdirs import user_cache_dir
-
-from buzz.transcriber.transcriber import FileTranscriptionTask
-
-
-class TasksCache:
- def __init__(self, cache_dir=user_cache_dir("Buzz")):
- os.makedirs(cache_dir, exist_ok=True)
- self.cache_dir = cache_dir
- self.pickle_cache_file_path = os.path.join(cache_dir, "tasks")
- self.tasks_list_file_path = os.path.join(cache_dir, "tasks.json")
-
- def save(self, tasks: List[FileTranscriptionTask]):
- self.save_json_tasks(tasks=tasks)
-
- def load(self) -> List[FileTranscriptionTask]:
- if os.path.exists(self.tasks_list_file_path):
- return self.load_json_tasks()
-
- try:
- with open(self.pickle_cache_file_path, "rb") as file:
- return pickle.load(file)
- except FileNotFoundError:
- return []
- except (
- pickle.UnpicklingError,
- AttributeError,
- ValueError,
- ): # delete corrupted cache
- os.remove(self.pickle_cache_file_path)
- return []
-
- def load_json_tasks(self) -> List[FileTranscriptionTask]:
- task_ids: List[int]
- try:
- with open(self.tasks_list_file_path) as file:
- task_ids = json.load(file)
- except json.JSONDecodeError:
- logging.debug(
- "Got JSONDecodeError while reading tasks list file path, "
- "resetting cache..."
- )
- task_ids = []
-
- tasks = []
- for task_id in task_ids:
- try:
- with open(self.get_task_path(task_id=task_id)) as file:
- tasks.append(FileTranscriptionTask.from_json(file.read()))
- except (FileNotFoundError, json.JSONDecodeError):
- pass
-
- return tasks
-
- def save_json_tasks(self, tasks: List[FileTranscriptionTask]):
- json_str = json.dumps([task.id for task in tasks])
- with open(self.tasks_list_file_path, "w") as file:
- file.write(json_str)
-
- for task in tasks:
- file_path = self.get_task_path(task_id=task.id)
- json_str = task.to_json()
- with open(file_path, "w") as file:
- file.write(json_str)
-
- def get_task_path(self, task_id: int):
- path = os.path.join(self.cache_dir, "transcriptions", f"{task_id}.json")
- os.makedirs(os.path.dirname(path), exist_ok=True)
- return path
-
- def clear(self):
- if os.path.exists(self.pickle_cache_file_path):
- os.remove(self.pickle_cache_file_path)
diff --git a/buzz/cli.py b/buzz/cli.py
deleted file mode 100644
index 6fd56df0..00000000
--- a/buzz/cli.py
+++ /dev/null
@@ -1,253 +0,0 @@
-import enum
-import sys
-import typing
-import urllib.parse
-
-from PyQt6.QtCore import QCommandLineParser, QCommandLineOption
-
-from buzz.model_loader import (
- ModelType,
- WhisperModelSize,
- TranscriptionModel,
- ModelDownloader,
-)
-from buzz.store.keyring_store import get_password, Key
-from buzz.transcriber.transcriber import (
- Task,
- FileTranscriptionTask,
- FileTranscriptionOptions,
- TranscriptionOptions,
- LANGUAGES,
- OutputFormat,
-)
-from buzz.widgets.application import Application
-
-
-class CommandLineError(Exception):
- def __init__(self, message: str):
- super().__init__(message)
-
-
-class CommandLineModelType(enum.Enum):
- WHISPER = "whisper"
- WHISPER_CPP = "whispercpp"
- HUGGING_FACE = "huggingface"
- FASTER_WHISPER = "fasterwhisper"
- OPEN_AI_WHISPER_API = "openaiapi"
-
-
-def parse_command_line(app: Application):
- parser = QCommandLineParser()
- try:
- parse(app, parser)
- except CommandLineError as exc:
- print(f"Error: {str(exc)}\n", file=sys.stderr)
- print(parser.helpText())
- sys.exit(1)
-
-def is_url(path: str) -> bool:
- parsed = urllib.parse.urlparse(path)
- return all([parsed.scheme, parsed.netloc])
-
-def parse(app: Application, parser: QCommandLineParser):
- parser.addPositionalArgument("", "One of the following commands:\n- add")
- parser.parse(app.arguments())
-
- args = parser.positionalArguments()
- if len(args) == 0:
- parser.addHelpOption()
- parser.addVersionOption()
-
- parser.process(app)
- return
-
- command = args[0]
- if command == "add":
- parser.clearPositionalArguments()
-
- parser.addPositionalArgument("files", "Input file paths", "[file file file...]")
-
- task_option = QCommandLineOption(
- ["t", "task"],
- f"The task to perform. Allowed: {join_values(Task)}. Default: {Task.TRANSCRIBE.value}.",
- "task",
- Task.TRANSCRIBE.value,
- )
- model_type_option = QCommandLineOption(
- ["m", "model-type"],
- f"Model type. Allowed: {join_values(CommandLineModelType)}. Default: {CommandLineModelType.WHISPER.value}.",
- "model-type",
- CommandLineModelType.WHISPER.value,
- )
- model_size_option = QCommandLineOption(
- ["s", "model-size"],
- f"Model size. Use only when --model-type is whisper, whispercpp, or fasterwhisper. Allowed: {join_values(WhisperModelSize)}. Default: {WhisperModelSize.TINY.value}.",
- "model-size",
- WhisperModelSize.TINY.value,
- )
- hugging_face_model_id_option = QCommandLineOption(
- ["hfid"],
- 'Hugging Face model ID. Use only when --model-type is huggingface. Example: "openai/whisper-tiny"',
- "id",
- )
- language_option = QCommandLineOption(
- ["l", "language"],
- f'Language code. Allowed: {", ".join(sorted([k + " (" + LANGUAGES[k].title() + ")" for k in LANGUAGES]))}. Leave empty to detect language.',
- "code",
- "",
- )
- initial_prompt_option = QCommandLineOption(
- ["p", "prompt"], "Initial prompt.", "prompt", ""
- )
- word_timestamp_option = QCommandLineOption(
- ["w", "word-timestamps"], "Generate word-level timestamps."
- )
- extract_speech_option = QCommandLineOption(
- ["e", "extract-speech"], "Extract speech from audio before transcribing."
- )
- open_ai_access_token_option = QCommandLineOption(
- "openai-token",
- f"OpenAI access token. Use only when --model-type is {CommandLineModelType.OPEN_AI_WHISPER_API.value}. Defaults to your previously saved access token, if one exists.",
- "token",
- )
- output_directory_option = QCommandLineOption(
- ["d", "output-directory"], "Output directory", "directory"
- )
- srt_option = QCommandLineOption(["srt"], "Output result in an SRT file.")
- vtt_option = QCommandLineOption(["vtt"], "Output result in a VTT file.")
- txt_option = QCommandLineOption("txt", "Output result in a TXT file.")
- hide_gui_option = QCommandLineOption("hide-gui", "Hide the main application window.")
-
- parser.addOptions(
- [
- task_option,
- model_type_option,
- model_size_option,
- hugging_face_model_id_option,
- language_option,
- initial_prompt_option,
- word_timestamp_option,
- extract_speech_option,
- open_ai_access_token_option,
- output_directory_option,
- srt_option,
- vtt_option,
- txt_option,
- hide_gui_option,
- ]
- )
-
- parser.addHelpOption()
- parser.addVersionOption()
-
- parser.process(app)
-
- # slice after first argument, the command
- file_paths = parser.positionalArguments()[1:]
- if len(file_paths) == 0:
- raise CommandLineError("No input files")
-
- task = parse_enum_option(task_option, parser, Task)
-
- model_type = parse_enum_option(model_type_option, parser, CommandLineModelType)
- model_size = parse_enum_option(model_size_option, parser, WhisperModelSize)
-
- hugging_face_model_id = parser.value(hugging_face_model_id_option)
-
- if (
- hugging_face_model_id == ""
- and model_type == CommandLineModelType.HUGGING_FACE
- ):
- raise CommandLineError(
- "--hfid is required when --model-type is huggingface"
- )
-
- model = TranscriptionModel(
- model_type=ModelType[model_type.name],
- whisper_model_size=model_size,
- hugging_face_model_id=hugging_face_model_id,
- )
- ModelDownloader(model=model).run()
- model_path = model.get_local_model_path()
-
- if model_path is None:
- raise CommandLineError("Model not found")
-
- language = parser.value(language_option)
- if language == "":
- language = None
- elif LANGUAGES.get(language) is None:
- raise CommandLineError("Invalid language option")
-
- initial_prompt = parser.value(initial_prompt_option)
-
- word_timestamps = parser.isSet(word_timestamp_option)
- extract_speech = parser.isSet(extract_speech_option)
-
- output_formats: typing.Set[OutputFormat] = set()
- if parser.isSet(srt_option):
- output_formats.add(OutputFormat.SRT)
- if parser.isSet(vtt_option):
- output_formats.add(OutputFormat.VTT)
- if parser.isSet(txt_option):
- output_formats.add(OutputFormat.TXT)
-
- openai_access_token = parser.value(open_ai_access_token_option)
- if (
- model.model_type == ModelType.OPEN_AI_WHISPER_API
- and openai_access_token == ""
- ):
- openai_access_token = get_password(key=Key.OPENAI_API_KEY)
-
- if openai_access_token == "":
- raise CommandLineError("No OpenAI access token found")
-
- output_directory = parser.value(output_directory_option)
-
- transcription_options = TranscriptionOptions(
- model=model,
- task=task,
- language=language,
- initial_prompt=initial_prompt,
- word_level_timings=word_timestamps,
- extract_speech=extract_speech,
- openai_access_token=openai_access_token,
- )
-
- for file_path in file_paths:
- path_is_url = is_url(file_path)
-
- file_transcription_options = FileTranscriptionOptions(
- file_paths=[file_path] if not path_is_url else None,
- url=file_path if path_is_url else None,
- output_formats=output_formats,
- )
-
- transcription_task = FileTranscriptionTask(
- file_path=file_path if not path_is_url else None,
- url=file_path if path_is_url else None,
- source=FileTranscriptionTask.Source.FILE_IMPORT if not path_is_url else FileTranscriptionTask.Source.URL_IMPORT,
- model_path=model_path,
- transcription_options=transcription_options,
- file_transcription_options=file_transcription_options,
- output_directory=output_directory if output_directory != "" else None,
- )
- app.add_task(transcription_task, quit_on_complete=True)
-
- if parser.isSet(hide_gui_option):
- app.hide_main_window = True
-
-T = typing.TypeVar("T", bound=enum.Enum)
-
-
-def parse_enum_option(
- option: QCommandLineOption, parser: QCommandLineParser, enum_class: typing.Type[T]
-) -> T:
- try:
- return enum_class(parser.value(option))
- except ValueError:
- raise CommandLineError(f"Invalid value for --{option.names()[-1]} option.")
-
-
-def join_values(enum_class: typing.Type[enum.Enum]) -> str:
- return ", ".join([v.value for v in enum_class])
diff --git a/buzz/conn.py b/buzz/conn.py
deleted file mode 100644
index b17fcb63..00000000
--- a/buzz/conn.py
+++ /dev/null
@@ -1,21 +0,0 @@
-import sys
-from contextlib import contextmanager
-from multiprocessing.connection import Connection
-
-
-class ConnWriter:
- def __init__(self, conn: Connection):
- self.conn = conn
-
- def write(self, s: str):
- self.conn.send(s.strip())
-
-
-@contextmanager
-def pipe_stderr(conn: Connection):
- sys.stderr = ConnWriter(conn)
-
- try:
- yield
- finally:
- sys.stderr = sys.__stderr__
diff --git a/buzz/cuda_setup.py b/buzz/cuda_setup.py
deleted file mode 100644
index d99b402e..00000000
--- a/buzz/cuda_setup.py
+++ /dev/null
@@ -1,130 +0,0 @@
-"""
-CUDA library path setup for nvidia packages installed via pip.
-
-This module must be imported BEFORE any torch or CUDA-dependent libraries are imported.
-It handles locating and loading CUDA libraries (cuDNN, cuBLAS, etc.) from the nvidia
-pip packages.
-
-On Windows: Uses os.add_dll_directory() to add library paths
-On Linux: Uses ctypes to preload libraries (LD_LIBRARY_PATH is read at process start)
-On macOS: No action needed (CUDA not supported)
-"""
-
-import ctypes
-import logging
-import os
-import platform
-import sys
-from pathlib import Path
-
-
-logger = logging.getLogger(__name__)
-
-
-def _get_nvidia_package_lib_dirs() -> list[Path]:
- """Find all nvidia package library directories in site-packages."""
- lib_dirs = []
-
- # Find site-packages directories
- site_packages_dirs = []
- for path in sys.path:
- if "site-packages" in path:
- site_packages_dirs.append(Path(path))
-
- # Also check relative to the current module for frozen apps
- if getattr(sys, "frozen", False):
- # For frozen apps, check the _internal directory
- frozen_lib_dir = Path(sys._MEIPASS) if hasattr(sys, "_MEIPASS") else Path(sys.executable).parent
- nvidia_dir = frozen_lib_dir / "nvidia"
- if nvidia_dir.exists():
- for pkg_dir in nvidia_dir.iterdir():
- if pkg_dir.is_dir():
- lib_subdir = pkg_dir / "lib"
- if lib_subdir.exists():
- lib_dirs.append(lib_subdir)
- # Some packages have bin directory on Windows
- bin_subdir = pkg_dir / "bin"
- if bin_subdir.exists():
- lib_dirs.append(bin_subdir)
-
- # Check each site-packages for nvidia packages
- for sp_dir in site_packages_dirs:
- nvidia_dir = sp_dir / "nvidia"
- if nvidia_dir.exists():
- for pkg_dir in nvidia_dir.iterdir():
- if pkg_dir.is_dir():
- lib_subdir = pkg_dir / "lib"
- if lib_subdir.exists():
- lib_dirs.append(lib_subdir)
- # Some packages have bin directory on Windows
- bin_subdir = pkg_dir / "bin"
- if bin_subdir.exists():
- lib_dirs.append(bin_subdir)
-
- return lib_dirs
-
-
-def _setup_windows_dll_directories():
- """Add nvidia library directories to Windows DLL search path."""
- lib_dirs = _get_nvidia_package_lib_dirs()
- for lib_dir in lib_dirs:
- try:
- os.add_dll_directory(str(lib_dir))
- except (OSError, AttributeError) as e:
- pass
-
-
-def _preload_linux_libraries():
- """Preload CUDA libraries on Linux using ctypes.
-
- On Linux, LD_LIBRARY_PATH is only read at process start, so we need to
- manually load the libraries using ctypes before torch tries to load them.
- """
- lib_dirs = _get_nvidia_package_lib_dirs()
-
- # Libraries to skip - NVBLAS requires special configuration and causes issues
- skip_patterns = ["libnvblas"]
-
- loaded_libs = set()
-
- for lib_dir in lib_dirs:
- if not lib_dir.exists():
- continue
-
- # Find all .so files in the directory
- for lib_file in sorted(lib_dir.glob("*.so*")):
- if lib_file.name in loaded_libs:
- continue
- if lib_file.is_symlink() and not lib_file.exists():
- continue
-
- # Skip problematic libraries
- if any(pattern in lib_file.name for pattern in skip_patterns):
- continue
-
- try:
- # Use RTLD_GLOBAL so symbols are available to other libraries
- ctypes.CDLL(str(lib_file), mode=ctypes.RTLD_GLOBAL)
- loaded_libs.add(lib_file.name)
- except OSError as e:
- # Some libraries may have missing dependencies, that's ok
- pass
-
-
-def setup_cuda_libraries():
- """Set up CUDA library paths for the current platform.
-
- This function should be called as early as possible, before any torch
- or CUDA-dependent libraries are imported.
- """
- system = platform.system()
-
- if system == "Windows":
- _setup_windows_dll_directories()
- elif system == "Linux":
- _preload_linux_libraries()
- # macOS doesn't have CUDA support, so nothing to do
-
-
-# Auto-run setup when this module is imported
-setup_cuda_libraries()
diff --git a/buzz/db/__init__.py b/buzz/db/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/buzz/db/dao/__init__.py b/buzz/db/dao/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/buzz/db/dao/dao.py b/buzz/db/dao/dao.py
deleted file mode 100644
index e173d19e..00000000
--- a/buzz/db/dao/dao.py
+++ /dev/null
@@ -1,63 +0,0 @@
-# Adapted from https://github.com/zhiyiYo/Groove
-from abc import ABC
-from typing import TypeVar, Generic, Any, Type, List
-
-from PyQt6.QtSql import QSqlDatabase, QSqlQuery, QSqlRecord
-
-from buzz.db.entity.entity import Entity
-
-T = TypeVar("T", bound=Entity)
-
-
-class DAO(ABC, Generic[T]):
- entity: Type[T]
- ignore_fields = []
-
- def __init__(self, table: str, db: QSqlDatabase):
- self.db = db
- self.table = table
-
- def insert(self, record: T):
- query = self._create_query()
- fields = [
- field for field in record.__dict__.keys() if field not in self.ignore_fields
- ]
- query.prepare(
- f"""
- INSERT INTO {self.table} ({", ".join(fields)})
- VALUES ({", ".join([f":{key}" for key in fields])})
- """
- )
- for field in fields:
- query.bindValue(f":{field}", getattr(record, field))
-
- if not query.exec():
- raise Exception(query.lastError().text())
-
- def find_by_id(self, id: Any) -> T | None:
- query = self._create_query()
- query.prepare(f"SELECT * FROM {self.table} WHERE id = :id")
- query.bindValue(":id", id)
- return self._execute(query)
-
- def to_entity(self, record: QSqlRecord) -> T:
- kwargs = {record.fieldName(i): record.value(i) for i in range(record.count())}
- return self.entity(**kwargs)
-
- def _execute(self, query: QSqlQuery) -> T | None:
- if not query.exec():
- raise Exception(query.lastError().text())
- if not query.first():
- return None
- return self.to_entity(query.record())
-
- def _execute_all(self, query: QSqlQuery) -> List[T]:
- if not query.exec():
- raise Exception(query.lastError().text())
- entities = []
- while query.next():
- entities.append(self.to_entity(query.record()))
- return entities
-
- def _create_query(self):
- return QSqlQuery(self.db)
diff --git a/buzz/db/dao/transcription_dao.py b/buzz/db/dao/transcription_dao.py
deleted file mode 100644
index db5107b4..00000000
--- a/buzz/db/dao/transcription_dao.py
+++ /dev/null
@@ -1,320 +0,0 @@
-import uuid
-from datetime import datetime
-from uuid import UUID
-
-from PyQt6.QtSql import QSqlDatabase
-
-from buzz.db.dao.dao import DAO
-from buzz.db.entity.transcription import Transcription
-from buzz.transcriber.transcriber import FileTranscriptionTask
-
-
-class TranscriptionDAO(DAO[Transcription]):
- entity = Transcription
-
- def __init__(self, db: QSqlDatabase):
- super().__init__("transcription", db)
-
- def create_transcription(self, task: FileTranscriptionTask):
- query = self._create_query()
- query.prepare(
- """
- INSERT INTO transcription (
- id,
- export_formats,
- file,
- output_folder,
- language,
- model_type,
- source,
- status,
- task,
- time_queued,
- url,
- whisper_model_size,
- hugging_face_model_id,
- word_level_timings,
- extract_speech,
- name,
- notes
- ) VALUES (
- :id,
- :export_formats,
- :file,
- :output_folder,
- :language,
- :model_type,
- :source,
- :status,
- :task,
- :time_queued,
- :url,
- :whisper_model_size,
- :hugging_face_model_id,
- :word_level_timings,
- :extract_speech,
- :name,
- :notes
- )
- """
- )
- query.bindValue(":id", str(task.uid))
- query.bindValue(
- ":export_formats",
- ", ".join(
- [
- output_format.value
- for output_format in task.file_transcription_options.output_formats
- ]
- ),
- )
- query.bindValue(":file", task.file_path)
- query.bindValue(":output_folder", task.output_directory)
- query.bindValue(":language", task.transcription_options.language)
- query.bindValue(
- ":model_type", task.transcription_options.model.model_type.value
- )
- query.bindValue(":source", task.source.value)
- query.bindValue(":status", FileTranscriptionTask.Status.QUEUED.value)
- query.bindValue(":task", task.transcription_options.task.value)
- query.bindValue(":time_queued", datetime.now().isoformat())
- query.bindValue(":url", task.url)
- query.bindValue(
- ":whisper_model_size",
- task.transcription_options.model.whisper_model_size.value
- if task.transcription_options.model.whisper_model_size
- else None,
- )
- query.bindValue(
- ":hugging_face_model_id",
- task.transcription_options.model.hugging_face_model_id
- if task.transcription_options.model.hugging_face_model_id
- else None,
- )
- query.bindValue(
- ":word_level_timings",
- task.transcription_options.word_level_timings
- )
- query.bindValue(
- ":extract_speech",
- task.transcription_options.extract_speech
- )
- query.bindValue(":name", None) # name is not available in FileTranscriptionTask
- query.bindValue(":notes", None) # notes is not available in FileTranscriptionTask
- if not query.exec():
- raise Exception(query.lastError().text())
-
- def copy_transcription(self, id: UUID) -> UUID:
- query = self._create_query()
- query.prepare("SELECT * FROM transcription WHERE id = :id")
- query.bindValue(":id", str(id))
- if not query.exec():
- raise Exception(query.lastError().text())
- if not query.next():
- raise Exception("Transcription not found")
-
- transcription_data = {field.name: query.value(field.name) for field in
- self.entity.__dataclass_fields__.values()}
-
- new_id = uuid.uuid4()
- transcription_data["id"] = str(new_id)
- transcription_data["time_queued"] = datetime.now().isoformat()
- transcription_data["status"] = FileTranscriptionTask.Status.QUEUED.value
-
- query.prepare(
- """
- INSERT INTO transcription (
- id,
- export_formats,
- file,
- output_folder,
- language,
- model_type,
- source,
- status,
- task,
- time_queued,
- url,
- whisper_model_size,
- hugging_face_model_id,
- word_level_timings,
- extract_speech,
- name,
- notes
- ) VALUES (
- :id,
- :export_formats,
- :file,
- :output_folder,
- :language,
- :model_type,
- :source,
- :status,
- :task,
- :time_queued,
- :url,
- :whisper_model_size,
- :hugging_face_model_id,
- :word_level_timings,
- :extract_speech,
- :name,
- :notes
- )
- """
- )
- for key, value in transcription_data.items():
- query.bindValue(f":{key}", value)
- if not query.exec():
- raise Exception(query.lastError().text())
-
- return new_id
-
- def update_transcription_as_started(self, id: UUID):
- query = self._create_query()
- query.prepare(
- """
- UPDATE transcription
- SET status = :status, time_started = :time_started
- WHERE id = :id
- """
- )
-
- query.bindValue(":id", str(id))
- query.bindValue(":status", FileTranscriptionTask.Status.IN_PROGRESS.value)
- query.bindValue(":time_started", datetime.now().isoformat())
- if not query.exec():
- raise Exception(query.lastError().text())
-
- def update_transcription_as_failed(self, id: UUID, error: str):
- query = self._create_query()
- query.prepare(
- """
- UPDATE transcription
- SET status = :status, time_ended = :time_ended, error_message = :error_message
- WHERE id = :id
- """
- )
-
- query.bindValue(":id", str(id))
- query.bindValue(":status", FileTranscriptionTask.Status.FAILED.value)
- query.bindValue(":time_ended", datetime.now().isoformat())
- query.bindValue(":error_message", error)
- if not query.exec():
- raise Exception(query.lastError().text())
-
- def update_transcription_as_canceled(self, id: UUID):
- query = self._create_query()
- query.prepare(
- """
- UPDATE transcription
- SET status = :status, time_ended = :time_ended
- WHERE id = :id
- """
- )
-
- query.bindValue(":id", str(id))
- query.bindValue(":status", FileTranscriptionTask.Status.CANCELED.value)
- query.bindValue(":time_ended", datetime.now().isoformat())
- if not query.exec():
- raise Exception(query.lastError().text())
-
- def update_transcription_progress(self, id: UUID, progress: float):
- query = self._create_query()
- query.prepare(
- """
- UPDATE transcription
- SET status = :status, progress = :progress
- WHERE id = :id
- """
- )
-
- query.bindValue(":id", str(id))
- query.bindValue(":status", FileTranscriptionTask.Status.IN_PROGRESS.value)
- query.bindValue(":progress", progress)
- if not query.exec():
- raise Exception(query.lastError().text())
-
- def update_transcription_as_completed(self, id: UUID):
- query = self._create_query()
- query.prepare(
- """
- UPDATE transcription
- SET status = :status, time_ended = :time_ended
- WHERE id = :id
- """
- )
-
- query.bindValue(":id", str(id))
- query.bindValue(":status", FileTranscriptionTask.Status.COMPLETED.value)
- query.bindValue(":time_ended", datetime.now().isoformat())
- if not query.exec():
- raise Exception(query.lastError().text())
-
- def update_transcription_file_and_name(self, id: UUID, file_path: str, name: str | None = None):
- query = self._create_query()
- query.prepare(
- """
- UPDATE transcription
- SET file = :file, name = COALESCE(:name, name)
- WHERE id = :id
- """
- )
-
- query.bindValue(":id", str(id))
- query.bindValue(":file", file_path)
- query.bindValue(":name", name)
- if not query.exec():
- raise Exception(query.lastError().text())
-
- def update_transcription_name(self, id: UUID, name: str):
- query = self._create_query()
- query.prepare(
- """
- UPDATE transcription
- SET name = :name
- WHERE id = :id
- """
- )
-
- query.bindValue(":id", str(id))
- query.bindValue(":name", name)
- if not query.exec():
- raise Exception(query.lastError().text())
- if query.numRowsAffected() == 0:
- raise Exception("Transcription not found")
-
- def update_transcription_notes(self, id: UUID, notes: str):
- query = self._create_query()
- query.prepare(
- """
- UPDATE transcription
- SET notes = :notes
- WHERE id = :id
- """
- )
-
- query.bindValue(":id", str(id))
- query.bindValue(":notes", notes)
- if not query.exec():
- raise Exception(query.lastError().text())
- if query.numRowsAffected() == 0:
- raise Exception("Transcription not found")
-
- def reset_transcription_for_restart(self, id: UUID):
- """Reset a transcription to queued status for restart"""
- query = self._create_query()
- query.prepare(
- """
- UPDATE transcription
- SET status = :status, progress = :progress, time_started = NULL, time_ended = NULL, error_message = NULL
- WHERE id = :id
- """
- )
-
- query.bindValue(":id", str(id))
- query.bindValue(":status", FileTranscriptionTask.Status.QUEUED.value)
- query.bindValue(":progress", 0.0)
- if not query.exec():
- raise Exception(query.lastError().text())
- if query.numRowsAffected() == 0:
- raise Exception("Transcription not found")
diff --git a/buzz/db/dao/transcription_segment_dao.py b/buzz/db/dao/transcription_segment_dao.py
deleted file mode 100644
index bb222555..00000000
--- a/buzz/db/dao/transcription_segment_dao.py
+++ /dev/null
@@ -1,53 +0,0 @@
-from typing import List
-from uuid import UUID
-
-from PyQt6.QtSql import QSqlDatabase
-
-from buzz.db.dao.dao import DAO
-from buzz.db.entity.transcription_segment import TranscriptionSegment
-
-
-class TranscriptionSegmentDAO(DAO[TranscriptionSegment]):
- entity = TranscriptionSegment
- ignore_fields = ["id"]
-
- def __init__(self, db: QSqlDatabase):
- super().__init__("transcription_segment", db)
-
- def get_segments(self, transcription_id: UUID) -> List[TranscriptionSegment]:
- query = self._create_query()
- query.prepare(
- f"""
- SELECT * FROM {self.table}
- WHERE transcription_id = :transcription_id
- """
- )
- query.bindValue(":transcription_id", str(transcription_id))
- return self._execute_all(query)
-
- def delete_segments(self, transcription_id: UUID):
- query = self._create_query()
- query.prepare(
- f"""
- DELETE FROM {self.table}
- WHERE transcription_id = :transcription_id
- """
- )
- query.bindValue(":transcription_id", str(transcription_id))
- if not query.exec():
- raise Exception(query.lastError().text())
-
- def update_segment_translation(self, segment_id: int, translation: str):
- query = self._create_query()
- query.prepare(
- """
- UPDATE transcription_segment
- SET translation = :translation
- WHERE id = :id
- """
- )
-
- query.bindValue(":id", segment_id)
- query.bindValue(":translation", translation)
- if not query.exec():
- raise Exception(query.lastError().text())
diff --git a/buzz/db/db.py b/buzz/db/db.py
deleted file mode 100644
index 0abd40f8..00000000
--- a/buzz/db/db.py
+++ /dev/null
@@ -1,52 +0,0 @@
-import logging
-import os
-import sqlite3
-import tempfile
-
-from PyQt6.QtSql import QSqlDatabase
-from platformdirs import user_data_dir
-
-from buzz.db.helpers import (
- run_sqlite_migrations,
- copy_transcriptions_from_json_to_sqlite,
- mark_in_progress_and_queued_transcriptions_as_canceled,
-)
-
-
-def setup_app_db() -> QSqlDatabase:
- data_dir = user_data_dir("Buzz")
- os.makedirs(data_dir, exist_ok=True)
- return _setup_db(os.path.join(data_dir, "Buzz.sqlite"))
-
-
-def setup_test_db() -> QSqlDatabase:
- return _setup_db(tempfile.mktemp())
-
-
-def _setup_db(path: str) -> QSqlDatabase:
- # Run migrations
- db = sqlite3.connect(path, isolation_level=None, timeout=10.0)
- try:
- run_sqlite_migrations(db)
- copy_transcriptions_from_json_to_sqlite(db)
- mark_in_progress_and_queued_transcriptions_as_canceled(db)
- db.commit()
- finally:
- db.close()
-
- db = QSqlDatabase.addDatabase("QSQLITE")
- db.setDatabaseName(path)
- if not db.open():
- raise RuntimeError(f"Failed to open database connection: {db.databaseName()}")
- db.exec('PRAGMA foreign_keys = ON')
- logging.debug("Database connection opened: %s", db.databaseName())
- return db
-
-
-def close_app_db():
- db = QSqlDatabase.database()
- if not db.isValid():
- return
-
- if db.isOpen():
- db.close()
\ No newline at end of file
diff --git a/buzz/db/entity/__init__.py b/buzz/db/entity/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/buzz/db/entity/entity.py b/buzz/db/entity/entity.py
deleted file mode 100644
index 9ad32f01..00000000
--- a/buzz/db/entity/entity.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from abc import ABC
-
-from PyQt6.QtSql import QSqlRecord
-
-
-class Entity(ABC):
- @classmethod
- def from_record(cls, record: QSqlRecord):
- entity = cls()
- for i in range(record.count()):
- setattr(entity, record.fieldName(i), record.value(i))
- return entity
diff --git a/buzz/db/entity/transcription.py b/buzz/db/entity/transcription.py
deleted file mode 100644
index ffb1b11a..00000000
--- a/buzz/db/entity/transcription.py
+++ /dev/null
@@ -1,66 +0,0 @@
-import datetime
-import os
-import uuid
-from dataclasses import dataclass, field
-
-from buzz.db.entity.entity import Entity
-from buzz.model_loader import ModelType
-from buzz.settings.settings import Settings
-from buzz.transcriber.transcriber import OutputFormat, Task, FileTranscriptionTask
-
-
-@dataclass
-class Transcription(Entity):
- status: str = FileTranscriptionTask.Status.QUEUED.value
- task: str = Task.TRANSCRIBE.value
- model_type: str = ModelType.WHISPER.value
- whisper_model_size: str | None = None
- hugging_face_model_id: str | None = None
- word_level_timings: str | None = None
- extract_speech: str | None = None
- language: str | None = None
- id: str = field(default_factory=lambda: str(uuid.uuid4()))
- error_message: str | None = None
- file: str | None = None
- time_queued: str = datetime.datetime.now().isoformat()
- progress: float = 0.0
- time_ended: str | None = None
- time_started: str | None = None
- export_formats: str | None = None
- output_folder: str | None = None
- source: str | None = None
- url: str | None = None
- name: str | None = None
- notes: str | None = None
-
- @property
- def id_as_uuid(self):
- return uuid.UUID(hex=self.id)
-
- @property
- def status_as_status(self):
- return FileTranscriptionTask.Status(self.status)
-
- def get_output_file_path(
- self,
- output_format: OutputFormat,
- output_directory: str | None = None,
- ):
- input_file_name = os.path.splitext(os.path.basename(self.file))[0]
-
- date_time_now = datetime.datetime.now().strftime("%d-%b-%Y %H-%M-%S")
-
- export_file_name_template = Settings().get_default_export_file_template()
-
- output_file_name = (
- export_file_name_template.replace("{{ input_file_name }}", input_file_name)
- .replace("{{ task }}", self.task)
- .replace("{{ language }}", self.language or "")
- .replace("{{ model_type }}", self.model_type)
- .replace("{{ model_size }}", self.whisper_model_size or "")
- .replace("{{ date_time }}", date_time_now)
- + f".{output_format.value}"
- )
-
- output_directory = output_directory or os.path.dirname(self.file)
- return os.path.join(output_directory, output_file_name)
diff --git a/buzz/db/entity/transcription_segment.py b/buzz/db/entity/transcription_segment.py
deleted file mode 100644
index 4af38a5e..00000000
--- a/buzz/db/entity/transcription_segment.py
+++ /dev/null
@@ -1,13 +0,0 @@
-from dataclasses import dataclass
-
-from buzz.db.entity.entity import Entity
-
-
-@dataclass
-class TranscriptionSegment(Entity):
- start_time: int
- end_time: int
- text: str
- translation: str
- transcription_id: str
- id: int = -1
diff --git a/buzz/db/helpers.py b/buzz/db/helpers.py
deleted file mode 100644
index 35c35757..00000000
--- a/buzz/db/helpers.py
+++ /dev/null
@@ -1,89 +0,0 @@
-import os
-from datetime import datetime
-from sqlite3 import Connection
-
-from buzz.assets import get_path
-from buzz.cache import TasksCache
-from buzz.db.migrator import dumb_migrate_db
-
-
-def copy_transcriptions_from_json_to_sqlite(conn: Connection):
- cache = TasksCache()
- if os.path.exists(cache.tasks_list_file_path):
- tasks = cache.load()
- cursor = conn.cursor()
- for task in tasks:
- cursor.execute(
- """
- INSERT INTO transcription (id, error_message, export_formats, file, output_folder, progress, language, model_type, source, status, task, time_ended, time_queued, time_started, url, whisper_model_size, hugging_face_model_id)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, COALESCE(?, ?), ?, ?, ?, ?)
- RETURNING id;
- """,
- (
- str(task.uid),
- task.error,
- ", ".join(
- [
- format.value
- for format in task.file_transcription_options.output_formats
- ]
- ),
- task.file_path,
- task.output_directory,
- task.fraction_completed,
- task.transcription_options.language,
- task.transcription_options.model.model_type.value,
- task.source.value,
- task.status.value,
- task.transcription_options.task.value,
- task.completed_at,
- task.queued_at, datetime.now().isoformat(),
- task.started_at,
- task.url,
- task.transcription_options.model.whisper_model_size.value
- if task.transcription_options.model.whisper_model_size
- else None,
- task.transcription_options.model.hugging_face_model_id
- if task.transcription_options.model.hugging_face_model_id
- else None,
- ),
- )
- transcription_id = cursor.fetchone()[0]
-
- for segment in task.segments:
- cursor.execute(
- """
- INSERT INTO transcription_segment (end_time, start_time, text, translation, transcription_id)
- VALUES (?, ?, ?, ?, ?);
- """,
- (
- segment.end,
- segment.start,
- segment.text,
- segment.translation,
- transcription_id,
- ),
- )
- # os.remove(cache.tasks_list_file_path)
- conn.commit()
-
-
-def run_sqlite_migrations(db: Connection):
- schema_path = get_path("schema.sql")
-
- with open(schema_path) as schema_file:
- schema = schema_file.read()
- dumb_migrate_db(db=db, schema=schema)
-
-
-def mark_in_progress_and_queued_transcriptions_as_canceled(conn: Connection):
- cursor = conn.cursor()
- cursor.execute(
- """
- UPDATE transcription
- SET status = 'canceled', time_ended = ?
- WHERE status = 'in_progress' OR status = 'queued';
- """,
- (datetime.now().isoformat(),),
- )
- conn.commit()
diff --git a/buzz/db/migrator.py b/buzz/db/migrator.py
deleted file mode 100644
index d36f9b34..00000000
--- a/buzz/db/migrator.py
+++ /dev/null
@@ -1,285 +0,0 @@
-# coding: utf-8
-# https://gist.github.com/simonw/664b4b0851c1899dc55e1fb655181037
-
-"""Simple declarative schema migration for SQLite.
-See .
-Author: William Manley .
-Copyright © 2019-2022 Stb-tester.com Ltd.
-License: MIT.
-"""
-
-import logging
-import re
-import sqlite3
-from textwrap import dedent
-
-
-def dumb_migrate_db(db, schema, allow_deletions=False):
- """
- Migrates a database to the new schema given by the SQL text `schema`
- preserving the data. We create any table that exists in schema, delete any
- old table that is no longer used and add/remove columns and indices as
- necessary.
- Under this scheme there are a set of changes that we can make to the schema
- and this script will handle it fine:
- 1. Adding a new table
- 2. Adding, deleting or modifying an index
- 3. Adding a column to an existing table as long as the new column can be
- NULL or has a DEFAULT value specified.
- 4. Changing a column to remove NULL or DEFAULT as long as all values in the
- database are not NULL
- 5. Changing the type of a column
- 6. Changing the user_version
- In addition this function is capable of:
- 1. Deleting tables
- 2. Deleting columns from tables
- But only if allow_deletions=True. If the new schema requires a column/table
- to be deleted and allow_deletions=False this function will raise
- `RuntimeError`.
- Note: When this function is called a transaction must not be held open on
- db. A transaction will be used internally. If you wish to perform
- additional migration steps as part of a migration use DBMigrator directly.
- Any internally generated rowid columns by SQLite may change values by this
- migration.
- """
- with DBMigrator(db, schema, allow_deletions) as migrator:
- migrator.migrate()
- return bool(migrator.n_changes)
-
-
-class DBMigrator:
- def __init__(self, db, schema, allow_deletions=False):
- self.db = db
- self.schema = schema
- self.allow_deletions = allow_deletions
-
- self.pristine = sqlite3.connect(":memory:")
- self.pristine.executescript(schema)
- self.n_changes = 0
-
- self.orig_foreign_keys = None
-
- def log_execute(self, msg, sql, args=None):
- # It's important to log any changes we're making to the database for
- # forensics later
- msg_tmpl = "Database migration: %s with SQL:\n%s"
- msg_argv = (msg, _left_pad(dedent(sql)))
- if args:
- msg_tmpl += " args = %r"
- msg_argv += (args,)
- else:
- args = []
- # Uncomment this to get debugging information
- # logging.info(msg_tmpl, *msg_argv)
- self.db.execute(sql, args)
- self.n_changes += 1
-
- def __enter__(self):
- self.orig_foreign_keys = self.db.execute("PRAGMA foreign_keys").fetchone()[0]
- if self.orig_foreign_keys:
- self.log_execute(
- "Disable foreign keys temporarily for migration",
- "PRAGMA foreign_keys = OFF",
- )
- # This doesn't count as a change because we'll undo it at the end
- self.n_changes = 0
-
- self.db.__enter__()
- self.db.execute("BEGIN")
- return self
-
- def __exit__(self, exc_type, exc_value, exc_tb):
- self.db.__exit__(exc_type, exc_value, exc_tb)
- if exc_value is None:
- # The SQLite docs say:
- #
- # > This pragma is a no-op within a transaction; foreign key
- # > constraint enforcement may only be enabled or disabled when
- # > there is no pending BEGIN or SAVEPOINT.
- old_changes = self.n_changes
- new_val = self._migrate_pragma("foreign_keys")
- if new_val == self.orig_foreign_keys:
- self.n_changes = old_changes
-
- # SQLite docs say:
- #
- # > A VACUUM will fail if there is an open transaction on the database
- # > connection that is attempting to run the VACUUM.
- if self.n_changes:
- self.db.execute("VACUUM")
- else:
- if self.orig_foreign_keys:
- self.log_execute(
- "Re-enable foreign keys after migration", "PRAGMA foreign_keys = ON"
- )
-
- def migrate(self):
- # In CI the database schema may be changing all the time. This checks
- # the current db and if it doesn't match database.sql we will
- # modify it so it does match where possible.
- pristine_tables = dict(
- self.pristine.execute(
- """\
- SELECT name, sql FROM sqlite_master
- WHERE type = \"table\" AND name != \"sqlite_sequence\""""
- ).fetchall()
- )
- pristine_indices = dict(
- self.pristine.execute(
- """\
- SELECT name, sql FROM sqlite_master
- WHERE type = \"index\""""
- ).fetchall()
- )
-
- tables = dict(
- self.db.execute(
- """\
- SELECT name, sql FROM sqlite_master
- WHERE type = \"table\" AND name != \"sqlite_sequence\""""
- ).fetchall()
- )
-
- new_tables = set(pristine_tables.keys()) - set(tables.keys())
- removed_tables = set(tables.keys()) - set(pristine_tables.keys())
- if removed_tables and not self.allow_deletions:
- raise RuntimeError(
- "Database migration: Refusing to delete tables %r" % removed_tables
- )
-
- modified_tables = set(
- name
- for name, sql in pristine_tables.items()
- if normalise_sql(tables.get(name, "")) != normalise_sql(sql)
- )
-
- # This PRAGMA is automatically disabled when the db is committed
- self.db.execute("PRAGMA defer_foreign_keys = TRUE")
-
- # New and removed tables are easy:
- for tbl_name in new_tables:
- self.log_execute("Create table %s" % tbl_name, pristine_tables[tbl_name])
- for tbl_name in removed_tables:
- self.log_execute("Drop table %s" % tbl_name, "DROP TABLE %s" % tbl_name)
-
- for tbl_name in modified_tables:
- # The SQLite documentation insists that we create the new table and
- # rename it over the old rather than moving the old out of the way
- # and then creating the new
- create_table_sql = pristine_tables[tbl_name]
- create_table_sql = re.sub(
- r"\b%s\b" % re.escape(tbl_name),
- tbl_name + "_migration_new",
- create_table_sql,
- )
- self.log_execute(
- "Columns change: Create table %s with updated schema" % tbl_name,
- create_table_sql,
- )
-
- cols = set(
- [x[1] for x in self.db.execute("PRAGMA table_info(%s)" % tbl_name)]
- )
- pristine_cols = set(
- [
- x[1]
- for x in self.pristine.execute("PRAGMA table_info(%s)" % tbl_name)
- ]
- )
-
- removed_columns = cols - pristine_cols
- if not self.allow_deletions and removed_columns:
- logging.warning(
- "Database migration: Refusing to remove columns %r from "
- "table %s. Current cols are %r attempting migration to %r",
- removed_columns,
- tbl_name,
- cols,
- pristine_cols,
- )
- raise RuntimeError(
- "Database migration: Refusing to remove columns %r from "
- "table %s" % (removed_columns, tbl_name)
- )
-
- logging.info("cols: %s, pristine_cols: %s", cols, pristine_cols)
- self.log_execute(
- "Migrate data for table %s" % tbl_name,
- """\
- INSERT INTO {tbl_name}_migration_new ({common})
- SELECT {common} FROM {tbl_name}""".format(
- tbl_name=tbl_name,
- common=", ".join(cols.intersection(pristine_cols)),
- ),
- )
-
- # Don't need the old table any more
- self.log_execute(
- "Drop old table %s now data has been migrated" % tbl_name,
- "DROP TABLE %s" % tbl_name,
- )
-
- self.log_execute(
- "Columns change: Move new table %s over old" % tbl_name,
- "ALTER TABLE %s_migration_new RENAME TO %s" % (tbl_name, tbl_name),
- )
-
- # Migrate the indices
- indices = dict(
- self.db.execute(
- """\
- SELECT name, sql FROM sqlite_master
- WHERE type = \"index\""""
- ).fetchall()
- )
- for name in set(indices.keys()) - set(pristine_indices.keys()):
- self.log_execute(
- "Dropping obsolete index %s" % name, "DROP INDEX %s" % name
- )
- for name, sql in pristine_indices.items():
- if name not in indices:
- self.log_execute("Creating new index %s" % name, sql)
- elif sql != indices[name]:
- self.log_execute(
- "Index %s changed: Dropping old version" % name,
- "DROP INDEX %s" % name,
- )
- self.log_execute(
- "Index %s changed: Creating updated version in its place" % name,
- sql,
- )
-
- self._migrate_pragma("user_version")
-
- if self.pristine.execute("PRAGMA foreign_keys").fetchone()[0]:
- if self.db.execute("PRAGMA foreign_key_check").fetchall():
- raise RuntimeError("Database migration: Would fail foreign_key_check")
-
- def _migrate_pragma(self, pragma):
- pristine_val = self.pristine.execute("PRAGMA %s" % pragma).fetchone()[0]
- val = self.db.execute("PRAGMA %s" % pragma).fetchone()[0]
-
- if val != pristine_val:
- self.log_execute(
- "Set %s to %i from %i" % (pragma, pristine_val, val),
- "PRAGMA %s = %i" % (pragma, pristine_val),
- )
-
- return pristine_val
-
-
-def _left_pad(text, indent=" "):
- """Maybe I can find a package in pypi for this?"""
- return "\n".join(indent + line for line in text.split("\n"))
-
-
-def normalise_sql(sql):
- # Remove comments:
- sql = re.sub(r"--[^\n]*\n", "", sql)
- # Normalise whitespace:
- sql = re.sub(r"\s+", " ", sql)
- sql = re.sub(r" *([(),]) *", r"\1", sql)
- # Remove unnecessary quotes
- sql = re.sub(r'"(\w+)"', r"\1", sql)
-
- return sql.strip()
diff --git a/buzz/db/service/__init__.py b/buzz/db/service/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/buzz/db/service/transcription_service.py b/buzz/db/service/transcription_service.py
deleted file mode 100644
index 8a15a24e..00000000
--- a/buzz/db/service/transcription_service.py
+++ /dev/null
@@ -1,79 +0,0 @@
-from typing import List
-from uuid import UUID
-
-from buzz.db.dao.transcription_dao import TranscriptionDAO
-from buzz.db.dao.transcription_segment_dao import TranscriptionSegmentDAO
-from buzz.db.entity.transcription_segment import TranscriptionSegment
-from buzz.transcriber.transcriber import Segment
-
-
-class TranscriptionService:
- def __init__(
- self,
- transcription_dao: TranscriptionDAO,
- transcription_segment_dao: TranscriptionSegmentDAO,
- ):
- self.transcription_dao = transcription_dao
- self.transcription_segment_dao = transcription_segment_dao
-
- def create_transcription(self, task):
- self.transcription_dao.create_transcription(task)
-
- def copy_transcription(self, id: UUID) -> UUID:
- return self.transcription_dao.copy_transcription(id)
-
- def update_transcription_as_started(self, id: UUID):
- self.transcription_dao.update_transcription_as_started(id)
-
- def update_transcription_as_failed(self, id: UUID, error: str):
- self.transcription_dao.update_transcription_as_failed(id, error)
-
- def update_transcription_as_canceled(self, id: UUID):
- self.transcription_dao.update_transcription_as_canceled(id)
-
- def update_transcription_progress(self, id: UUID, progress: float):
- self.transcription_dao.update_transcription_progress(id, progress)
-
- def update_transcription_as_completed(self, id: UUID, segments: List[Segment]):
- self.transcription_dao.update_transcription_as_completed(id)
- for segment in segments:
- self.transcription_segment_dao.insert(
- TranscriptionSegment(
- start_time=segment.start,
- end_time=segment.end,
- text=segment.text,
- translation='',
- transcription_id=str(id),
- )
- )
-
- def update_transcription_file_and_name(self, id: UUID, file_path: str, name: str | None = None):
- self.transcription_dao.update_transcription_file_and_name(id, file_path, name)
-
- def update_transcription_name(self, id: UUID, name: str):
- self.transcription_dao.update_transcription_name(id, name)
-
- def update_transcription_notes(self, id: UUID, notes: str):
- self.transcription_dao.update_transcription_notes(id, notes)
-
- def reset_transcription_for_restart(self, id: UUID):
- self.transcription_dao.reset_transcription_for_restart(id)
-
- def replace_transcription_segments(self, id: UUID, segments: List[Segment]):
- self.transcription_segment_dao.delete_segments(id)
- for segment in segments:
- self.transcription_segment_dao.insert(
- TranscriptionSegment(
- start_time=segment.start,
- end_time=segment.end,
- text=segment.text,
- translation='',
- transcription_id=str(id),
- )
- )
-
- def get_transcription_segments(self, transcription_id: UUID):
- return self.transcription_segment_dao.get_segments(transcription_id)
-
- def update_segment_translation(self, segment_id: int, translation: str):
- return self.transcription_segment_dao.update_segment_translation(segment_id, translation)
diff --git a/buzz/dialogs.py b/buzz/dialogs.py
deleted file mode 100644
index 5f9354e9..00000000
--- a/buzz/dialogs.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from PyQt6.QtWidgets import QWidget, QMessageBox
-
-
-def show_model_download_error_dialog(parent: QWidget, error: str):
- message = (
- parent.tr("An error occurred while loading the Whisper model")
- + f": {error}{'' if error.endswith('.') else '.'}"
- + parent.tr("Please retry or check the application logs for more information.")
- )
-
- QMessageBox.critical(parent, "", message)
diff --git a/buzz/file_transcriber_queue_worker.py b/buzz/file_transcriber_queue_worker.py
deleted file mode 100644
index 8e5cf1a8..00000000
--- a/buzz/file_transcriber_queue_worker.py
+++ /dev/null
@@ -1,282 +0,0 @@
-import logging
-import multiprocessing
-import os
-import queue
-import ssl
-import sys
-from pathlib import Path
-from typing import Optional, Tuple, List, Set
-from uuid import UUID
-
-# Fix SSL certificate verification for bundled applications (macOS, Windows)
-# This must be done before importing demucs which uses torch.hub with urllib
-try:
- import certifi
- os.environ.setdefault('REQUESTS_CA_BUNDLE', certifi.where())
- os.environ.setdefault('SSL_CERT_FILE', certifi.where())
- os.environ.setdefault('SSL_CERT_DIR', os.path.dirname(certifi.where()))
- # Also update the default SSL context for urllib
- ssl._create_default_https_context = lambda: ssl.create_default_context(cafile=certifi.where())
-except ImportError:
- pass
-
-from PyQt6.QtCore import QObject, QThread, pyqtSignal, pyqtSlot, Qt
-
-# Patch subprocess for demucs to prevent console windows on Windows
-if sys.platform == "win32":
- import subprocess
- _original_run = subprocess.run
- _original_check_output = subprocess.check_output
-
- def _patched_run(*args, **kwargs):
- if 'startupinfo' not in kwargs:
- si = subprocess.STARTUPINFO()
- si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
- si.wShowWindow = subprocess.SW_HIDE
- kwargs['startupinfo'] = si
- if 'creationflags' not in kwargs:
- kwargs['creationflags'] = subprocess.CREATE_NO_WINDOW
- return _original_run(*args, **kwargs)
-
- def _patched_check_output(*args, **kwargs):
- if 'startupinfo' not in kwargs:
- si = subprocess.STARTUPINFO()
- si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
- si.wShowWindow = subprocess.SW_HIDE
- kwargs['startupinfo'] = si
- if 'creationflags' not in kwargs:
- kwargs['creationflags'] = subprocess.CREATE_NO_WINDOW
- return _original_check_output(*args, **kwargs)
-
- subprocess.run = _patched_run
- subprocess.check_output = _patched_check_output
-
-from demucs import api as demucsApi
-
-from buzz.locale import _
-from buzz.model_loader import ModelType
-from buzz.transcriber.file_transcriber import FileTranscriber
-from buzz.transcriber.openai_whisper_api_file_transcriber import (
- OpenAIWhisperAPIFileTranscriber,
-)
-from buzz.transcriber.transcriber import FileTranscriptionTask, Segment
-from buzz.transcriber.whisper_file_transcriber import WhisperFileTranscriber
-
-
-class FileTranscriberQueueWorker(QObject):
- tasks_queue: multiprocessing.Queue
- current_task: Optional[FileTranscriptionTask] = None
- current_transcriber: Optional[FileTranscriber] = None
- current_transcriber_thread: Optional[QThread] = None
-
- task_started = pyqtSignal(FileTranscriptionTask)
- task_progress = pyqtSignal(FileTranscriptionTask, float)
- task_download_progress = pyqtSignal(FileTranscriptionTask, float)
- task_completed = pyqtSignal(FileTranscriptionTask, list)
- task_error = pyqtSignal(FileTranscriptionTask, str)
-
- completed = pyqtSignal()
- trigger_run = pyqtSignal()
-
- def __init__(self, parent: Optional[QObject] = None):
- super().__init__(parent)
- self.tasks_queue = queue.Queue()
- self.canceled_tasks: Set[UUID] = set()
- self.current_transcriber = None
- self.speech_path = None
- self.is_running = False
- # Use QueuedConnection to ensure run() is called in the correct thread context
- # and doesn't block signal handlers
- self.trigger_run.connect(self.run, Qt.ConnectionType.QueuedConnection)
-
- @pyqtSlot()
- def run(self):
- if self.is_running:
- return
-
- logging.debug("Waiting for next transcription task")
-
- # Clean up of previous run.
- if self.current_transcriber is not None:
- self.current_transcriber.stop()
- self.current_transcriber = None
-
- # Get next non-canceled task from queue
- while True:
- self.current_task: Optional[FileTranscriptionTask] = self.tasks_queue.get()
-
- # Stop listening when a "None" task is received
- if self.current_task is None:
- self.is_running = False
- self.completed.emit()
- return
-
- if self.current_task.uid in self.canceled_tasks:
- continue
-
- break
-
- # Set is_running AFTER we have a valid task to process
- self.is_running = True
-
- if self.current_task.transcription_options.extract_speech:
- logging.debug("Will extract speech")
-
- def separator_progress_callback(progress):
- self.task_progress.emit(self.current_task, int(progress["segment_offset"] * 100) / int(progress["audio_length"] * 100))
-
- separator = None
- separated = None
- try:
- # Force CPU if specified, otherwise use CUDA if available
- force_cpu = os.getenv("BUZZ_FORCE_CPU", "false").lower() == "true"
- if force_cpu:
- device = "cpu"
- else:
- import torch
- device = "cuda" if torch.cuda.is_available() else "cpu"
- separator = demucsApi.Separator(
- device=device,
- progress=True,
- callback=separator_progress_callback,
- )
- _origin, separated = separator.separate_audio_file(Path(self.current_task.file_path))
-
- task_file_path = Path(self.current_task.file_path)
- self.speech_path = task_file_path.with_name(f"{task_file_path.stem}_speech.mp3")
- demucsApi.save_audio(separated["vocals"], self.speech_path, separator.samplerate)
-
- self.current_task.file_path = str(self.speech_path)
- except Exception as e:
- logging.error(f"Error during speech extraction: {e}", exc_info=True)
- self.task_error.emit(
- self.current_task,
- _("Speech extraction failed! Check your internet connection — a model may need to be downloaded."),
- )
- self.is_running = False
- return
- finally:
- # Release memory used by speech extractor
- del separator, separated
- try:
- import torch
- if torch.cuda.is_available():
- torch.cuda.empty_cache()
- except Exception:
- pass
-
- logging.debug("Starting next transcription task")
- self.task_progress.emit(self.current_task, 0)
-
- model_type = self.current_task.transcription_options.model.model_type
- if model_type == ModelType.OPEN_AI_WHISPER_API:
- self.current_transcriber = OpenAIWhisperAPIFileTranscriber(
- task=self.current_task
- )
- elif (
- model_type == ModelType.WHISPER_CPP
- or model_type == ModelType.HUGGING_FACE
- or model_type == ModelType.WHISPER
- or model_type == ModelType.FASTER_WHISPER
- ):
- self.current_transcriber = WhisperFileTranscriber(task=self.current_task)
- else:
- raise Exception(f"Unknown model type: {model_type}")
-
- self.current_transcriber_thread = QThread(self)
-
- self.current_transcriber.moveToThread(self.current_transcriber_thread)
-
- self.current_transcriber_thread.started.connect(self.current_transcriber.run)
- self.current_transcriber.completed.connect(self.current_transcriber_thread.quit)
- self.current_transcriber.error.connect(self.current_transcriber_thread.quit)
-
- self.current_transcriber.completed.connect(self.current_transcriber.deleteLater)
- self.current_transcriber.error.connect(self.current_transcriber.deleteLater)
- self.current_transcriber_thread.finished.connect(
- self.current_transcriber_thread.deleteLater
- )
-
- self.current_transcriber.progress.connect(self.on_task_progress)
- self.current_transcriber.download_progress.connect(
- self.on_task_download_progress
- )
- self.current_transcriber.error.connect(self.on_task_error)
-
- self.current_transcriber.completed.connect(self.on_task_completed)
-
- # Wait for next item on the queue
- self.current_transcriber.error.connect(lambda: self._on_task_finished())
- self.current_transcriber.completed.connect(lambda: self._on_task_finished())
-
- self.task_started.emit(self.current_task)
- self.current_transcriber_thread.start()
-
- def _on_task_finished(self):
- """Called when a task completes or errors, resets state and triggers next run"""
- self.is_running = False
- # Use signal to avoid blocking in signal handler context
- self.trigger_run.emit()
-
- def add_task(self, task: FileTranscriptionTask):
- # Remove from canceled tasks if it was previously canceled (for restart functionality)
- if task.uid in self.canceled_tasks:
- self.canceled_tasks.remove(task.uid)
-
- self.tasks_queue.put(task)
- # If the worker is not currently running, trigger it to start processing
- # Use signal to avoid blocking the main thread
- if not self.is_running:
- self.trigger_run.emit()
-
- def cancel_task(self, task_id: UUID):
- self.canceled_tasks.add(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(5000):
- logging.warning("Transcriber thread did not terminate gracefully")
- self.current_transcriber_thread.terminate()
-
- def on_task_error(self, error: str):
- if (
- self.current_task is not None
- and self.current_task.uid not in self.canceled_tasks
- ):
- # Check if the error indicates cancellation
- if "canceled" in error.lower() or "cancelled" in error.lower():
- self.current_task.status = FileTranscriptionTask.Status.CANCELED
- self.current_task.error = error
- else:
- self.current_task.status = FileTranscriptionTask.Status.FAILED
- self.current_task.error = error
- self.task_error.emit(self.current_task, error)
-
- @pyqtSlot(tuple)
- def on_task_progress(self, progress: Tuple[int, int]):
- if self.current_task is not None:
- self.task_progress.emit(self.current_task, progress[0] / progress[1])
-
- def on_task_download_progress(self, fraction_downloaded: float):
- if self.current_task is not None:
- self.task_download_progress.emit(self.current_task, fraction_downloaded)
-
- @pyqtSlot(list)
- def on_task_completed(self, segments: List[Segment]):
- if self.current_task is not None:
- self.task_completed.emit(self.current_task, segments)
-
- if self.speech_path is not None:
- try:
- Path(self.speech_path).unlink()
- except Exception:
- pass
- self.speech_path = None
-
- def stop(self):
- self.tasks_queue.put(None)
- if self.current_transcriber is not None:
- self.current_transcriber.stop()
diff --git a/buzz/locale.py b/buzz/locale.py
deleted file mode 100644
index 0ba15e39..00000000
--- a/buzz/locale.py
+++ /dev/null
@@ -1,23 +0,0 @@
-import os
-import logging
-import gettext
-
-from PyQt6.QtCore import QLocale
-
-from buzz.assets import get_path
-from buzz.settings.settings import APP_NAME, Settings
-
-locale_dir = get_path("locale")
-gettext.bindtextdomain("buzz", locale_dir)
-
-settings = Settings()
-
-languages = [
- settings.value(settings.Key.UI_LOCALE, QLocale().name())
-]
-
-translate = gettext.translation(
- APP_NAME.lower(), locale_dir, languages=languages, fallback=True
-)
-
-_ = translate.gettext
\ No newline at end of file
diff --git a/buzz/locale/ca_ES/LC_MESSAGES/buzz.po b/buzz/locale/ca_ES/LC_MESSAGES/buzz.po
deleted file mode 100644
index 8d989312..00000000
--- a/buzz/locale/ca_ES/LC_MESSAGES/buzz.po
+++ /dev/null
@@ -1,1688 +0,0 @@
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
-# This file is distributed under the same license as the PACKAGE package.
-# Jordi Mas i Hernàndez , 2022, 2023, 2024
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: buzz\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2026-03-07 20:20+0200\n"
-"PO-Revision-Date: 2025-10-17 07:59+0200\n"
-"Last-Translator: Éric Duarte \n"
-"Language-Team: Catalan \n"
-"Language: ca\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Generator: Poedit 3.7\n"
-
-#: buzz/widgets/import_url_dialog.py buzz/settings/shortcut.py
-msgid "Import URL"
-msgstr "URL d'importació"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "https://example.com/audio.mp3"
-msgstr "https://exemple.com/audio.mp3"
-
-#: buzz/widgets/import_url_dialog.py
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-#: buzz/widgets/main_window.py
-msgid "Ok"
-msgstr "D’acord"
-
-#: buzz/widgets/import_url_dialog.py
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-#: buzz/widgets/model_download_progress_dialog.py buzz/widgets/main_window.py
-msgid "Cancel"
-msgstr "Cancel·lar"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "URL:"
-msgstr "URL:"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "Invalid URL"
-msgstr "URL no vàlida"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "The URL you entered is invalid."
-msgstr "L'URL que heu introduït no és vàlid."
-
-#: buzz/widgets/presentation_window.py
-msgid "Live Transcript Presentation"
-msgstr "Presentació de transcripció en directe"
-
-#: buzz/widgets/preferences_dialog/shortcuts_editor_preferences_widget.py
-msgid "Reset to Defaults"
-msgstr "Restableix als valors predeterminats"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "English"
-msgstr "Anglès"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Catalan"
-msgstr "Català"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Danish"
-msgstr "Danès"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Dutch"
-msgstr "Holandès"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "German"
-msgstr "Alemany"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Spanish"
-msgstr "Castellà"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Italian"
-msgstr "Italià"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Japanese"
-msgstr "Japonès"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Latvian"
-msgstr "Letó"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Polish"
-msgstr "Polonès"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Portuguese (Brazil)"
-msgstr "Portuguès (Brasil)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Ukrainian"
-msgstr "Ucraïnès"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Chinese (Simplified)"
-msgstr "Xinès (simplificat)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Chinese (Traditional)"
-msgstr "Xinès (Tradicional)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Restart required!"
-msgstr "Cal reiniciar!"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Ui Language"
-msgstr "Idioma UI"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Font Size"
-msgstr "Mida de la lletra"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Test"
-msgstr "Prova"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API key"
-msgstr "Clau de l'API d'OpenAI"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI base url"
-msgstr "URL base d'OpenAI"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API model"
-msgstr "Model de l'API d'OpenAI"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Default export file name"
-msgstr "Nom del fitxer d'exportació per defecte"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Enable live recording transcription export"
-msgstr "Activa l'exportació de transcripcions en directe"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Browse"
-msgstr "Navega"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Export folder"
-msgstr "Exporta la carpeta"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Live recording mode"
-msgstr "Mode d'enregistrament en directe"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Note: Live recording export settings will be moved to the Advanced Settings "
-"in the Live Recording screen in a future version."
-msgstr ""
-"Nota: La configuració d'exportació d'enregistrament en directe es mourà a la "
-"Configuració avançada a la pantalla d'enregistrament en directe en una "
-"versió futura."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Use 8-bit quantization to reduce memory usage"
-msgstr "Utilitza la quantització de 8 bits per reduir l'ús de memòria"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Applies to Huggingface and Faster Whisper models. Reduces GPU memory usage "
-"but may slightly decrease transcription quality."
-msgstr ""
-"S'aplica als models Huggingface i Faster Whisper. Redueix l'ús de memòria de "
-"la GPU, però pot reduir lleugerament la qualitat de la transcripció."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Reduce GPU RAM"
-msgstr "Redueix la RAM de la GPU"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Use only CPU and disable GPU acceleration"
-msgstr "Utilitza només la CPU i desactiveu l'acceleració de la GPU"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Set this if larger models do not fit your GPU memory and Buzz crashes"
-msgstr ""
-"Establiu això si els models més grans no s'ajusten a la memòria de la GPU i "
-"Buzz es bloqueja"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Disable GPU"
-msgstr "Desactiva la GPU"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API Key Test"
-msgstr "Prova de clau OpenAI API"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Your API key is valid. Buzz will use this key to perform Whisper API "
-"transcriptions and AI translations."
-msgstr ""
-"La vostra clau API és vàlida. Buzz utilitzarà aquesta clau per realitzar "
-"transcripcions de l'API de Whisper i traduccions de la IA."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Invalid API key"
-msgstr "Clau API no vàlida"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"API supports only base64 characters (A-Za-z0-9+/=_-). Other characters in "
-"API key may cause errors."
-msgstr ""
-"L'API només admet caràcters base64 (A-Za-z0-9+/).-). Altres caràcters de la "
-"clau API poden causar errors."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Select Export Folder"
-msgstr "Selecciona la carpeta d'exportació"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"OpenAI API returned invalid response. Please check the API url or your key. "
-"Transcription and translation may still work if the API does not support key "
-"validation."
-msgstr ""
-"L'API d'OpenAI ha retornat una resposta no vàlida. Comproveu l'URL de l'API "
-"o la vostra clau. La transcripció i la traducció encara poden funcionar si "
-"l'API no admet la validació de claus."
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Enable folder watch"
-msgstr "Habilita el seguiment de carpetes"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Delete processed files"
-msgstr "Eliminar fitxers processats"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Input folder"
-msgstr "Carpeta d'entrada"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Output folder"
-msgstr "Carpeta de sortida"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Select Input Folder"
-msgstr "Selecciona la carpeta d'entrada"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Select Output Folder"
-msgstr "Selecciona la carpeta de sortida"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Preferences"
-msgstr "Preferències"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "General"
-msgstr "General"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Models"
-msgstr "Models"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Shortcuts"
-msgstr "Dreceres"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Folder Watch"
-msgstr "Vigila la carpeta"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Group"
-msgstr "Grup"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Huggingface ID of a Faster whisper model"
-msgstr "ID de la cara oculta d'un model de whisper més ràpid"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download"
-msgstr "Descàrrega"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Show file location"
-msgstr "Mostra la ubicació del fitxer"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Delete"
-msgstr "Suprimeix"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Downloaded"
-msgstr "Descarregat"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Available for Download"
-msgstr "Disponible per descarregar"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download link to Whisper.cpp ggml model file"
-msgstr "Enllaç de descàrrega a Whisper.cpp fitxer de model ggml"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Delete Model"
-msgstr "Suprimeix el model"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Are you sure you want to delete the selected model?"
-msgstr "Esteu segur que voleu suprimir el model seleccionat?"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download failed"
-msgstr "Descàrrega fallida"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcription_tasks_table_widget.py
-#: buzz/widgets/update_dialog.py buzz/widgets/main_window.py
-#: buzz/model_loader.py
-msgid "Error"
-msgstr "Error"
-
-#: buzz/widgets/record_button.py buzz/widgets/main_window_toolbar.py
-msgid "Record"
-msgstr "Enregistra"
-
-#: buzz/widgets/record_button.py
-msgid "Stop"
-msgstr "Atura"
-
-#: buzz/widgets/transcriber/languages_combo_box.py
-#: buzz/transcriber/transcriber.py
-msgid "Detect Language"
-msgstr "Detecta l'idioma"
-
-#: buzz/widgets/transcriber/mms_language_line_edit.py
-msgid "e.g., eng, fra, deu"
-msgstr "p. ex., eng, fra, deu"
-
-#: buzz/widgets/transcriber/mms_language_line_edit.py
-msgid ""
-"Enter an ISO 639-3 language code (3 letters).\n"
-"Examples: eng (English), fra (French), deu (German),\n"
-"spa (Spanish), lav (Latvian)"
-msgstr ""
-"Introduïu un codi d'idioma ISO 639-3 (3 lletres).\n"
-"Exemples: eng (anglès), fra (francès), deu (alemany),\n"
-"spa (castellà), lav (letó)"
-
-#: buzz/widgets/transcriber/file_transcriber_widget.py
-msgid "Run"
-msgstr "Executa"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Model:"
-msgstr "Model:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-#: buzz/transcriber/recording_transcriber.py
-msgid "First time use of a model may take up to several minutest to load."
-msgstr ""
-"L'ús per primera vegada d'un model pot trigar diversos minuts a carregar-se."
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Api Key:"
-msgstr "Clau API:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Task:"
-msgstr "Tasca:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Language:"
-msgstr "Idioma:"
-
-#: buzz/widgets/transcriber/initial_prompt_text_edit.py
-msgid "Enter prompt..."
-msgstr "Introduïu el prompt..."
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Advanced Settings"
-msgstr "Configuració avançada"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Speech recognition settings"
-msgstr "Configuració del reconeixement de veu"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Initial Prompt:"
-msgstr "Pregunta inicial:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Translation settings"
-msgstr "Configuració de la traducció"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Enable AI translation"
-msgstr "Habilita la traducció de la IA"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "AI model:"
-msgstr "Model d'IA:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid ""
-"Please translate each text sent to you from English to Spanish. Translation "
-"will be used in an automated system, please do not add any comments or "
-"notes, just the translation."
-msgstr ""
-"Si us plau, traduïu cada text que us enviï de l'anglès al castellà. La "
-"traducció s'utilitzarà en un sistema automatitzat; si us plau, no afegiu cap "
-"comentari ni nota, només la traducció."
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Instructions for AI:"
-msgstr "Instruccions per a la IA:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Recording settings"
-msgstr "Configuració d'enregistrament"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Silence threshold:"
-msgstr "Llindar de silenci:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Live recording mode:"
-msgstr "Mode d'enregistrament en directe:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Line separator:"
-msgstr "Separador de línies:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Transcription step:"
-msgstr "Pas de transcripció:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Hide unconfirmed"
-msgstr "Amaga el no confirmat"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Enable live recording export"
-msgstr "Activa l'exportació d'enregistrament en directe"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export folder:"
-msgstr "Carpeta d'exportació:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export file name:"
-msgstr "Nom del fitxer d'exportació:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Text file (.txt)"
-msgstr "Fitxer de text (.txt)"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "CSV (.csv)"
-msgstr "CSV (.csv)"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export file type:"
-msgstr "Tipus de fitxer d'exportació:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid ""
-"Limit export entries\n"
-"(0 = export all):"
-msgstr ""
-"Limita les entrades d'exportació\n"
-"(0 = exporta tot):"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Word-level timings"
-msgstr "Temps amb granularitat de paraula"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Extract speech"
-msgstr "Extreu la veu"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Export:"
-msgstr "Exporta:"
-
-#: buzz/widgets/transcriber/hugging_face_search_line_edit.py
-msgid "Huggingface ID of a model"
-msgstr "ID de la cara oculta d'un model"
-
-#: buzz/widgets/transcriber/advanced_settings_button.py
-msgid "Advanced..."
-msgstr "Avançat..."
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "New File Transcription"
-msgstr "Nova transcripció de fitxers"
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "New URL Transcription"
-msgstr "Nova transcripció d'URL"
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "Open Transcript"
-msgstr "Obre una transcripció"
-
-#: buzz/widgets/main_window_toolbar.py buzz/settings/shortcut.py
-msgid "Cancel Transcription"
-msgstr "Cancel·la la transcripció"
-
-#: buzz/widgets/main_window_toolbar.py buzz/widgets/main_window.py
-#: buzz/settings/shortcut.py
-msgid "Clear History"
-msgstr "Neteja l'historial"
-
-#: buzz/widgets/main_window_toolbar.py buzz/widgets/update_dialog.py
-msgid "Update Available"
-msgstr "Actualització disponible"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "In Progress"
-msgstr "En progrés"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Completed"
-msgstr "Completat"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Failed"
-msgstr "Ha fallat"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Canceled"
-msgstr "Cancel·lat"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Queued"
-msgstr "A la cua"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "File Name / URL"
-msgstr "Nom del fitxer / URL"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Model"
-msgstr "Model"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Task"
-msgstr "Tasca"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Status"
-msgstr "Estat"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Date Completed"
-msgstr "Data de finalització"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Date Added"
-msgstr "Data d'addició"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Notes"
-msgstr "Notes"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Reset Column Order"
-msgstr "Restableix l'ordre de les columnes"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Restart Transcription"
-msgstr "Reinicia la transcripció"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Rename"
-msgstr "Canvia el nom"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Add/Edit Notes"
-msgstr "Afegeix/Edita notes"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Rename Transcription"
-msgstr "Canvia el nom de la transcripció"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Enter new name:"
-msgstr "Introduïu el nou nom:"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Enter some relevant notes for this transcription:"
-msgstr "Introduïu algunes notes rellevants per a aquesta transcripció:"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Cannot Restart"
-msgstr "No es pot reiniciar"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Only failed or canceled transcriptions can be restarted."
-msgstr "Només es poden reiniciar les transcripcions fallides o cancel·lades."
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Failed to restart transcription: {}"
-msgstr "No s'ha pogut reiniciar la transcripció: {}"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid ""
-"Could not restart transcription: model not available and could not be "
-"downloaded."
-msgstr ""
-"No s'ha pogut reiniciar la transcripció: el model no està disponible i no "
-"s'ha pogut descarregar."
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Could not restart transcription: transcriber worker not found."
-msgstr ""
-"No s'ha pogut reiniciar la transcripció: no s'ha trobat el treballador del "
-"transcriptor."
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Live Recording"
-msgstr "Enregistrament en directe"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Click Record to begin..."
-msgstr "Feu clic a Enregistra per a començar..."
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Waiting for AI translation..."
-msgstr "Esperant la traducció de la IA..."
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Microphone:"
-msgstr "Micròfon:"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Show in new window"
-msgstr "Mostra en una nova finestra"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Text Size:"
-msgstr "Mida del text:"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Theme"
-msgstr "Tema"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Light"
-msgstr "Clar"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Dark"
-msgstr "Fosc"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Custom"
-msgstr "Personalitzat"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Text Color"
-msgstr "Color del text"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Background Color"
-msgstr "Color de fons"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Fullscreen"
-msgstr "Pantalla completa"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy"
-msgstr "Copia"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy transcription to clipboard"
-msgstr "Copia la transcripció al porta-retalls"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Nothing to copy!"
-msgstr "Res per copiar!"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy failed"
-msgstr "Ha fallat la còpia"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copied!"
-msgstr "Copiat!"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Select Text Color"
-msgstr "Selecciona el color del text"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Select Background Color"
-msgstr "Selecciona el color de fons"
-
-#: buzz/widgets/recording_transcriber_widget.py
-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
-msgid ""
-"Please check your audio devices or check the application logs for more "
-"information."
-msgstr ""
-"Comproveu els vostres dispositius d'àudio o els registres de l'aplicació per "
-"a més informació."
-
-#: buzz/widgets/update_dialog.py
-msgid "A new version of Buzz is available!"
-msgstr "Hi ha una nova versió de Buzz disponible!"
-
-#: buzz/widgets/update_dialog.py
-msgid "Current version:"
-msgstr "Versió actual:"
-
-#: buzz/widgets/update_dialog.py
-msgid "New version:"
-msgstr "Nova versió:"
-
-#: buzz/widgets/update_dialog.py
-msgid "Release Notes:"
-msgstr "Notes de la versió:"
-
-#: buzz/widgets/update_dialog.py
-msgid "Download and Install"
-msgstr "Descarrega i instal·la"
-
-#: buzz/widgets/update_dialog.py
-msgid "No download URL available for your platform."
-msgstr "No hi ha cap URL de descàrrega disponible per a la vostra plataforma."
-
-#: buzz/widgets/update_dialog.py
-msgid "Downloading file {} of {}..."
-msgstr "S'està descarregant el fitxer {} de {}..."
-
-#: buzz/widgets/update_dialog.py
-msgid "Downloading file {} of {} ({:.1f} MB / {:.1f} MB)..."
-msgstr "S'està descarregant el fitxer {} de {} ({:.1f} MB / {:.1f} MB)..."
-
-#: buzz/widgets/update_dialog.py
-msgid "Download Failed"
-msgstr "Descàrrega fallida"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to download the update: {}"
-msgstr "No s'ha pogut descarregar l'actualització: {}"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to save the installer: {}"
-msgstr "No s'ha pogut desar l'instal·lador: {}"
-
-#: buzz/widgets/update_dialog.py
-msgid "Download complete!"
-msgstr "Descàrrega completada!"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to run the installer: {}"
-msgstr "No s'ha pogut executar l'instal·lador: {}"
-
-#: buzz/widgets/about_dialog.py
-msgid "Check for updates"
-msgstr "Comprova si hi ha actualitzacions"
-
-#: buzz/widgets/about_dialog.py
-msgid "Show logs"
-msgstr "Mostra els registres"
-
-#: buzz/widgets/about_dialog.py
-msgid "You're up to date!"
-msgstr "Estàs al dia!"
-
-#: buzz/widgets/audio_meter_widget.py
-msgid "Average volume"
-msgstr "Volum mitjà"
-
-#: buzz/widgets/audio_meter_widget.py
-msgid "Queue"
-msgstr "Cua"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-msgid "Start"
-msgstr "Inicia"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-msgid "End"
-msgstr "Finalitza"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Text"
-msgstr "Text"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Translation"
-msgstr "Traducció"
-
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-msgid "View"
-msgstr "Veure"
-
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-msgid "Timestamps"
-msgstr "Marqua de temps"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Export"
-msgstr "Exporta"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Translate"
-msgstr "Traduir"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Resize"
-msgstr "Redimensionar"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Identify Speakers"
-msgstr "Identifica els parlants"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Find"
-msgstr "Cerca"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Show/Hide Search Bar (Ctrl+F)"
-msgstr "Mostra/amaga la barra de cerca (Ctrl+F)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Find:"
-msgstr "Cerca:"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Enter text to find..."
-msgstr "Introduïu el text a cercar..."
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Previous match (Shift+Enter)"
-msgstr "Coincidència anterior (Maj+Retorn)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Next match (Ctrl+Enter)"
-msgstr "Coincidència següent (Ctrl+Enter)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Clear"
-msgstr "Neteja"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Playback Controls:"
-msgstr "Controls de reproducció:"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Loop Segment"
-msgstr "Segment de bucle"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Enable/disable looping when clicking on transcript segments"
-msgstr "Activa/desactiva el bucle en fer clic als segments de transcripció"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Follow Audio"
-msgstr "Segueix l'àudio"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid ""
-"Enable/disable following the current audio position in the transcript. When "
-"enabled, automatically scrolls to current text."
-msgstr ""
-"Activa/desactiva seguint la posició d'àudio actual a la transcripció. Quan "
-"està activada, es desplaça automàticament al text actual."
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Scroll to Current"
-msgstr "Desplaça't fins a l'actual"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Scroll to the currently spoken text"
-msgstr "Desplaçar-se fins al text que es parla actualment"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "1 of 100+ matches"
-msgstr "1 de més de 100 coincidències"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "1 of "
-msgstr "1 de "
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " matches"
-msgstr " coincidències"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "No matches found"
-msgstr "No s'ha trobat cap coincidència"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " of 100+ matches"
-msgstr " de més de 100 coincidències"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " of "
-msgstr " de "
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "API Key Required"
-msgstr "Clau API necessària"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Please enter OpenAI API Key in preferences"
-msgstr "Introduïu la clau API d'OpenAI a les preferències"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend end time"
-msgstr "Amplia el temps final"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend endings by up to (seconds)"
-msgstr "Amplia els finals fins a (segons)"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend endings"
-msgstr "Amplia els finals"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Resize Options"
-msgstr "Opcions de redimensionament"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Desired subtitle length"
-msgstr "Longitud desitjada dels subtítols"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Available only if word level timings were disabled during transcription"
-msgstr ""
-"Disponible només si els temps a nivell de paraula estaven desactivats durant "
-"la transcripció"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge Options"
-msgstr "Opcions de fusió"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge by gap"
-msgstr "Fusiona per buit"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Split by punctuation"
-msgstr "Divideix per puntuació"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Split by max length"
-msgstr "Divideix per la longitud màxima"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge"
-msgstr "Fusiona"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Available only if word level timings were enabled during transcription"
-msgstr ""
-"Disponible només si els temps a nivell de paraula estaven activats durant la "
-"transcripció"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid ""
-"Speaker identification is not available: failed to load required libraries."
-msgstr ""
-"La identificació de parlants no està disponible: no s'han pogut carregar les "
-"biblioteques necessàries."
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "1/8 Collecting transcripts"
-msgstr "1/8 Recollint transcripcions"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "2/8 Loading audio"
-msgstr "2/8 Carregant àudio"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "3/8 Loading alignment model"
-msgstr "3/8 Carregant el model d'alineació"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "3/8 Loading alignment model (retrying with cache...)"
-msgstr ""
-"3/8 Carregant el model d'alineació (tornant a intentar amb la memòria cau...)"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid ""
-"Failed to load alignment model. Please check your internet connection and "
-"try again."
-msgstr ""
-"No s'ha pogut carregar el model d'alineació. Comproveu la vostra connexió a "
-"Internet i torneu-ho a intentar."
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "4/8 Processing audio"
-msgstr "4/8 Processant àudio"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "5/8 Preparing transcripts"
-msgstr "5/8 Preparant transcripcions"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "6/8 Identifying speakers"
-msgstr "6/8 Identificant parlants"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "7/8 Mapping speakers to transcripts"
-msgstr "7/8 Assignant parlants a transcripcions"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "8/8 Identification done"
-msgstr "8/8 Identificació completada"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "0/0 Error identifying speakers"
-msgstr "0/0 Error en identificar parlants"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Step 1: Identify speakers"
-msgstr "Pas 1: Identifica els parlants"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Identify"
-msgstr "Identifica"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Ready to identify speakers"
-msgstr "Llest per identificar parlants"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Audio file not found"
-msgstr "Fitxer d'àudio no trobat"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Step 2: Name speakers"
-msgstr "Pas 2: Anomena els parlants"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Play sample"
-msgstr "Reprodueix la mostra"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Merge speaker sentences"
-msgstr "Fusiona les frases del parlant"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Save"
-msgstr "Desa"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Cancelling..."
-msgstr "Cancel·lant..."
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Cancelled"
-msgstr "Cancel·lat"
-
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Save File"
-msgstr "Desa el fitxer"
-
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Text files"
-msgstr "Fitxers de text"
-
-#: buzz/widgets/model_download_progress_dialog.py
-msgid "Downloading model"
-msgstr "Descarregant el model"
-
-#: buzz/widgets/model_download_progress_dialog.py
-msgid "remaining"
-msgstr "restant"
-
-#: buzz/widgets/menu_bar.py
-msgid "Import File..."
-msgstr "Importa fitxer..."
-
-#: buzz/widgets/menu_bar.py
-msgid "Import URL..."
-msgstr "Importa l'URL..."
-
-#: buzz/widgets/menu_bar.py
-msgid "Import Folder..."
-msgstr "Importa carpeta..."
-
-#: buzz/widgets/menu_bar.py
-msgid "About"
-msgstr "Quant a"
-
-#: buzz/widgets/menu_bar.py
-msgid "Preferences..."
-msgstr "Preferències..."
-
-#: buzz/widgets/menu_bar.py
-msgid "Help"
-msgstr "Ajuda"
-
-#: buzz/widgets/menu_bar.py
-msgid "File"
-msgstr "Fitxer"
-
-#: buzz/widgets/main_window.py
-msgid ""
-"Are you sure you want to delete the selected transcription(s)? This action "
-"cannot be undone."
-msgstr ""
-"Esteu segur que voleu suprimir les transcripcions seleccionades? Aquesta "
-"acció no es pot desfer."
-
-#: buzz/widgets/main_window.py
-msgid "Select audio file"
-msgstr "Selecciona un fitxer d'àudio"
-
-#: buzz/widgets/main_window.py
-msgid "Select folder"
-msgstr "Selecciona carpeta"
-
-#: buzz/widgets/main_window.py
-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
-#: buzz/transcriber/recording_transcriber.py
-msgid "Whisper server failed to start. Check logs for details."
-msgstr ""
-"El servidor Whisper no s'ha pogut iniciar. Consulteu els registres per "
-"obtenir més informació."
-
-#: buzz/transcriber/local_whisper_cpp_server_transcriber.py
-#: buzz/transcriber/recording_transcriber.py
-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 ""
-"El servidor Whisper no s'ha pogut iniciar a causa de la memòria insuficient. "
-"Si us plau, torneu-ho a provar amb un model més petit. Per forçar el mode "
-"CPU, utilitzeu la variable d'entorn BUZZ_FORCE_CPU=TRUE."
-
-#: buzz/transcriber/transcriber.py
-msgid "Translate to English"
-msgstr "Traduir a l'anglès"
-
-#: buzz/transcriber/transcriber.py
-msgid "Transcribe"
-msgstr "Transcriure"
-
-#: buzz/transcriber/transcriber.py
-msgid "Chinese"
-msgstr "Xinès"
-
-#: buzz/transcriber/transcriber.py
-msgid "Russian"
-msgstr "Rus"
-
-#: buzz/transcriber/transcriber.py
-msgid "Korean"
-msgstr "Coreà"
-
-#: buzz/transcriber/transcriber.py
-msgid "French"
-msgstr "Francés"
-
-#: buzz/transcriber/transcriber.py
-msgid "Portuguese"
-msgstr "Portuguès"
-
-#: buzz/transcriber/transcriber.py
-msgid "Turkish"
-msgstr "Turc"
-
-#: buzz/transcriber/transcriber.py
-msgid "Arabic"
-msgstr "Àrab"
-
-#: buzz/transcriber/transcriber.py
-msgid "Swedish"
-msgstr "Suec"
-
-#: buzz/transcriber/transcriber.py
-msgid "Indonesian"
-msgstr "Indonesi"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hindi"
-msgstr "Hindi"
-
-#: buzz/transcriber/transcriber.py
-msgid "Finnish"
-msgstr "Finès"
-
-#: buzz/transcriber/transcriber.py
-msgid "Vietnamese"
-msgstr "Vietnamita"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hebrew"
-msgstr "Hebreu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Greek"
-msgstr "Grec"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malay"
-msgstr "Malai"
-
-#: buzz/transcriber/transcriber.py
-msgid "Czech"
-msgstr "Txec"
-
-#: buzz/transcriber/transcriber.py
-msgid "Romanian"
-msgstr "Romanès"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hungarian"
-msgstr "Hongarès"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tamil"
-msgstr "Tàmil"
-
-#: buzz/transcriber/transcriber.py
-msgid "Norwegian"
-msgstr "Noruec"
-
-#: buzz/transcriber/transcriber.py
-msgid "Thai"
-msgstr "Tailandès"
-
-#: buzz/transcriber/transcriber.py
-msgid "Urdu"
-msgstr "Urdú"
-
-#: buzz/transcriber/transcriber.py
-msgid "Croatian"
-msgstr "Croata"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bulgarian"
-msgstr "Bùlgar"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lithuanian"
-msgstr "Lituà"
-
-#: buzz/transcriber/transcriber.py
-msgid "Latin"
-msgstr "Llatí"
-
-#: buzz/transcriber/transcriber.py
-msgid "Maori"
-msgstr "Maori"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malayalam"
-msgstr "Malaiàlam"
-
-#: buzz/transcriber/transcriber.py
-msgid "Welsh"
-msgstr "Gal·lès"
-
-#: buzz/transcriber/transcriber.py
-msgid "Slovak"
-msgstr "Eslovac"
-
-#: buzz/transcriber/transcriber.py
-msgid "Telugu"
-msgstr "Telugu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Persian"
-msgstr "Persa"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bengali"
-msgstr "Bengalí"
-
-#: buzz/transcriber/transcriber.py
-msgid "Serbian"
-msgstr "Serbi"
-
-#: buzz/transcriber/transcriber.py
-msgid "Azerbaijani"
-msgstr "Àzeri"
-
-#: buzz/transcriber/transcriber.py
-msgid "Slovenian"
-msgstr "Eslovè"
-
-#: buzz/transcriber/transcriber.py
-msgid "Kannada"
-msgstr "Kannada"
-
-#: buzz/transcriber/transcriber.py
-msgid "Estonian"
-msgstr "Estònia"
-
-#: buzz/transcriber/transcriber.py
-msgid "Macedonian"
-msgstr "Macedoni"
-
-#: buzz/transcriber/transcriber.py
-msgid "Breton"
-msgstr "Breton"
-
-#: buzz/transcriber/transcriber.py
-msgid "Basque"
-msgstr "Basc"
-
-#: buzz/transcriber/transcriber.py
-msgid "Icelandic"
-msgstr "Islandès"
-
-#: buzz/transcriber/transcriber.py
-msgid "Armenian"
-msgstr "Armeni"
-
-#: buzz/transcriber/transcriber.py
-msgid "Nepali"
-msgstr "Nepalès"
-
-#: buzz/transcriber/transcriber.py
-msgid "Mongolian"
-msgstr "Mongol"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bosnian"
-msgstr "Bosnià"
-
-#: buzz/transcriber/transcriber.py
-msgid "Kazakh"
-msgstr "Kazakh"
-
-#: buzz/transcriber/transcriber.py
-msgid "Albanian"
-msgstr "Albanès"
-
-#: buzz/transcriber/transcriber.py
-msgid "Swahili"
-msgstr "Suahili"
-
-#: buzz/transcriber/transcriber.py
-msgid "Galician"
-msgstr "Gallec"
-
-#: buzz/transcriber/transcriber.py
-msgid "Marathi"
-msgstr "Marathi"
-
-#: buzz/transcriber/transcriber.py
-msgid "Punjabi"
-msgstr "Panjabi"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sinhala"
-msgstr "Singalès"
-
-#: buzz/transcriber/transcriber.py
-msgid "Khmer"
-msgstr "Khmer"
-
-#: buzz/transcriber/transcriber.py
-msgid "Shona"
-msgstr "Shona"
-
-#: buzz/transcriber/transcriber.py
-msgid "Yoruba"
-msgstr "Ioruba"
-
-#: buzz/transcriber/transcriber.py
-msgid "Somali"
-msgstr "Somali"
-
-#: buzz/transcriber/transcriber.py
-msgid "Afrikaans"
-msgstr "Afrikaans"
-
-#: buzz/transcriber/transcriber.py
-msgid "Occitan"
-msgstr "Occità"
-
-#: buzz/transcriber/transcriber.py
-msgid "Georgian"
-msgstr "Georgià"
-
-#: buzz/transcriber/transcriber.py
-msgid "Belarusian"
-msgstr "Bielorús"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tajik"
-msgstr "Tadjik"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sindhi"
-msgstr "Sindhi"
-
-#: buzz/transcriber/transcriber.py
-msgid "Gujarati"
-msgstr "Gujarati"
-
-#: buzz/transcriber/transcriber.py
-msgid "Amharic"
-msgstr "Amhàric"
-
-#: buzz/transcriber/transcriber.py
-msgid "Yiddish"
-msgstr "Yiddish"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lao"
-msgstr "Lao"
-
-#: buzz/transcriber/transcriber.py
-msgid "Uzbek"
-msgstr "Uzbek"
-
-#: buzz/transcriber/transcriber.py
-msgid "Faroese"
-msgstr "Feroès"
-
-#: buzz/transcriber/transcriber.py
-msgid "Haitian Creole"
-msgstr "Crioll d'Haití"
-
-#: buzz/transcriber/transcriber.py
-msgid "Pashto"
-msgstr "Paixtu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Turkmen"
-msgstr "Turcomans"
-
-#: buzz/transcriber/transcriber.py
-msgid "Nynorsk"
-msgstr "Nynorsk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Maltese"
-msgstr "Maltès"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sanskrit"
-msgstr "Sànscrit"
-
-#: buzz/transcriber/transcriber.py
-msgid "Luxembourgish"
-msgstr "Luxemburguès"
-
-#: buzz/transcriber/transcriber.py
-msgid "Myanmar"
-msgstr "Myanmar"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tibetan"
-msgstr "Tibetà"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tagalog"
-msgstr "Tagàlog"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malagasy"
-msgstr "Malgaix"
-
-#: buzz/transcriber/transcriber.py
-msgid "Assamese"
-msgstr "Assamès"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tatar"
-msgstr "Tàtar"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hawaiian"
-msgstr "Hawaià"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lingala"
-msgstr "Lingala"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hausa"
-msgstr "Hausa"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bashkir"
-msgstr "Bashkir"
-
-#: buzz/transcriber/transcriber.py
-msgid "Javanese"
-msgstr "Javanès"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sundanese"
-msgstr "Sundanès"
-
-#: buzz/transcriber/transcriber.py
-msgid "Cantonese"
-msgstr "Cantonès"
-
-#: buzz/transcriber/recording_transcriber.py buzz/model_loader.py
-msgid "A connection error occurred"
-msgstr "S'ha produït un error de connexió"
-
-#: buzz/transcriber/recording_transcriber.py
-msgid "Starting Whisper.cpp..."
-msgstr "Començant Whisper.cpp..."
-
-#: buzz/transcriber/recording_transcriber.py
-msgid "Starting transcription..."
-msgstr "Iniciant la transcripció..."
-
-#: buzz/settings/shortcut.py
-msgid "Open Record Window"
-msgstr "Obre la finestra de registre"
-
-#: buzz/settings/shortcut.py
-msgid "Import File"
-msgstr "Importar arxiu"
-
-#: buzz/settings/shortcut.py
-msgid "Open Preferences Window"
-msgstr "Obre la finestra de preferències"
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Text"
-msgstr "Veure el text de la transcripció"
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Translation"
-msgstr "Veure la traducció de transcripció"
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Timestamps"
-msgstr "Veure les marques de temps de la transcripció"
-
-#: buzz/settings/shortcut.py
-msgid "Search Transcript"
-msgstr "Cerca una transcripció"
-
-#: buzz/settings/shortcut.py
-msgid "Go to Next Transcript Search Result"
-msgstr "Ves al resultat de cerca de transcripció següent"
-
-#: buzz/settings/shortcut.py
-msgid "Go to Previous Transcript Search Result"
-msgstr "Ves al resultat de cerca de transcripció anterior"
-
-#: buzz/settings/shortcut.py
-msgid "Scroll to Current Text"
-msgstr "Desplaça't fins al text actual"
-
-#: buzz/settings/shortcut.py
-msgid "Play/Pause Audio"
-msgstr "Reproduir/posar en pausa l'àudio"
-
-#: buzz/settings/shortcut.py
-msgid "Replay Current Segment"
-msgstr "Reprodueix el segment actual"
-
-#: buzz/settings/shortcut.py
-msgid "Toggle Playback Controls"
-msgstr "Commuta els controls de reproducció"
-
-#: buzz/settings/shortcut.py
-msgid "Decrease Segment Start Time"
-msgstr "Disminuir l'hora d'inici del segment"
-
-#: buzz/settings/shortcut.py
-msgid "Increase Segment Start Time"
-msgstr "Augmenta l'hora d'inici del segment"
-
-#: buzz/settings/shortcut.py
-msgid "Decrease Segment End Time"
-msgstr "Disminueix l'hora de finalització del segment"
-
-#: buzz/settings/shortcut.py
-msgid "Increase Segment End Time"
-msgstr "Augmenta l'hora de finalització del segment"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append below"
-msgstr "Afegeix a sota"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append above"
-msgstr "Afegeix a sobre"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append and correct"
-msgstr "Afegeix i corregeix"
-
-#: buzz/file_transcriber_queue_worker.py
-msgid ""
-"Speech extraction failed! Check your internet connection — a model may need "
-"to be downloaded."
-msgstr ""
-"Ha fallat l'extracció de veu! Comproveu la vostra connexió a Internet — pot "
-"ser que s'hagi de descarregar un model."
-
-#~ msgid "Comma-separated, e.g. \"0.0, 0.2, 0.4, 0.6, 0.8, 1.0\""
-#~ msgstr "Separat per comes, p. ex. «0.0, 0.2, 0.4, 0.6, 0.8, 1.0»"
-
-#~ msgid "Temperature:"
-#~ msgstr "Temperatura:"
-
-#~ msgid "Please translate each text sent to you from English to Spanish."
-#~ msgstr "Si us plau, tradueix cada text que t'enviï de l'anglès al castellà."
-
-#~ msgid "Translation error, see logs!"
-#~ msgstr "Error de traducció, vegeu els registres!"
-
-#~ msgid "Snap permission notice"
-#~ msgstr "Avís de permís d'ajust"
-
-#~ msgid ""
-#~ "Detected missing permissions, please check that snap permissions have "
-#~ "been granted"
-#~ msgstr ""
-#~ "S'han detectat permisos que manquen, comproveu que s'han concedit "
-#~ "permisos de captura"
-
-#~ msgid ""
-#~ "To enable necessary permissions run the following commands in the terminal"
-#~ msgstr ""
-#~ "Per habilitar els permisos necessaris, executeu les ordres següents al "
-#~ "terminal"
-
-#~ msgid "Close"
-#~ msgstr "Tanca"
-
-#~ msgid "Enter instructions for AI on how to translate..."
-#~ msgstr "Introduïu les instruccions per a la IA sobre com traduir..."
-
-#~ msgid "ID"
-#~ msgstr "ID"
-
-#~ msgid "Undo"
-#~ msgstr "Desfés"
-
-#~ msgid "Redo"
-#~ msgstr "Refés"
diff --git a/buzz/locale/da_DK/LC_MESSAGES/buzz.po b/buzz/locale/da_DK/LC_MESSAGES/buzz.po
deleted file mode 100644
index f7835ae7..00000000
--- a/buzz/locale/da_DK/LC_MESSAGES/buzz.po
+++ /dev/null
@@ -1,1645 +0,0 @@
-msgid ""
-msgstr ""
-"Project-Id-Version: \n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2026-03-07 20:20+0200\n"
-"PO-Revision-Date: \n"
-"Last-Translator: Ole Guldberg2 \n"
-"Language-Team: \n"
-"Language: da_DK\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=4; plural=n==1 ? 3 : n%10==1 && n%100!=11 ? 0 : "
-"n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
-
-# "X-Generator: Poedit 3.4.4\n"
-#: buzz/widgets/import_url_dialog.py buzz/settings/shortcut.py
-msgid "Import URL"
-msgstr "Importer fra URL"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "https://example.com/audio.mp3"
-msgstr "https://example.com/audio.mp3"
-
-#: buzz/widgets/import_url_dialog.py
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-#: buzz/widgets/main_window.py
-msgid "Ok"
-msgstr "OK"
-
-#: buzz/widgets/import_url_dialog.py
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-#: buzz/widgets/model_download_progress_dialog.py buzz/widgets/main_window.py
-msgid "Cancel"
-msgstr "Afbryd"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "URL:"
-msgstr "URL:"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "Invalid URL"
-msgstr "Ugyldig URL"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "The URL you entered is invalid."
-msgstr "Den URL du har angivet er ikke gyldig."
-
-#: buzz/widgets/presentation_window.py
-msgid "Live Transcript Presentation"
-msgstr "Live transkriptionspræsentation"
-
-#: buzz/widgets/preferences_dialog/shortcuts_editor_preferences_widget.py
-msgid "Reset to Defaults"
-msgstr "Gendan standard-indstillinger"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "English"
-msgstr "Engelsk"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Catalan"
-msgstr "Catalansk"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Danish"
-msgstr "Dansk"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Dutch"
-msgstr "Nederlandsk"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "German"
-msgstr "Tysk"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Spanish"
-msgstr "Spansk"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Italian"
-msgstr "Italiensk"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Japanese"
-msgstr "Japansk"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Latvian"
-msgstr "Lettisk"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Polish"
-msgstr "Polsk"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Portuguese (Brazil)"
-msgstr "Portugisisk (Brasilien)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Ukrainian"
-msgstr "Ukrainsk"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Chinese (Simplified)"
-msgstr "Kinesisk (forenklet)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Chinese (Traditional)"
-msgstr "Kinesisk (traditionelt)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Restart required!"
-msgstr "Genstart påkrævet!"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Ui Language"
-msgstr "Brugerfladesprog"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Font Size"
-msgstr "Skriftypestørrelse"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Test"
-msgstr "Test"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API key"
-msgstr "OpenAI API-nøgle"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI base url"
-msgstr "OpenAI base-URL"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API model"
-msgstr "OpenAI API-model"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Default export file name"
-msgstr "Standard eksport filnavn"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Enable live recording transcription export"
-msgstr "Slå transkription af live optagelse eksport til"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Browse"
-msgstr "Gennemse"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Export folder"
-msgstr "Eksportmappe"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Live recording mode"
-msgstr "Live optagelsestilstand"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Note: Live recording export settings will be moved to the Advanced Settings "
-"in the Live Recording screen in a future version."
-msgstr ""
-"Bemærk: Indstillinger for live optagelseseksport vil i en fremtidig version "
-"blive flyttet til Avancerede indstillinger på skærmen for live optagelse."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Use 8-bit quantization to reduce memory usage"
-msgstr "Brug 8-bit kvantisering for at reducere hukommelsesforbruget"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Applies to Huggingface and Faster Whisper models. Reduces GPU memory usage "
-"but may slightly decrease transcription quality."
-msgstr ""
-"Gælder for Huggingface og Faster Whisper modeller. Reducerer GPU "
-"hukommelsesforbrug, men kan en smule forringe transkriptionskvaliteten."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Reduce GPU RAM"
-msgstr "Reducer GPU RAM"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Use only CPU and disable GPU acceleration"
-msgstr "Brug kun CPU og deaktiver GPU-acceleration"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Set this if larger models do not fit your GPU memory and Buzz crashes"
-msgstr ""
-"Aktivér dette hvis større modeller ikke passer i GPU-hukommelsen og Buzz "
-"crasher"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Disable GPU"
-msgstr "Deaktiver GPU"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API Key Test"
-msgstr "OpenAI API Nøgle test"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Your API key is valid. Buzz will use this key to perform Whisper API "
-"transcriptions and AI translations."
-msgstr ""
-"Din API nøgle er gyldig. Buzz vil benytte nøglen til at anvende Whisper API "
-"transkription og AI oversættelser."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Invalid API key"
-msgstr "Ugyldig API-nøgle"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"API supports only base64 characters (A-Za-z0-9+/=_-). Other characters in "
-"API key may cause errors."
-msgstr ""
-"API supporterer kun base64 tegn (A-Za-z0-9+/=_-). Andre tegn i API-nøglen "
-"kan guve fejl. "
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Select Export Folder"
-msgstr "Vælg eksport-mappe"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"OpenAI API returned invalid response. Please check the API url or your key. "
-"Transcription and translation may still work if the API does not support key "
-"validation."
-msgstr ""
-"OpenAI API returnerede et ugyldigt svar. Tjek venligst API-URL og nøgle. "
-"Transkription og oversættelse virker måske stadig, selvom API'et ikke "
-"understøtter nøgle validering."
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Enable folder watch"
-msgstr "Aktiver mappeovervågning"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Delete processed files"
-msgstr "Slet behandlede filer"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Input folder"
-msgstr "Inputmappe"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Output folder"
-msgstr "Outputmappe"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Select Input Folder"
-msgstr "Vælg inputmappe"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Select Output Folder"
-msgstr "Vælg outputmappe"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Preferences"
-msgstr "Indstillinger"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "General"
-msgstr "Generelt"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Models"
-msgstr "Modeller"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Shortcuts"
-msgstr "Genveje"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Folder Watch"
-msgstr "Mappeovervågning"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Group"
-msgstr "Grupper"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Huggingface ID of a Faster whisper model"
-msgstr "Huggingface ID af Faster Whisper model"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download"
-msgstr "Download"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Show file location"
-msgstr "Vis fil-lokation"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Delete"
-msgstr "Slet"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Downloaded"
-msgstr "Downloadded"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Available for Download"
-msgstr "Tilgængelige til download"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download link to Whisper.cpp ggml model file"
-msgstr "Download link til Whisper.cpp ggml model-fil"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Delete Model"
-msgstr "Slet model"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Are you sure you want to delete the selected model?"
-msgstr "Er du sikker på at du vil slette den valgte model?"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download failed"
-msgstr "Download mislykkedes"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcription_tasks_table_widget.py
-#: buzz/widgets/update_dialog.py buzz/widgets/main_window.py
-#: buzz/model_loader.py
-msgid "Error"
-msgstr "Fejl"
-
-#: buzz/widgets/record_button.py buzz/widgets/main_window_toolbar.py
-msgid "Record"
-msgstr "Optag"
-
-#: buzz/widgets/record_button.py
-msgid "Stop"
-msgstr "Stop"
-
-#: buzz/widgets/transcriber/languages_combo_box.py
-#: buzz/transcriber/transcriber.py
-msgid "Detect Language"
-msgstr "Detekter sprog"
-
-#: buzz/widgets/transcriber/mms_language_line_edit.py
-msgid "e.g., eng, fra, deu"
-msgstr "f.eks. eng, fra, deu"
-
-#: buzz/widgets/transcriber/mms_language_line_edit.py
-msgid ""
-"Enter an ISO 639-3 language code (3 letters).\n"
-"Examples: eng (English), fra (French), deu (German),\n"
-"spa (Spanish), lav (Latvian)"
-msgstr ""
-"Indtast en ISO 639-3 sprogkode (3 bogstaver).\n"
-"Eksempler: eng (engelsk), fra (fransk), deu (tysk),\n"
-"spa (spansk), lav (lettisk)"
-
-#: buzz/widgets/transcriber/file_transcriber_widget.py
-msgid "Run"
-msgstr "Kør"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Model:"
-msgstr "Model:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-#: buzz/transcriber/recording_transcriber.py
-msgid "First time use of a model may take up to several minutest to load."
-msgstr "Først gang kan brug af en model tage flere minutter at indlæse."
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Api Key:"
-msgstr "API-nøgle:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Task:"
-msgstr "Opgave:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Language:"
-msgstr "Sprog:"
-
-#: buzz/widgets/transcriber/initial_prompt_text_edit.py
-msgid "Enter prompt..."
-msgstr "Input tekst..."
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Advanced Settings"
-msgstr "Advancerede indstillinger"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Speech recognition settings"
-msgstr "Talegenkendelsesindstillinger"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Initial Prompt:"
-msgstr "Start prompt:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Translation settings"
-msgstr "Oversættelsesindstillinger"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Enable AI translation"
-msgstr "Brug AI oversættelse"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "AI model:"
-msgstr "AI model:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid ""
-"Please translate each text sent to you from English to Spanish. Translation "
-"will be used in an automated system, please do not add any comments or "
-"notes, just the translation."
-msgstr ""
-"Oversæt venligst hver tekst, der sendes til dig, fra engelsk til spansk. "
-"Oversættelsen vil blive brugt i et automatiseret system, så tilføj venligst "
-"ingen kommentarer eller noter, kun oversættelsen."
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Instructions for AI:"
-msgstr "Instruktioner for AI:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Recording settings"
-msgstr "Optagelsesindstillinger"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Silence threshold:"
-msgstr "Tærskelværdi for stilhed:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Live recording mode:"
-msgstr "Live optagelsestilstand:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Line separator:"
-msgstr "Linjeseparator:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Transcription step:"
-msgstr "Transskriberingstrin:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Hide unconfirmed"
-msgstr "Skjul ubekræftet"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Enable live recording export"
-msgstr "Aktiver eksport af live optagelse"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export folder:"
-msgstr "Eksportmappe:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export file name:"
-msgstr "Eksport filnavn:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Text file (.txt)"
-msgstr "Tekstfil (.txt)"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "CSV (.csv)"
-msgstr "CSV (.csv)"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export file type:"
-msgstr "Eksport filtype:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid ""
-"Limit export entries\n"
-"(0 = export all):"
-msgstr ""
-"Begræns eksportposter\n"
-"(0 = eksporter alle):"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Word-level timings"
-msgstr "Ord tidsniveau"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Extract speech"
-msgstr "Eksakt tale"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Export:"
-msgstr "Eksporter:"
-
-#: buzz/widgets/transcriber/hugging_face_search_line_edit.py
-msgid "Huggingface ID of a model"
-msgstr "Huggingface ID til en model"
-
-#: buzz/widgets/transcriber/advanced_settings_button.py
-msgid "Advanced..."
-msgstr "Advanceret..."
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "New File Transcription"
-msgstr "Ny filtranskription"
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "New URL Transcription"
-msgstr "Ny URL-transkription"
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "Open Transcript"
-msgstr "Åben transkription"
-
-#: buzz/widgets/main_window_toolbar.py buzz/settings/shortcut.py
-msgid "Cancel Transcription"
-msgstr "Afbryd transkription"
-
-#: buzz/widgets/main_window_toolbar.py buzz/widgets/main_window.py
-#: buzz/settings/shortcut.py
-msgid "Clear History"
-msgstr "Ryd historik"
-
-#: buzz/widgets/main_window_toolbar.py buzz/widgets/update_dialog.py
-msgid "Update Available"
-msgstr "Opdatering tilgængelig"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "In Progress"
-msgstr "Arbejder"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Completed"
-msgstr "Færdig"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Failed"
-msgstr "Mislykkedes"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Canceled"
-msgstr "Afbrudt"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Queued"
-msgstr "Sat i kø"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "File Name / URL"
-msgstr "Filnavn / URL"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Model"
-msgstr "Model"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Task"
-msgstr "Opgave"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Status"
-msgstr "Status"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Date Completed"
-msgstr "Dato for færdiggørelse"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Date Added"
-msgstr "Dato for tilføjelse"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Notes"
-msgstr "Noter"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Reset Column Order"
-msgstr "Nulstil kolonnerækkefølge"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Restart Transcription"
-msgstr "Genstart transkription"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Rename"
-msgstr "Omdøb"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Add/Edit Notes"
-msgstr "Tilføj/rediger noter"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Rename Transcription"
-msgstr "Omdøb transkription"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Enter new name:"
-msgstr "Indtast nyt navn:"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Enter some relevant notes for this transcription:"
-msgstr "Indtast relevante noter til denne transkription:"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Cannot Restart"
-msgstr "Kan ikke genstarte"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Only failed or canceled transcriptions can be restarted."
-msgstr "Kun mislykkede eller annullerede transkriptioner kan genstartes."
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Failed to restart transcription: {}"
-msgstr "Kunne ikke genstarte transkription: {}"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid ""
-"Could not restart transcription: model not available and could not be "
-"downloaded."
-msgstr ""
-"Kunne ikke genstarte transkription: modellen er ikke tilgængelig og kunne "
-"ikke downloades."
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Could not restart transcription: transcriber worker not found."
-msgstr ""
-"Kunne ikke genstarte transkription: transkriptionsprocessen blev ikke fundet."
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Live Recording"
-msgstr "Live optagelse"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Click Record to begin..."
-msgstr "Klik Optage for at begynde..."
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Waiting for AI translation..."
-msgstr "Venter på AI oversættelse..."
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Microphone:"
-msgstr "Mikrofon:"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Show in new window"
-msgstr "Vis i nyt vindue"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Text Size:"
-msgstr "Tekststørrelse:"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Theme"
-msgstr "Tema"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Light"
-msgstr "Lys"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Dark"
-msgstr "Mørk"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Custom"
-msgstr "Brugerdefineret"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Text Color"
-msgstr "Tekstfarve"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Background Color"
-msgstr "Baggrundsfarve"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Fullscreen"
-msgstr "Fuldskærm"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy"
-msgstr "Kopiér"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy transcription to clipboard"
-msgstr "Kopiér transkription til udklipsholder"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Nothing to copy!"
-msgstr "Intet at kopiere!"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy failed"
-msgstr "Kopiering mislykkedes"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copied!"
-msgstr "Kopieret!"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Select Text Color"
-msgstr "Vælg tekstfarve"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Select Background Color"
-msgstr "Vælg baggrundsfarve"
-
-#: buzz/widgets/recording_transcriber_widget.py
-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
-msgid ""
-"Please check your audio devices or check the application logs for more "
-"information."
-msgstr ""
-"Tjek venligst dine audioenheder eller tjek applikationens logs for "
-"mereinformation."
-
-#: buzz/widgets/update_dialog.py
-msgid "A new version of Buzz is available!"
-msgstr "En ny version af Buzz er tilgængelig!"
-
-#: buzz/widgets/update_dialog.py
-msgid "Current version:"
-msgstr "Nuværende version:"
-
-#: buzz/widgets/update_dialog.py
-msgid "New version:"
-msgstr "Ny version:"
-
-#: buzz/widgets/update_dialog.py
-msgid "Release Notes:"
-msgstr "Udgivelsesnoter:"
-
-#: buzz/widgets/update_dialog.py
-msgid "Download and Install"
-msgstr "Download og installer"
-
-#: buzz/widgets/update_dialog.py
-msgid "No download URL available for your platform."
-msgstr "Ingen download-URL tilgængelig for din platform."
-
-#: buzz/widgets/update_dialog.py
-msgid "Downloading file {} of {}..."
-msgstr "Downloader fil {} af {}..."
-
-#: buzz/widgets/update_dialog.py
-msgid "Downloading file {} of {} ({:.1f} MB / {:.1f} MB)..."
-msgstr "Downloader fil {} af {} ({:.1f} MB / {:.1f} MB)..."
-
-#: buzz/widgets/update_dialog.py
-msgid "Download Failed"
-msgstr "Download mislykkedes"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to download the update: {}"
-msgstr "Kunne ikke downloade opdateringen: {}"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to save the installer: {}"
-msgstr "Kunne ikke gemme installationsprogrammet: {}"
-
-#: buzz/widgets/update_dialog.py
-msgid "Download complete!"
-msgstr "Download fuldført!"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to run the installer: {}"
-msgstr "Kunne ikke køre installationsprogrammet: {}"
-
-#: buzz/widgets/about_dialog.py
-msgid "Check for updates"
-msgstr "Tjek for opdateringer"
-
-#: buzz/widgets/about_dialog.py
-msgid "Show logs"
-msgstr "Vis logfiler"
-
-#: buzz/widgets/about_dialog.py
-msgid "You're up to date!"
-msgstr "Du er opdateret!"
-
-#: buzz/widgets/audio_meter_widget.py
-msgid "Average volume"
-msgstr "Gennemsnitlig lydstyrke"
-
-#: buzz/widgets/audio_meter_widget.py
-msgid "Queue"
-msgstr "Kø"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-msgid "Start"
-msgstr "Start"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-msgid "End"
-msgstr "Slut"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Text"
-msgstr "Tekst"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Translation"
-msgstr "Oversættelse"
-
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-msgid "View"
-msgstr "Vis"
-
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-msgid "Timestamps"
-msgstr "Tidsstempler"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Export"
-msgstr "Eksporter"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Translate"
-msgstr "Oversæt"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Resize"
-msgstr "Behandel størrelse"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Identify Speakers"
-msgstr "Identificer talere"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Find"
-msgstr "Find"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Show/Hide Search Bar (Ctrl+F)"
-msgstr "Vis/skjul søgebjælke (Ctrl+F)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Find:"
-msgstr "Find:"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Enter text to find..."
-msgstr "Indtast tekst at søge efter..."
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Previous match (Shift+Enter)"
-msgstr "Forrige match (Shift+Enter)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Next match (Ctrl+Enter)"
-msgstr "Næste match (Ctrl+Enter)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Clear"
-msgstr "Ryd"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Playback Controls:"
-msgstr "Afspilningskontroller:"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Loop Segment"
-msgstr "Gentag segment"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Enable/disable looping when clicking on transcript segments"
-msgstr "Aktiver/deaktiver gentagelse ved klik på transkriptionssegmenter"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Follow Audio"
-msgstr "Følg lyd"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid ""
-"Enable/disable following the current audio position in the transcript. When "
-"enabled, automatically scrolls to current text."
-msgstr ""
-"Aktiver/deaktiver at følge den aktuelle lydposition i transkriptionen. Når "
-"aktiveret, scrolles der automatisk til den aktuelle tekst."
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Scroll to Current"
-msgstr "Rul til aktuel"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Scroll to the currently spoken text"
-msgstr "Rul til den tekst der tales i øjeblikket"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "1 of 100+ matches"
-msgstr "1 af 100+ matches"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "1 of "
-msgstr "1 af "
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " matches"
-msgstr " matches"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "No matches found"
-msgstr "Ingen matches fundet"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " of 100+ matches"
-msgstr " af 100+ matches"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " of "
-msgstr " af "
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "API Key Required"
-msgstr "API-nøgle påkrævet"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Please enter OpenAI API Key in preferences"
-msgstr "Indtast venligst OpenAI API-nøgle i indstillinger"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend end time"
-msgstr "Forlæng sluttidspunkt"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend endings by up to (seconds)"
-msgstr "Forlæng slutninger med op til (sekunder)"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend endings"
-msgstr "Forlæng slutninger"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Resize Options"
-msgstr "Størrelsesindstillinger"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Desired subtitle length"
-msgstr "Ønskede undertekst længde"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Available only if word level timings were disabled during transcription"
-msgstr ""
-"Kun tilgængeligt hvis ordniveau-tidsstempler var deaktiveret under "
-"transkription"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge Options"
-msgstr "Sammenfletningsindstillinger"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge by gap"
-msgstr "Sammenflet ved hul"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Split by punctuation"
-msgstr "Split ved punktum"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Split by max length"
-msgstr "Split ved max længde"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge"
-msgstr "Sammenflet"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Available only if word level timings were enabled during transcription"
-msgstr ""
-"Kun tilgængeligt hvis ordniveau-tidsstempler var aktiveret under "
-"transkription"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid ""
-"Speaker identification is not available: failed to load required libraries."
-msgstr ""
-"Taler-identifikation er ikke tilgængelig: kunne ikke indlæse nødvendige "
-"biblioteker."
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "1/8 Collecting transcripts"
-msgstr "1/8 Indsamler transkriptioner"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "2/8 Loading audio"
-msgstr "2/8 Indlæser lyd"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "3/8 Loading alignment model"
-msgstr "3/8 Indlæser justeringsmodel"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "3/8 Loading alignment model (retrying with cache...)"
-msgstr "3/8 Indlæser justeringsmodel (prøver igen med cache...)"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid ""
-"Failed to load alignment model. Please check your internet connection and "
-"try again."
-msgstr ""
-"Kunne ikke indlæse justeringsmodel. Tjek venligst din internetforbindelse og "
-"prøv igen."
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "4/8 Processing audio"
-msgstr "4/8 Behandler lyd"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "5/8 Preparing transcripts"
-msgstr "5/8 Forbereder transkriptioner"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "6/8 Identifying speakers"
-msgstr "6/8 Identificerer talere"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "7/8 Mapping speakers to transcripts"
-msgstr "7/8 Kortlægger talere til transkriptioner"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "8/8 Identification done"
-msgstr "8/8 Identifikation afsluttet"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "0/0 Error identifying speakers"
-msgstr "0/0 Fejl ved identifikation af talere"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Step 1: Identify speakers"
-msgstr "Trin 1: Identificer talere"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Identify"
-msgstr "Identificer"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Ready to identify speakers"
-msgstr "Klar til at identificere talere"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Audio file not found"
-msgstr "Lydfil ikke fundet"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Step 2: Name speakers"
-msgstr "Trin 2: Navngiv talere"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Play sample"
-msgstr "Afspil eksempel"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Merge speaker sentences"
-msgstr "Sammenflet talerens sætninger"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Save"
-msgstr "Gem"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Cancelling..."
-msgstr "Annullerer..."
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Cancelled"
-msgstr "Annulleret"
-
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Save File"
-msgstr "Gem fil"
-
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Text files"
-msgstr "Tekst filer"
-
-#: buzz/widgets/model_download_progress_dialog.py
-msgid "Downloading model"
-msgstr "Downloader model"
-
-#: buzz/widgets/model_download_progress_dialog.py
-msgid "remaining"
-msgstr "tilbageværende"
-
-#: buzz/widgets/menu_bar.py
-msgid "Import File..."
-msgstr "Importer Fil..."
-
-#: buzz/widgets/menu_bar.py
-msgid "Import URL..."
-msgstr "Importer URL..."
-
-#: buzz/widgets/menu_bar.py
-msgid "Import Folder..."
-msgstr "Importer Mappe..."
-
-#: buzz/widgets/menu_bar.py
-msgid "About"
-msgstr "Om"
-
-#: buzz/widgets/menu_bar.py
-msgid "Preferences..."
-msgstr "Indstillinger..."
-
-#: buzz/widgets/menu_bar.py
-msgid "Help"
-msgstr "Hjælp"
-
-#: buzz/widgets/menu_bar.py
-msgid "File"
-msgstr "Fil"
-
-#: buzz/widgets/main_window.py
-msgid ""
-"Are you sure you want to delete the selected transcription(s)? This action "
-"cannot be undone."
-msgstr ""
-"Er du sikker på at du vil slette den valgte transkription? Denne handling "
-"kan ikke fortrydes."
-
-#: buzz/widgets/main_window.py
-msgid "Select audio file"
-msgstr "Vælg audio-fil"
-
-#: buzz/widgets/main_window.py
-msgid "Select folder"
-msgstr "Vælg inputmappe"
-
-#: buzz/widgets/main_window.py
-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
-#: buzz/transcriber/recording_transcriber.py
-msgid "Whisper server failed to start. Check logs for details."
-msgstr "Whisper-serveren kunne ikke starte. Tjek logfilerne for detaljer."
-
-#: buzz/transcriber/local_whisper_cpp_server_transcriber.py
-#: buzz/transcriber/recording_transcriber.py
-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-serveren kunne ikke starte på grund af utilstrækkelig hukommelse. "
-"Prøv igen med en mindre model. For at tvinge CPU-tilstand, brug "
-"miljøvariablen BUZZ_FORCE_CPU=TRUE."
-
-#: buzz/transcriber/transcriber.py
-msgid "Translate to English"
-msgstr "Oversæt til engelsk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Transcribe"
-msgstr "Transkriber"
-
-#: buzz/transcriber/transcriber.py
-msgid "Chinese"
-msgstr "Kinesisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Russian"
-msgstr "Russisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Korean"
-msgstr "Koreansk"
-
-#: buzz/transcriber/transcriber.py
-msgid "French"
-msgstr "Fransk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Portuguese"
-msgstr "Portugisisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Turkish"
-msgstr "Tyrkisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Arabic"
-msgstr "Arabisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Swedish"
-msgstr "Svensk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Indonesian"
-msgstr "Indonesisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hindi"
-msgstr "Hindi"
-
-#: buzz/transcriber/transcriber.py
-msgid "Finnish"
-msgstr "Finsk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Vietnamese"
-msgstr "Vietnamesisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hebrew"
-msgstr "Hebraisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Greek"
-msgstr "Græsk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malay"
-msgstr "Malaysisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Czech"
-msgstr "Tjekkisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Romanian"
-msgstr "Rumænsk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hungarian"
-msgstr "Ungarsk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tamil"
-msgstr "Tamilsk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Norwegian"
-msgstr "Norsk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Thai"
-msgstr "Thailandsk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Urdu"
-msgstr "Urdu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Croatian"
-msgstr "Kroatisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bulgarian"
-msgstr "Bulgarsk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lithuanian"
-msgstr "Litauisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Latin"
-msgstr "Latin"
-
-#: buzz/transcriber/transcriber.py
-msgid "Maori"
-msgstr "Maori"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malayalam"
-msgstr "Malayalam"
-
-#: buzz/transcriber/transcriber.py
-msgid "Welsh"
-msgstr "Walisisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Slovak"
-msgstr "Slovakisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Telugu"
-msgstr "Telugu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Persian"
-msgstr "Persisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bengali"
-msgstr "Bengalsk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Serbian"
-msgstr "Serbisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Azerbaijani"
-msgstr "Aserbajdsjansk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Slovenian"
-msgstr "Slovensk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Kannada"
-msgstr "Kannada"
-
-#: buzz/transcriber/transcriber.py
-msgid "Estonian"
-msgstr "Estisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Macedonian"
-msgstr "Makedonsk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Breton"
-msgstr "Bretonsk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Basque"
-msgstr "Baskisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Icelandic"
-msgstr "Islandsk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Armenian"
-msgstr "Armensk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Nepali"
-msgstr "Nepalesisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Mongolian"
-msgstr "Mongolsk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bosnian"
-msgstr "Bosnisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Kazakh"
-msgstr "Kasakhisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Albanian"
-msgstr "Albansk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Swahili"
-msgstr "Swahili"
-
-#: buzz/transcriber/transcriber.py
-msgid "Galician"
-msgstr "Galicisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Marathi"
-msgstr "Marathi"
-
-#: buzz/transcriber/transcriber.py
-msgid "Punjabi"
-msgstr "Punjabi"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sinhala"
-msgstr "Singalesisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Khmer"
-msgstr "Khmer"
-
-#: buzz/transcriber/transcriber.py
-msgid "Shona"
-msgstr "Shona"
-
-#: buzz/transcriber/transcriber.py
-msgid "Yoruba"
-msgstr "Yoruba"
-
-#: buzz/transcriber/transcriber.py
-msgid "Somali"
-msgstr "Somalisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Afrikaans"
-msgstr "Afrikaans"
-
-#: buzz/transcriber/transcriber.py
-msgid "Occitan"
-msgstr "Occitansk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Georgian"
-msgstr "Georgisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Belarusian"
-msgstr "Hviderussisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tajik"
-msgstr "Tadsjikisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sindhi"
-msgstr "Sindhi"
-
-#: buzz/transcriber/transcriber.py
-msgid "Gujarati"
-msgstr "Gujarati"
-
-#: buzz/transcriber/transcriber.py
-msgid "Amharic"
-msgstr "Amharisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Yiddish"
-msgstr "Jiddisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lao"
-msgstr "Laotisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Uzbek"
-msgstr "Usbekisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Faroese"
-msgstr "Færøsk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Haitian Creole"
-msgstr "Haitiansk kreolsk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Pashto"
-msgstr "Pashto"
-
-#: buzz/transcriber/transcriber.py
-msgid "Turkmen"
-msgstr "Turkmensk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Nynorsk"
-msgstr "Nynorsk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Maltese"
-msgstr "Maltesisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sanskrit"
-msgstr "Sanskrit"
-
-#: buzz/transcriber/transcriber.py
-msgid "Luxembourgish"
-msgstr "Luxembourgsk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Myanmar"
-msgstr "Burmesisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tibetan"
-msgstr "Tibetansk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tagalog"
-msgstr "Tagalog"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malagasy"
-msgstr "Malagassisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Assamese"
-msgstr "Assamesisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tatar"
-msgstr "Tatarisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hawaiian"
-msgstr "Hawaiiansk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lingala"
-msgstr "Lingala"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hausa"
-msgstr "Hausa"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bashkir"
-msgstr "Basjkirsk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Javanese"
-msgstr "Javanesisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sundanese"
-msgstr "Sundanesisk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Cantonese"
-msgstr "Kantonesisk"
-
-#: buzz/transcriber/recording_transcriber.py buzz/model_loader.py
-msgid "A connection error occurred"
-msgstr "Der er opstået en forbindelsesfejl"
-
-#: buzz/transcriber/recording_transcriber.py
-msgid "Starting Whisper.cpp..."
-msgstr "Starter Whisper.cpp..."
-
-#: buzz/transcriber/recording_transcriber.py
-msgid "Starting transcription..."
-msgstr "Starter transkription..."
-
-#: buzz/settings/shortcut.py
-msgid "Open Record Window"
-msgstr "Åben optagevinduet"
-
-#: buzz/settings/shortcut.py
-msgid "Import File"
-msgstr "Import fil"
-
-#: buzz/settings/shortcut.py
-msgid "Open Preferences Window"
-msgstr "Åben indstillingsvinduet"
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Text"
-msgstr "Vis transkriberede tekst"
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Translation"
-msgstr "Vis transkriberede oversættelse"
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Timestamps"
-msgstr "Vis transkriptionstidstempler "
-
-#: buzz/settings/shortcut.py
-msgid "Search Transcript"
-msgstr "Søg i transskription"
-
-#: buzz/settings/shortcut.py
-msgid "Go to Next Transcript Search Result"
-msgstr "Gå til næste søgeresultat i transskription"
-
-#: buzz/settings/shortcut.py
-msgid "Go to Previous Transcript Search Result"
-msgstr "Gå til forrige søgeresultat i transskription"
-
-#: buzz/settings/shortcut.py
-msgid "Scroll to Current Text"
-msgstr "Rul til aktuel tekst"
-
-#: buzz/settings/shortcut.py
-msgid "Play/Pause Audio"
-msgstr "Afspil/Pause lyd"
-
-#: buzz/settings/shortcut.py
-msgid "Replay Current Segment"
-msgstr "Afspil nuværende segment igen"
-
-#: buzz/settings/shortcut.py
-msgid "Toggle Playback Controls"
-msgstr "Skift afspilningskontroller"
-
-#: buzz/settings/shortcut.py
-msgid "Decrease Segment Start Time"
-msgstr "Reducer segmentets starttidspunkt"
-
-#: buzz/settings/shortcut.py
-msgid "Increase Segment Start Time"
-msgstr "Øg segmentets starttidspunkt"
-
-#: buzz/settings/shortcut.py
-msgid "Decrease Segment End Time"
-msgstr "Reducer segmentets sluttidspunkt"
-
-#: buzz/settings/shortcut.py
-msgid "Increase Segment End Time"
-msgstr "Øg segmentets sluttidspunkt"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append below"
-msgstr "Tilføj herunder"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append above"
-msgstr "Tilføj herover"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append and correct"
-msgstr "Tilføj og ret"
-
-#: buzz/file_transcriber_queue_worker.py
-msgid ""
-"Speech extraction failed! Check your internet connection — a model may need "
-"to be downloaded."
-msgstr ""
-"Taleoprydning mislykkedes! Kontroller din internetforbindelse — en model "
-"skal muligvis hentes ned."
-
-#~ msgid "Comma-separated, e.g. \"0.0, 0.2, 0.4, 0.6, 0.8, 1.0\""
-#~ msgstr "Komma-separerede, fx., \"0.0, 0.2, 0.4, 0.6, 0.8, 1.0\""
-
-#~ msgid "Temperature:"
-#~ msgstr "Temperatur:"
-
-#~ msgid "Please translate each text sent to you from English to Spanish."
-#~ msgstr "Oversæt venligst hver tekst du modtager fra engelsk til spansk."
diff --git a/buzz/locale/de_DE/LC_MESSAGES/buzz.po b/buzz/locale/de_DE/LC_MESSAGES/buzz.po
deleted file mode 100644
index d7a796de..00000000
--- a/buzz/locale/de_DE/LC_MESSAGES/buzz.po
+++ /dev/null
@@ -1,1686 +0,0 @@
-# Copyright (C) 2025 THE PACKAGE'S COPYRIGHT HOLDER
-# This file is distributed under the same license as the PACKAGE package.
-# Automatically generated, 2025.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: \n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2026-03-07 20:20+0200\n"
-"PO-Revision-Date: 2025-03-05 14:41+0100\n"
-"Last-Translator: \n"
-"Language-Team: \n"
-"Language: de_DE@formal\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Generator: Poedit 3.5\n"
-
-#: buzz/widgets/import_url_dialog.py buzz/settings/shortcut.py
-msgid "Import URL"
-msgstr "URL importieren"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "https://example.com/audio.mp3"
-msgstr "https://example.com/audio.mp3"
-
-#: buzz/widgets/import_url_dialog.py
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-#: buzz/widgets/main_window.py
-msgid "Ok"
-msgstr "OK"
-
-#: buzz/widgets/import_url_dialog.py
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-#: buzz/widgets/model_download_progress_dialog.py buzz/widgets/main_window.py
-msgid "Cancel"
-msgstr "Abbrechen"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "URL:"
-msgstr "URL:"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "Invalid URL"
-msgstr "Ungültige URL"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "The URL you entered is invalid."
-msgstr "Die von Ihnen eingegebene URL ist ungültig."
-
-#: buzz/widgets/presentation_window.py
-msgid "Live Transcript Presentation"
-msgstr "Live-Transkript-Präsentation"
-
-#: buzz/widgets/preferences_dialog/shortcuts_editor_preferences_widget.py
-msgid "Reset to Defaults"
-msgstr "Auf Standardeinstellungen zurücksetzen"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "English"
-msgstr "Englisch"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Catalan"
-msgstr "Katalanisch"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Danish"
-msgstr "Dänisch"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Dutch"
-msgstr "Niederländisch"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "German"
-msgstr "Deutsch"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Spanish"
-msgstr "Spanisch"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Italian"
-msgstr "Italienisch"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Japanese"
-msgstr "Japanisch"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Latvian"
-msgstr "Lettisch"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Polish"
-msgstr "Polnisch"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Portuguese (Brazil)"
-msgstr "Portugiesisch (Brasilien)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Ukrainian"
-msgstr "Ukrainisch"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Chinese (Simplified)"
-msgstr "Chinesisch (vereinfacht)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Chinese (Traditional)"
-msgstr "Chinesisch (traditionell)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Restart required!"
-msgstr "Neustart erforderlich!"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Ui Language"
-msgstr "Sprache der Benutzeroberfläche"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Font Size"
-msgstr "Schriftgröße"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Test"
-msgstr "Test"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API key"
-msgstr "OpenAI-API-Schlüssel"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI base url"
-msgstr "OpenAI-Basis-URL"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API model"
-msgstr "OpenAI-API-Modell"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Default export file name"
-msgstr "Standardname der Exportdatei"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Enable live recording transcription export"
-msgstr "Export von Live-Aufnahmetranskriptionen aktivieren"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Browse"
-msgstr "Durchsuchen"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Export folder"
-msgstr "Exportordner"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Live recording mode"
-msgstr "Live-Aufnahmemodus"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Note: Live recording export settings will be moved to the Advanced Settings "
-"in the Live Recording screen in a future version."
-msgstr ""
-"Hinweis: Die Exporteinstellungen für Live-Aufnahmen werden in einer "
-"zukünftigen Version in die Erweiterten Einstellungen im Live-Aufnahme-"
-"Bildschirm verschoben."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Use 8-bit quantization to reduce memory usage"
-msgstr "8-Bit-Quantisierung zur Reduzierung des Speicherverbrauchs verwenden"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Applies to Huggingface and Faster Whisper models. Reduces GPU memory usage "
-"but may slightly decrease transcription quality."
-msgstr ""
-"Gilt für Huggingface- und Faster Whisper-Modelle. Reduziert den GPU-"
-"Speicherverbrauch, kann jedoch die Transkriptionsqualität leicht verringern."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Reduce GPU RAM"
-msgstr "GPU-Arbeitsspeicher reduzieren"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Use only CPU and disable GPU acceleration"
-msgstr "Nur CPU verwenden und GPU-Beschleunigung deaktivieren"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Set this if larger models do not fit your GPU memory and Buzz crashes"
-msgstr ""
-"Diese Option aktivieren, wenn größere Modelle nicht in den GPU-Speicher "
-"passen und Buzz abstürzt"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Disable GPU"
-msgstr "GPU deaktivieren"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API Key Test"
-msgstr "OpenAI-API-Schlüssel Test"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Your API key is valid. Buzz will use this key to perform Whisper API "
-"transcriptions and AI translations."
-msgstr ""
-"Ihr API-Schlüssel ist gültig. Buzz verwendet diesen Schlüssel, um Whisper-"
-"API-Transkriptionen und KI-Übersetzungen durchzuführen."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Invalid API key"
-msgstr "Ungültiger API-Schlüssel"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"API supports only base64 characters (A-Za-z0-9+/=_-). Other characters in "
-"API key may cause errors."
-msgstr ""
-"Die API unterstützt nur Base64-Zeichen (A-Za-z0-9+/=_-). Andere Zeichen im "
-"API-Schlüssel können Fehler verursachen."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Select Export Folder"
-msgstr "Exportordner auswählen"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"OpenAI API returned invalid response. Please check the API url or your key. "
-"Transcription and translation may still work if the API does not support key "
-"validation."
-msgstr ""
-"Die OpenAI-API hat eine ungültige Antwort zurückgegeben. Bitte überprüfen "
-"Sie die API-URL oder Ihren Schlüssel. Transkription und Übersetzung "
-"funktionieren möglicherweise weiterhin, wenn die API keine "
-"Schlüsselvalidierung unterstützt."
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Enable folder watch"
-msgstr "Ordner überwachen aktivieren"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Delete processed files"
-msgstr "Verarbeitete Dateien löschen"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Input folder"
-msgstr "Eingabeordner"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Output folder"
-msgstr "Ausgabeordner"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Select Input Folder"
-msgstr "Eingabeordner auswählen"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Select Output Folder"
-msgstr "Ausgabeordner auswählen"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Preferences"
-msgstr "Einstellungen"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "General"
-msgstr "Allgemein"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Models"
-msgstr "Modelle"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Shortcuts"
-msgstr "Tastenkombinationen"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Folder Watch"
-msgstr "Ordner überwachen"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Group"
-msgstr "Gruppe"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Huggingface ID of a Faster whisper model"
-msgstr "Huggingface-ID eines Faster Whisper-Modells"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download"
-msgstr "Herunterladen"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Show file location"
-msgstr "Dateispeicherort anzeigen"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Delete"
-msgstr "Löschen"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Downloaded"
-msgstr "Heruntergeladen"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Available for Download"
-msgstr "Zum Herunterladen verfügbar"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download link to Whisper.cpp ggml model file"
-msgstr "Laden Sie den Link zur ggml-Modelldatei Whisper.cpp herunter"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Delete Model"
-msgstr "Modell löschen"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Are you sure you want to delete the selected model?"
-msgstr "Sind Sie sicher, dass Sie das ausgewählte Modell löschen möchten?"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download failed"
-msgstr "Der Download ist fehlgeschlagen"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcription_tasks_table_widget.py
-#: buzz/widgets/update_dialog.py buzz/widgets/main_window.py
-#: buzz/model_loader.py
-msgid "Error"
-msgstr "Fehler"
-
-#: buzz/widgets/record_button.py buzz/widgets/main_window_toolbar.py
-msgid "Record"
-msgstr "Aufnehmen"
-
-#: buzz/widgets/record_button.py
-msgid "Stop"
-msgstr "Stoppen"
-
-#: buzz/widgets/transcriber/languages_combo_box.py
-#: buzz/transcriber/transcriber.py
-msgid "Detect Language"
-msgstr "Sprache erkennen"
-
-#: buzz/widgets/transcriber/mms_language_line_edit.py
-msgid "e.g., eng, fra, deu"
-msgstr "z.B. eng, fra, deu"
-
-#: buzz/widgets/transcriber/mms_language_line_edit.py
-msgid ""
-"Enter an ISO 639-3 language code (3 letters).\n"
-"Examples: eng (English), fra (French), deu (German),\n"
-"spa (Spanish), lav (Latvian)"
-msgstr ""
-"Geben Sie einen ISO 639-3-Sprachcode (3 Buchstaben) ein.\n"
-"Beispiele: eng (Englisch), fra (Französisch), deu (Deutsch),\n"
-"spa (Spanisch), lav (Lettisch)"
-
-#: buzz/widgets/transcriber/file_transcriber_widget.py
-msgid "Run"
-msgstr "Ausführen"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Model:"
-msgstr "Modell:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-#: buzz/transcriber/recording_transcriber.py
-msgid "First time use of a model may take up to several minutest to load."
-msgstr ""
-"Bei der ersten Verwendung eines Modells kann das Laden mehrere Minuten "
-"dauern."
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Api Key:"
-msgstr "API-Schlüssel:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Task:"
-msgstr "Aufgabe:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Language:"
-msgstr "Sprache:"
-
-#: buzz/widgets/transcriber/initial_prompt_text_edit.py
-msgid "Enter prompt..."
-msgstr "Anweisung eingeben..."
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Advanced Settings"
-msgstr "Erweiterte Einstellungen"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Speech recognition settings"
-msgstr "Einstellungen für die Spracherkennung"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Initial Prompt:"
-msgstr "Erste Anweisung:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Translation settings"
-msgstr "Übersetzungseinstellungen"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Enable AI translation"
-msgstr "KI-Übersetzung aktivieren"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "AI model:"
-msgstr "KI-Modell:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid ""
-"Please translate each text sent to you from English to Spanish. Translation "
-"will be used in an automated system, please do not add any comments or "
-"notes, just the translation."
-msgstr ""
-"Bitte übersetzen Sie jeden Text, der Ihnen gesendet wird, vom Englischen ins "
-"Spanische. Die Übersetzung wird in einem automatisierten System verwendet. "
-"Bitte fügen Sie keine Kommentare oder Anmerkungen hinzu, nur die Übersetzung."
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Instructions for AI:"
-msgstr "Anweisung zur KI:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Recording settings"
-msgstr "Aufnahmeeinstellungen"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Silence threshold:"
-msgstr "Stille-Schwellenwert:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Live recording mode:"
-msgstr "Live-Aufnahmemodus:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Line separator:"
-msgstr "Zeilentrenner:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Transcription step:"
-msgstr "Transkriptionsschritt:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Hide unconfirmed"
-msgstr "Unbestätigtes ausblenden"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Enable live recording export"
-msgstr "Export von Live-Aufnahmen aktivieren"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export folder:"
-msgstr "Exportordner:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export file name:"
-msgstr "Exportdateiname:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Text file (.txt)"
-msgstr "Textdatei (.txt)"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "CSV (.csv)"
-msgstr "CSV (.csv)"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export file type:"
-msgstr "Exportdateityp:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid ""
-"Limit export entries\n"
-"(0 = export all):"
-msgstr ""
-"Exporteinträge begrenzen\n"
-"(0 = alle exportieren):"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Word-level timings"
-msgstr "Zeitangaben auf Wortebene"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Extract speech"
-msgstr "Sprache extrahieren"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Export:"
-msgstr "Export:"
-
-#: buzz/widgets/transcriber/hugging_face_search_line_edit.py
-msgid "Huggingface ID of a model"
-msgstr "Huggingface-ID eines Models"
-
-#: buzz/widgets/transcriber/advanced_settings_button.py
-msgid "Advanced..."
-msgstr "Erweitert..."
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "New File Transcription"
-msgstr "Neue Dateitranskription"
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "New URL Transcription"
-msgstr "Neue URL-Transkription"
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "Open Transcript"
-msgstr "Transkript öffnen"
-
-#: buzz/widgets/main_window_toolbar.py buzz/settings/shortcut.py
-msgid "Cancel Transcription"
-msgstr "Transkription abbrechen"
-
-#: buzz/widgets/main_window_toolbar.py buzz/widgets/main_window.py
-#: buzz/settings/shortcut.py
-msgid "Clear History"
-msgstr "Verlauf löschen"
-
-#: buzz/widgets/main_window_toolbar.py buzz/widgets/update_dialog.py
-msgid "Update Available"
-msgstr "Update verfügbar"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "In Progress"
-msgstr "Im Gange"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Completed"
-msgstr "Fertiggestellt"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Failed"
-msgstr "Fehlgeschlagen"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Canceled"
-msgstr "Abgebrochen"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Queued"
-msgstr "In der Warteschlange"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "File Name / URL"
-msgstr "Dateiname/URL"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Model"
-msgstr "Modell"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Task"
-msgstr "Aufgabe"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Status"
-msgstr "Status"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Date Completed"
-msgstr "Datum abgeschlossen"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Date Added"
-msgstr "Datum hinzugefügt"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Notes"
-msgstr "Notizen"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Reset Column Order"
-msgstr "Spaltenreihenfolge zurücksetzen"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Restart Transcription"
-msgstr "Transkription neu starten"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Rename"
-msgstr "Umbenennen"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Add/Edit Notes"
-msgstr "Notizen hinzufügen/bearbeiten"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Rename Transcription"
-msgstr "Transkription umbenennen"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Enter new name:"
-msgstr "Neuen Namen eingeben:"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Enter some relevant notes for this transcription:"
-msgstr "Relevante Notizen für diese Transkription eingeben:"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Cannot Restart"
-msgstr "Neustart nicht möglich"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Only failed or canceled transcriptions can be restarted."
-msgstr ""
-"Nur fehlgeschlagene oder abgebrochene Transkriptionen können neu gestartet "
-"werden."
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Failed to restart transcription: {}"
-msgstr "Fehler beim Neustart der Transkription: {}"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid ""
-"Could not restart transcription: model not available and could not be "
-"downloaded."
-msgstr ""
-"Transkription konnte nicht neu gestartet werden: Modell nicht verfügbar und "
-"konnte nicht heruntergeladen werden."
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Could not restart transcription: transcriber worker not found."
-msgstr ""
-"Transkription konnte nicht neu gestartet werden: Transkriptions-Worker nicht "
-"gefunden."
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Live Recording"
-msgstr "Live-Aufnahme"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Click Record to begin..."
-msgstr "Auf Aufnehmen klicken um zu beginnen …"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Waiting for AI translation..."
-msgstr "Warten auf KI-Übersetzung..."
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Microphone:"
-msgstr "Mikrofon:"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Show in new window"
-msgstr "In neuem Fenster anzeigen"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Text Size:"
-msgstr "Textgröße:"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Theme"
-msgstr "Design"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Light"
-msgstr "Hell"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Dark"
-msgstr "Dunkel"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Custom"
-msgstr "Benutzerdefiniert"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Text Color"
-msgstr "Textfarbe"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Background Color"
-msgstr "Hintergrundfarbe"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Fullscreen"
-msgstr "Vollbild"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy"
-msgstr "Kopieren"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy transcription to clipboard"
-msgstr "Transkription in die Zwischenablage kopieren"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Nothing to copy!"
-msgstr "Nichts zum Kopieren vorhanden!"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy failed"
-msgstr "Kopieren fehlgeschlagen"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copied!"
-msgstr "Kopiert!"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Select Text Color"
-msgstr "Textfarbe auswählen"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Select Background Color"
-msgstr "Hintergrundfarbe auswählen"
-
-#: buzz/widgets/recording_transcriber_widget.py
-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
-msgid ""
-"Please check your audio devices or check the application logs for more "
-"information."
-msgstr ""
-"Bitte überprüfen Sie Ihre Audiogeräte oder prüfen Sie die "
-"Anwendungsprotokolle für weitere Informationen."
-
-#: buzz/widgets/update_dialog.py
-msgid "A new version of Buzz is available!"
-msgstr "Eine neue Version von Buzz ist verfügbar!"
-
-#: buzz/widgets/update_dialog.py
-msgid "Current version:"
-msgstr "Aktuelle Version:"
-
-#: buzz/widgets/update_dialog.py
-msgid "New version:"
-msgstr "Neue Version:"
-
-#: buzz/widgets/update_dialog.py
-msgid "Release Notes:"
-msgstr "Versionshinweise:"
-
-#: buzz/widgets/update_dialog.py
-msgid "Download and Install"
-msgstr "Herunterladen und installieren"
-
-#: buzz/widgets/update_dialog.py
-msgid "No download URL available for your platform."
-msgstr "Kein Download-Link für Ihre Plattform verfügbar."
-
-#: buzz/widgets/update_dialog.py
-msgid "Downloading file {} of {}..."
-msgstr "Datei {} von {} wird heruntergeladen..."
-
-#: buzz/widgets/update_dialog.py
-msgid "Downloading file {} of {} ({:.1f} MB / {:.1f} MB)..."
-msgstr "Datei {} von {} wird heruntergeladen ({:.1f} MB / {:.1f} MB)..."
-
-#: buzz/widgets/update_dialog.py
-msgid "Download Failed"
-msgstr "Download fehlgeschlagen"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to download the update: {}"
-msgstr "Das Update konnte nicht heruntergeladen werden: {}"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to save the installer: {}"
-msgstr "Installer konnte nicht gespeichert werden: {}"
-
-#: buzz/widgets/update_dialog.py
-msgid "Download complete!"
-msgstr "Download abgeschlossen!"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to run the installer: {}"
-msgstr "Installer konnte nicht ausgeführt werden: {}"
-
-#: buzz/widgets/about_dialog.py
-msgid "Check for updates"
-msgstr "Nach Updates suchen"
-
-#: buzz/widgets/about_dialog.py
-msgid "Show logs"
-msgstr "Protokolle anzeigen"
-
-#: buzz/widgets/about_dialog.py
-msgid "You're up to date!"
-msgstr "Sie sind auf dem Laufenden!"
-
-#: buzz/widgets/audio_meter_widget.py
-msgid "Average volume"
-msgstr "Durchschnittliche Lautstärke"
-
-#: buzz/widgets/audio_meter_widget.py
-msgid "Queue"
-msgstr "Warteschlange"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-msgid "Start"
-msgstr "Start"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-msgid "End"
-msgstr "Ende"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Text"
-msgstr "Text"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Translation"
-msgstr "Übersetzung"
-
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-msgid "View"
-msgstr "Anzeigen"
-
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-msgid "Timestamps"
-msgstr "Zeitstempel"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Export"
-msgstr "Export"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Translate"
-msgstr "Übersetzen"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Resize"
-msgstr "Größe ändern"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Identify Speakers"
-msgstr "Sprecher identifizieren"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Find"
-msgstr "Suchen"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Show/Hide Search Bar (Ctrl+F)"
-msgstr "Suchleiste ein-/ausblenden (Strg+F)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Find:"
-msgstr "Suchen:"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Enter text to find..."
-msgstr "Suchtext eingeben..."
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Previous match (Shift+Enter)"
-msgstr "Vorheriges Ergebnis (Umschalt+Eingabe)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Next match (Ctrl+Enter)"
-msgstr "Nächstes Ergebnis (Strg+Eingabe)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Clear"
-msgstr "Löschen"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Playback Controls:"
-msgstr "Wiedergabesteuerung:"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Loop Segment"
-msgstr "Segment wiederholen"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Enable/disable looping when clicking on transcript segments"
-msgstr ""
-"Wiederholen beim Klicken auf Transkript-Segmente aktivieren/deaktivieren"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Follow Audio"
-msgstr "Audio folgen"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid ""
-"Enable/disable following the current audio position in the transcript. When "
-"enabled, automatically scrolls to current text."
-msgstr ""
-"Aktuelle Audioposition im Transkript verfolgen aktivieren/deaktivieren. Bei "
-"Aktivierung wird automatisch zum aktuellen Text gescrollt."
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Scroll to Current"
-msgstr "Zur aktuellen Stelle scrollen"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Scroll to the currently spoken text"
-msgstr "Zum aktuell gesprochenen Text scrollen"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "1 of 100+ matches"
-msgstr "1 von 100+ Treffern"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "1 of "
-msgstr "1 von "
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " matches"
-msgstr " Treffer"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "No matches found"
-msgstr "Keine Treffer gefunden"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " of 100+ matches"
-msgstr " von 100+ Treffern"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " of "
-msgstr " von "
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "API Key Required"
-msgstr "API-Schlüssel erforderlich"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Please enter OpenAI API Key in preferences"
-msgstr "Bitte geben Sie den OpenAI-API-Schlüssel in den Einstellungen ein"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend end time"
-msgstr "Endzeit verlängern"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend endings by up to (seconds)"
-msgstr "Enden um bis zu (Sekunden) verlängern"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend endings"
-msgstr "Enden verlängern"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Resize Options"
-msgstr "Größenänderungsoptionen"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Desired subtitle length"
-msgstr "Gewünschte Untertitellänge"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Available only if word level timings were disabled during transcription"
-msgstr ""
-"Nur verfügbar, wenn Zeitangaben auf Wortebene bei der Transkription "
-"deaktiviert waren"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge Options"
-msgstr "Zusammenführungsoptionen"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge by gap"
-msgstr "Nach Abstand zusammenführen"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Split by punctuation"
-msgstr "Durch Satzzeichen getrennt"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Split by max length"
-msgstr "Aufgeteilt nach maximaler Länge"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge"
-msgstr "Vereinigen"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Available only if word level timings were enabled during transcription"
-msgstr ""
-"Nur verfügbar, wenn Zeitangaben auf Wortebene bei der Transkription "
-"aktiviert waren"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid ""
-"Speaker identification is not available: failed to load required libraries."
-msgstr ""
-"Sprecheridentifikation nicht verfügbar: Erforderliche Bibliotheken konnten "
-"nicht geladen werden."
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "1/8 Collecting transcripts"
-msgstr "1/8 Transkripte werden gesammelt"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "2/8 Loading audio"
-msgstr "2/8 Audio wird geladen"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "3/8 Loading alignment model"
-msgstr "3/8 Ausrichtungsmodell wird geladen"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "3/8 Loading alignment model (retrying with cache...)"
-msgstr "3/8 Ausrichtungsmodell wird geladen (Wiederholung mit Cache...)"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid ""
-"Failed to load alignment model. Please check your internet connection and "
-"try again."
-msgstr ""
-"Ausrichtungsmodell konnte nicht geladen werden. Bitte überprüfen Sie Ihre "
-"Internetverbindung und versuchen Sie es erneut."
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "4/8 Processing audio"
-msgstr "4/8 Audio wird verarbeitet"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "5/8 Preparing transcripts"
-msgstr "5/8 Transkripte werden vorbereitet"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "6/8 Identifying speakers"
-msgstr "6/8 Sprecher werden identifiziert"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "7/8 Mapping speakers to transcripts"
-msgstr "7/8 Sprecher werden den Transkripten zugeordnet"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "8/8 Identification done"
-msgstr "8/8 Identifikation abgeschlossen"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "0/0 Error identifying speakers"
-msgstr "0/0 Fehler bei der Sprecheridentifikation"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Step 1: Identify speakers"
-msgstr "Schritt 1: Sprecher identifizieren"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Identify"
-msgstr "Identifizieren"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Ready to identify speakers"
-msgstr "Bereit zur Sprecheridentifikation"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Audio file not found"
-msgstr "Audiodatei nicht gefunden"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Step 2: Name speakers"
-msgstr "Schritt 2: Sprecher benennen"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Play sample"
-msgstr "Beispiel abspielen"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Merge speaker sentences"
-msgstr "Sprechersätze zusammenführen"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Save"
-msgstr "Speichern"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Cancelling..."
-msgstr "Wird abgebrochen..."
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Cancelled"
-msgstr "Abgebrochen"
-
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Save File"
-msgstr "Datei speichern"
-
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Text files"
-msgstr "Textdateien"
-
-#: buzz/widgets/model_download_progress_dialog.py
-msgid "Downloading model"
-msgstr "Modell wird heruntergeladen"
-
-#: buzz/widgets/model_download_progress_dialog.py
-msgid "remaining"
-msgstr "verbleibend"
-
-#: buzz/widgets/menu_bar.py
-msgid "Import File..."
-msgstr "Datei importieren..."
-
-#: buzz/widgets/menu_bar.py
-msgid "Import URL..."
-msgstr "URL importieren..."
-
-#: buzz/widgets/menu_bar.py
-msgid "Import Folder..."
-msgstr "Ordner importieren..."
-
-#: buzz/widgets/menu_bar.py
-msgid "About"
-msgstr "Über"
-
-#: buzz/widgets/menu_bar.py
-msgid "Preferences..."
-msgstr "Einstellungen..."
-
-#: buzz/widgets/menu_bar.py
-msgid "Help"
-msgstr "Hilfe"
-
-#: buzz/widgets/menu_bar.py
-msgid "File"
-msgstr "Datei"
-
-#: buzz/widgets/main_window.py
-msgid ""
-"Are you sure you want to delete the selected transcription(s)? This action "
-"cannot be undone."
-msgstr ""
-"Sind Sie sicher, dass Sie die ausgewählte(n) Transkription(en) löschen "
-"möchten? Diese Aktion kann nicht rückgängig gemacht werden."
-
-#: buzz/widgets/main_window.py
-msgid "Select audio file"
-msgstr "Audiodatei auswählen"
-
-#: buzz/widgets/main_window.py
-msgid "Select folder"
-msgstr "Eingabeordner auswählen"
-
-#: buzz/widgets/main_window.py
-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
-#: buzz/transcriber/recording_transcriber.py
-msgid "Whisper server failed to start. Check logs for details."
-msgstr ""
-"Whisper-Server konnte nicht gestartet werden. Details in den Protokollen "
-"prüfen."
-
-#: buzz/transcriber/local_whisper_cpp_server_transcriber.py
-#: buzz/transcriber/recording_transcriber.py
-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-Server konnte aufgrund von unzureichendem Arbeitsspeicher nicht "
-"gestartet werden. Bitte versuchen Sie es mit einem kleineren Modell erneut. "
-"Um den CPU-Modus zu erzwingen, verwenden Sie die Umgebungsvariable "
-"BUZZ_FORCE_CPU=TRUE."
-
-#: buzz/transcriber/transcriber.py
-msgid "Translate to English"
-msgstr "Ins Englische übersetzen"
-
-#: buzz/transcriber/transcriber.py
-msgid "Transcribe"
-msgstr "Transkribieren"
-
-#: buzz/transcriber/transcriber.py
-msgid "Chinese"
-msgstr "Chinesisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Russian"
-msgstr "Russisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Korean"
-msgstr "Koreanisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "French"
-msgstr "Französisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Portuguese"
-msgstr "Portugiesisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Turkish"
-msgstr "Türkisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Arabic"
-msgstr "Arabisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Swedish"
-msgstr "Schwedisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Indonesian"
-msgstr "Indonesisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hindi"
-msgstr "Hindi"
-
-#: buzz/transcriber/transcriber.py
-msgid "Finnish"
-msgstr "Finnisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Vietnamese"
-msgstr "Vietnamesisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hebrew"
-msgstr "Hebräisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Greek"
-msgstr "Griechisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malay"
-msgstr "Malaiisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Czech"
-msgstr "Tschechisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Romanian"
-msgstr "Rumänisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hungarian"
-msgstr "Ungarisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tamil"
-msgstr "Tamilisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Norwegian"
-msgstr "Norwegisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Thai"
-msgstr "Thailändisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Urdu"
-msgstr "Urdu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Croatian"
-msgstr "Kroatisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bulgarian"
-msgstr "Bulgarisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lithuanian"
-msgstr "Litauisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Latin"
-msgstr "Latein"
-
-#: buzz/transcriber/transcriber.py
-msgid "Maori"
-msgstr "Maori"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malayalam"
-msgstr "Malayalam"
-
-#: buzz/transcriber/transcriber.py
-msgid "Welsh"
-msgstr "Walisisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Slovak"
-msgstr "Slowakisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Telugu"
-msgstr "Telugu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Persian"
-msgstr "Persisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bengali"
-msgstr "Bengalisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Serbian"
-msgstr "Serbisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Azerbaijani"
-msgstr "Aserbaidschanisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Slovenian"
-msgstr "Slowenisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Kannada"
-msgstr "Kannada"
-
-#: buzz/transcriber/transcriber.py
-msgid "Estonian"
-msgstr "Estnisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Macedonian"
-msgstr "Mazedonisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Breton"
-msgstr "Bretonisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Basque"
-msgstr "Baskisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Icelandic"
-msgstr "Isländisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Armenian"
-msgstr "Armenisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Nepali"
-msgstr "Nepali"
-
-#: buzz/transcriber/transcriber.py
-msgid "Mongolian"
-msgstr "Mongolisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bosnian"
-msgstr "Bosnisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Kazakh"
-msgstr "Kasachisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Albanian"
-msgstr "Albanisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Swahili"
-msgstr "Suaheli"
-
-#: buzz/transcriber/transcriber.py
-msgid "Galician"
-msgstr "Galizisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Marathi"
-msgstr "Marathi"
-
-#: buzz/transcriber/transcriber.py
-msgid "Punjabi"
-msgstr "Punjabi"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sinhala"
-msgstr "Singhalesisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Khmer"
-msgstr "Khmer"
-
-#: buzz/transcriber/transcriber.py
-msgid "Shona"
-msgstr "Schona"
-
-#: buzz/transcriber/transcriber.py
-msgid "Yoruba"
-msgstr "Yoruba"
-
-#: buzz/transcriber/transcriber.py
-msgid "Somali"
-msgstr "Somali"
-
-#: buzz/transcriber/transcriber.py
-msgid "Afrikaans"
-msgstr "Afrikanisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Occitan"
-msgstr "Okzitanisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Georgian"
-msgstr "Georgisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Belarusian"
-msgstr "Belarussisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tajik"
-msgstr "Tadschikisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sindhi"
-msgstr "Sindhi"
-
-#: buzz/transcriber/transcriber.py
-msgid "Gujarati"
-msgstr "Gujarati"
-
-#: buzz/transcriber/transcriber.py
-msgid "Amharic"
-msgstr "Amharisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Yiddish"
-msgstr "Jiddisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lao"
-msgstr "Lao"
-
-#: buzz/transcriber/transcriber.py
-msgid "Uzbek"
-msgstr "Usbekisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Faroese"
-msgstr "Färöisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Haitian Creole"
-msgstr "Haitianisch-Kreolisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Pashto"
-msgstr "Paschtu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Turkmen"
-msgstr "Turkmenisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Nynorsk"
-msgstr "Nynorsk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Maltese"
-msgstr "Maltesisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sanskrit"
-msgstr "Sanskrit"
-
-#: buzz/transcriber/transcriber.py
-msgid "Luxembourgish"
-msgstr "Luxemburgisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Myanmar"
-msgstr "Myanmar"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tibetan"
-msgstr "Tibetisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tagalog"
-msgstr "Tagalog"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malagasy"
-msgstr "Madagassisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Assamese"
-msgstr "Assamisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tatar"
-msgstr "Tatar"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hawaiian"
-msgstr "Hawaiianisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lingala"
-msgstr "Lingala"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hausa"
-msgstr "Hausa"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bashkir"
-msgstr "Baschkirisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Javanese"
-msgstr "Javanisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sundanese"
-msgstr "Sundanesisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Cantonese"
-msgstr "Kantonesisch"
-
-#: buzz/transcriber/recording_transcriber.py buzz/model_loader.py
-msgid "A connection error occurred"
-msgstr "Ein Verbindungsfehler ist aufgetreten"
-
-#: buzz/transcriber/recording_transcriber.py
-msgid "Starting Whisper.cpp..."
-msgstr "Whisper.cpp wird gestartet..."
-
-#: buzz/transcriber/recording_transcriber.py
-msgid "Starting transcription..."
-msgstr "Transkription wird gestartet..."
-
-#: buzz/settings/shortcut.py
-msgid "Open Record Window"
-msgstr "Aufnahmefenster öffnen"
-
-#: buzz/settings/shortcut.py
-msgid "Import File"
-msgstr "Datei importieren"
-
-#: buzz/settings/shortcut.py
-msgid "Open Preferences Window"
-msgstr "Einstellungsfenster öffnen"
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Text"
-msgstr "Transkriptionstext anzeigen"
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Translation"
-msgstr "Übersetzung des Transkripts anzeigen"
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Timestamps"
-msgstr "Zeitstempel des Transkripts anzeigen"
-
-#: buzz/settings/shortcut.py
-msgid "Search Transcript"
-msgstr "Transkript durchsuchen"
-
-#: buzz/settings/shortcut.py
-msgid "Go to Next Transcript Search Result"
-msgstr "Zum nächsten Transkript-Suchergebnis gehen"
-
-#: buzz/settings/shortcut.py
-msgid "Go to Previous Transcript Search Result"
-msgstr "Zum vorherigen Transkript-Suchergebnis gehen"
-
-#: buzz/settings/shortcut.py
-msgid "Scroll to Current Text"
-msgstr "Zum aktuellen Text scrollen"
-
-#: buzz/settings/shortcut.py
-msgid "Play/Pause Audio"
-msgstr "Audio abspielen/pausieren"
-
-#: buzz/settings/shortcut.py
-msgid "Replay Current Segment"
-msgstr "Aktuelles Segment erneut abspielen"
-
-#: buzz/settings/shortcut.py
-msgid "Toggle Playback Controls"
-msgstr "Wiedergabesteuerung ein-/ausblenden"
-
-#: buzz/settings/shortcut.py
-msgid "Decrease Segment Start Time"
-msgstr "Segmentanfangszeit verringern"
-
-#: buzz/settings/shortcut.py
-msgid "Increase Segment Start Time"
-msgstr "Segmentanfangszeit erhöhen"
-
-#: buzz/settings/shortcut.py
-msgid "Decrease Segment End Time"
-msgstr "Segmentendzeit verringern"
-
-#: buzz/settings/shortcut.py
-msgid "Increase Segment End Time"
-msgstr "Segmentendzeit erhöhen"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append below"
-msgstr "Unten anhängen"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append above"
-msgstr "Oben anhängen"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append and correct"
-msgstr "Anhängen und korrigieren"
-
-#: buzz/file_transcriber_queue_worker.py
-msgid ""
-"Speech extraction failed! Check your internet connection — a model may need "
-"to be downloaded."
-msgstr ""
-"Sprachextraktion fehlgeschlagen! Bitte Internetverbindung prüfen — ein "
-"Modell muss möglicherweise heruntergeladen werden."
-
-#~ msgid "Comma-separated, e.g. \"0.0, 0.2, 0.4, 0.6, 0.8, 1.0\""
-#~ msgstr "Durch Kommas getrennt, z.B. \"0,0, 0,2, 0,4, 0,6, 0,8, 1,0\""
-
-#~ msgid "Temperature:"
-#~ msgstr "Temperatur:"
-
-#~ msgid "Please translate each text sent to you from English to Spanish."
-#~ msgstr ""
-#~ "Bitte übersetzen Sie jeden an Sie gesendeten Text von Englisch nach "
-#~ "Spanisch."
-
-#~ msgid "Translation error, see logs!"
-#~ msgstr "Übersetzungsfehler, Protokolle prüfen!"
-
-#~ msgid "Snap permission notice"
-#~ msgstr "Snap-Berechtigungsmitteilung"
-
-#~ msgid ""
-#~ "Detected missing permissions, please check that snap permissions have "
-#~ "been granted"
-#~ msgstr ""
-#~ "Es wurden fehlende Berechtigungen festgestellt. Bitte überprüfen Sie, ob "
-#~ "Snap-Berechtigungen erteilt wurden"
-
-#~ msgid ""
-#~ "To enable necessary permissions run the following commands in the terminal"
-#~ msgstr ""
-#~ "Um die erforderlichen Berechtigungen zu aktivieren, führen Sie die "
-#~ "folgenden Befehle im Terminal aus"
-
-#~ msgid "Close"
-#~ msgstr "Schließen"
-
-#~ msgid "Enter instructions for AI on how to translate..."
-#~ msgstr "Geben Sie Anweisungen für die KI zum Übersetzen ein..."
diff --git a/buzz/locale/en_US/LC_MESSAGES/buzz.po b/buzz/locale/en_US/LC_MESSAGES/buzz.po
deleted file mode 100644
index 644ce509..00000000
--- a/buzz/locale/en_US/LC_MESSAGES/buzz.po
+++ /dev/null
@@ -1,1596 +0,0 @@
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
-# This file is distributed under the same license as the PACKAGE package.
-# FIRST AUTHOR , YEAR.
-#
-#, fuzzy
-msgid ""
-msgstr ""
-"Project-Id-Version: PACKAGE VERSION\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2026-03-07 20:20+0200\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME \n"
-"Language-Team: LANGUAGE \n"
-"Language: \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-
-#: buzz/widgets/import_url_dialog.py buzz/settings/shortcut.py
-msgid "Import URL"
-msgstr ""
-
-#: buzz/widgets/import_url_dialog.py
-msgid "https://example.com/audio.mp3"
-msgstr ""
-
-#: buzz/widgets/import_url_dialog.py
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-#: buzz/widgets/main_window.py
-msgid "Ok"
-msgstr ""
-
-#: buzz/widgets/import_url_dialog.py
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-#: buzz/widgets/model_download_progress_dialog.py buzz/widgets/main_window.py
-msgid "Cancel"
-msgstr ""
-
-#: buzz/widgets/import_url_dialog.py
-msgid "URL:"
-msgstr ""
-
-#: buzz/widgets/import_url_dialog.py
-msgid "Invalid URL"
-msgstr ""
-
-#: buzz/widgets/import_url_dialog.py
-msgid "The URL you entered is invalid."
-msgstr ""
-
-#: buzz/widgets/presentation_window.py
-msgid "Live Transcript Presentation"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/shortcuts_editor_preferences_widget.py
-msgid "Reset to Defaults"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "English"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Catalan"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Danish"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Dutch"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "German"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Spanish"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Italian"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Japanese"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Latvian"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Polish"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Portuguese (Brazil)"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Ukrainian"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Chinese (Simplified)"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Chinese (Traditional)"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Restart required!"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Ui Language"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Font Size"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Test"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API key"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI base url"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API model"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Default export file name"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Enable live recording transcription export"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Browse"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Export folder"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Live recording mode"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Note: Live recording export settings will be moved to the Advanced Settings "
-"in the Live Recording screen in a future version."
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Use 8-bit quantization to reduce memory usage"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Applies to Huggingface and Faster Whisper models. Reduces GPU memory usage "
-"but may slightly decrease transcription quality."
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Reduce GPU RAM"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Use only CPU and disable GPU acceleration"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Set this if larger models do not fit your GPU memory and Buzz crashes"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Disable GPU"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API Key Test"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Your API key is valid. Buzz will use this key to perform Whisper API "
-"transcriptions and AI translations."
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Invalid API key"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"API supports only base64 characters (A-Za-z0-9+/=_-). Other characters in "
-"API key may cause errors."
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Select Export Folder"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"OpenAI API returned invalid response. Please check the API url or your key. "
-"Transcription and translation may still work if the API does not support key "
-"validation."
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Enable folder watch"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Delete processed files"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Input folder"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Output folder"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Select Input Folder"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Select Output Folder"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Preferences"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "General"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Models"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Shortcuts"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Folder Watch"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Group"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Huggingface ID of a Faster whisper model"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Show file location"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Delete"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Downloaded"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Available for Download"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download link to Whisper.cpp ggml model file"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Delete Model"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Are you sure you want to delete the selected model?"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download failed"
-msgstr ""
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcription_tasks_table_widget.py
-#: buzz/widgets/update_dialog.py buzz/widgets/main_window.py
-#: buzz/model_loader.py
-msgid "Error"
-msgstr ""
-
-#: buzz/widgets/record_button.py buzz/widgets/main_window_toolbar.py
-msgid "Record"
-msgstr ""
-
-#: buzz/widgets/record_button.py
-msgid "Stop"
-msgstr ""
-
-#: buzz/widgets/transcriber/languages_combo_box.py
-#: buzz/transcriber/transcriber.py
-msgid "Detect Language"
-msgstr ""
-
-#: buzz/widgets/transcriber/mms_language_line_edit.py
-msgid "e.g., eng, fra, deu"
-msgstr ""
-
-#: buzz/widgets/transcriber/mms_language_line_edit.py
-msgid ""
-"Enter an ISO 639-3 language code (3 letters).\n"
-"Examples: eng (English), fra (French), deu (German),\n"
-"spa (Spanish), lav (Latvian)"
-msgstr ""
-
-#: buzz/widgets/transcriber/file_transcriber_widget.py
-msgid "Run"
-msgstr ""
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Model:"
-msgstr ""
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-#: buzz/transcriber/recording_transcriber.py
-msgid "First time use of a model may take up to several minutest to load."
-msgstr ""
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Api Key:"
-msgstr ""
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Task:"
-msgstr ""
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Language:"
-msgstr ""
-
-#: buzz/widgets/transcriber/initial_prompt_text_edit.py
-msgid "Enter prompt..."
-msgstr ""
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Advanced Settings"
-msgstr ""
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Speech recognition settings"
-msgstr ""
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Initial Prompt:"
-msgstr ""
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Translation settings"
-msgstr ""
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Enable AI translation"
-msgstr ""
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "AI model:"
-msgstr ""
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid ""
-"Please translate each text sent to you from English to Spanish. Translation "
-"will be used in an automated system, please do not add any comments or "
-"notes, just the translation."
-msgstr ""
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Instructions for AI:"
-msgstr ""
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Recording settings"
-msgstr ""
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Silence threshold:"
-msgstr ""
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Live recording mode:"
-msgstr ""
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Line separator:"
-msgstr ""
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Transcription step:"
-msgstr ""
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Hide unconfirmed"
-msgstr "Hide unconfirmed"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Enable live recording export"
-msgstr ""
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export folder:"
-msgstr ""
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export file name:"
-msgstr ""
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Text file (.txt)"
-msgstr ""
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "CSV (.csv)"
-msgstr ""
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export file type:"
-msgstr ""
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid ""
-"Limit export entries\n"
-"(0 = export all):"
-msgstr ""
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Word-level timings"
-msgstr ""
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Extract speech"
-msgstr ""
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Export:"
-msgstr ""
-
-#: buzz/widgets/transcriber/hugging_face_search_line_edit.py
-msgid "Huggingface ID of a model"
-msgstr ""
-
-#: buzz/widgets/transcriber/advanced_settings_button.py
-msgid "Advanced..."
-msgstr ""
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "New File Transcription"
-msgstr ""
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "New URL Transcription"
-msgstr ""
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "Open Transcript"
-msgstr ""
-
-#: buzz/widgets/main_window_toolbar.py buzz/settings/shortcut.py
-msgid "Cancel Transcription"
-msgstr ""
-
-#: buzz/widgets/main_window_toolbar.py buzz/widgets/main_window.py
-#: buzz/settings/shortcut.py
-msgid "Clear History"
-msgstr ""
-
-#: buzz/widgets/main_window_toolbar.py buzz/widgets/update_dialog.py
-msgid "Update Available"
-msgstr ""
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "In Progress"
-msgstr ""
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Completed"
-msgstr ""
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Failed"
-msgstr ""
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Canceled"
-msgstr ""
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Queued"
-msgstr ""
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "File Name / URL"
-msgstr ""
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Model"
-msgstr ""
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Task"
-msgstr ""
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Status"
-msgstr ""
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Date Completed"
-msgstr ""
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Date Added"
-msgstr ""
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Notes"
-msgstr ""
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Reset Column Order"
-msgstr ""
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Restart Transcription"
-msgstr ""
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Rename"
-msgstr ""
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Add/Edit Notes"
-msgstr ""
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Rename Transcription"
-msgstr ""
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Enter new name:"
-msgstr ""
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Enter some relevant notes for this transcription:"
-msgstr ""
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Cannot Restart"
-msgstr ""
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Only failed or canceled transcriptions can be restarted."
-msgstr ""
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Failed to restart transcription: {}"
-msgstr ""
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid ""
-"Could not restart transcription: model not available and could not be "
-"downloaded."
-msgstr ""
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Could not restart transcription: transcriber worker not found."
-msgstr ""
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Live Recording"
-msgstr ""
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Click Record to begin..."
-msgstr ""
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Waiting for AI translation..."
-msgstr ""
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Microphone:"
-msgstr ""
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Show in new window"
-msgstr ""
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Text Size:"
-msgstr ""
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Theme"
-msgstr ""
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Light"
-msgstr ""
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Dark"
-msgstr ""
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Custom"
-msgstr ""
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Text Color"
-msgstr ""
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Background Color"
-msgstr ""
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Fullscreen"
-msgstr ""
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy"
-msgstr ""
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy transcription to clipboard"
-msgstr ""
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Nothing to copy!"
-msgstr ""
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy failed"
-msgstr ""
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copied!"
-msgstr ""
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Select Text Color"
-msgstr ""
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Select Background Color"
-msgstr ""
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "An error occurred while starting a new recording:"
-msgstr ""
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid ""
-"Please check your audio devices or check the application logs for more "
-"information."
-msgstr ""
-
-#: buzz/widgets/update_dialog.py
-msgid "A new version of Buzz is available!"
-msgstr ""
-
-#: buzz/widgets/update_dialog.py
-msgid "Current version:"
-msgstr ""
-
-#: buzz/widgets/update_dialog.py
-msgid "New version:"
-msgstr ""
-
-#: buzz/widgets/update_dialog.py
-msgid "Release Notes:"
-msgstr ""
-
-#: buzz/widgets/update_dialog.py
-msgid "Download and Install"
-msgstr ""
-
-#: buzz/widgets/update_dialog.py
-msgid "No download URL available for your platform."
-msgstr ""
-
-#: buzz/widgets/update_dialog.py
-msgid "Downloading file {} of {}..."
-msgstr ""
-
-#: buzz/widgets/update_dialog.py
-msgid "Downloading file {} of {} ({:.1f} MB / {:.1f} MB)..."
-msgstr ""
-
-#: buzz/widgets/update_dialog.py
-msgid "Download Failed"
-msgstr ""
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to download the update: {}"
-msgstr ""
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to save the installer: {}"
-msgstr ""
-
-#: buzz/widgets/update_dialog.py
-msgid "Download complete!"
-msgstr ""
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to run the installer: {}"
-msgstr ""
-
-#: buzz/widgets/about_dialog.py
-msgid "Check for updates"
-msgstr ""
-
-#: buzz/widgets/about_dialog.py
-msgid "Show logs"
-msgstr ""
-
-#: buzz/widgets/about_dialog.py
-msgid "You're up to date!"
-msgstr ""
-
-#: buzz/widgets/audio_meter_widget.py
-msgid "Average volume"
-msgstr ""
-
-#: buzz/widgets/audio_meter_widget.py
-msgid "Queue"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-msgid "Start"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-msgid "End"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Text"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Translation"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-msgid "View"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-msgid "Timestamps"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Export"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Translate"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Resize"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Identify Speakers"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Find"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Show/Hide Search Bar (Ctrl+F)"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Find:"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Enter text to find..."
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Previous match (Shift+Enter)"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Next match (Ctrl+Enter)"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Clear"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Playback Controls:"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Loop Segment"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Enable/disable looping when clicking on transcript segments"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Follow Audio"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid ""
-"Enable/disable following the current audio position in the transcript. When "
-"enabled, automatically scrolls to current text."
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Scroll to Current"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Scroll to the currently spoken text"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "1 of 100+ matches"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "1 of "
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " matches"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "No matches found"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " of 100+ matches"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " of "
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "API Key Required"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Please enter OpenAI API Key in preferences"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend end time"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend endings by up to (seconds)"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend endings"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Resize Options"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Desired subtitle length"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Available only if word level timings were disabled during transcription"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge Options"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge by gap"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Split by punctuation"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Split by max length"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Available only if word level timings were enabled during transcription"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid ""
-"Speaker identification is not available: failed to load required libraries."
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "1/8 Collecting transcripts"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "2/8 Loading audio"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "3/8 Loading alignment model"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "3/8 Loading alignment model (retrying with cache...)"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid ""
-"Failed to load alignment model. Please check your internet connection and "
-"try again."
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "4/8 Processing audio"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "5/8 Preparing transcripts"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "6/8 Identifying speakers"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "7/8 Mapping speakers to transcripts"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "8/8 Identification done"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "0/0 Error identifying speakers"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Step 1: Identify speakers"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Identify"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Ready to identify speakers"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Audio file not found"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Step 2: Name speakers"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Play sample"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Merge speaker sentences"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Save"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Cancelling..."
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Cancelled"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Save File"
-msgstr ""
-
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Text files"
-msgstr ""
-
-#: buzz/widgets/model_download_progress_dialog.py
-msgid "Downloading model"
-msgstr ""
-
-#: buzz/widgets/model_download_progress_dialog.py
-msgid "remaining"
-msgstr ""
-
-#: buzz/widgets/menu_bar.py
-msgid "Import File..."
-msgstr ""
-
-#: buzz/widgets/menu_bar.py
-msgid "Import URL..."
-msgstr ""
-
-#: buzz/widgets/menu_bar.py
-msgid "Import Folder..."
-msgstr ""
-
-#: buzz/widgets/menu_bar.py
-msgid "About"
-msgstr ""
-
-#: buzz/widgets/menu_bar.py
-msgid "Preferences..."
-msgstr ""
-
-#: buzz/widgets/menu_bar.py
-msgid "Help"
-msgstr ""
-
-#: buzz/widgets/menu_bar.py
-msgid "File"
-msgstr ""
-
-#: buzz/widgets/main_window.py
-msgid ""
-"Are you sure you want to delete the selected transcription(s)? This action "
-"cannot be undone."
-msgstr ""
-
-#: buzz/widgets/main_window.py
-msgid "Select audio file"
-msgstr ""
-
-#: buzz/widgets/main_window.py
-msgid "Select folder"
-msgstr ""
-
-#: buzz/widgets/main_window.py
-msgid "Unable to save OpenAI API key to keyring"
-msgstr ""
-
-#: buzz/transcriber/local_whisper_cpp_server_transcriber.py
-#: buzz/transcriber/recording_transcriber.py
-msgid "Whisper server failed to start. Check logs for details."
-msgstr ""
-
-#: buzz/transcriber/local_whisper_cpp_server_transcriber.py
-#: buzz/transcriber/recording_transcriber.py
-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
-msgid "Translate to English"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Transcribe"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Chinese"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Russian"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Korean"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "French"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Portuguese"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Turkish"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Arabic"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Swedish"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Indonesian"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Hindi"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Finnish"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Vietnamese"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Hebrew"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Greek"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Malay"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Czech"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Romanian"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Hungarian"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Tamil"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Norwegian"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Thai"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Urdu"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Croatian"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Bulgarian"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Lithuanian"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Latin"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Maori"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Malayalam"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Welsh"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Slovak"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Telugu"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Persian"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Bengali"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Serbian"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Azerbaijani"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Slovenian"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Kannada"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Estonian"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Macedonian"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Breton"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Basque"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Icelandic"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Armenian"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Nepali"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Mongolian"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Bosnian"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Kazakh"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Albanian"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Swahili"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Galician"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Marathi"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Punjabi"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Sinhala"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Khmer"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Shona"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Yoruba"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Somali"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Afrikaans"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Occitan"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Georgian"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Belarusian"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Tajik"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Sindhi"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Gujarati"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Amharic"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Yiddish"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Lao"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Uzbek"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Faroese"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Haitian Creole"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Pashto"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Turkmen"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Nynorsk"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Maltese"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Sanskrit"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Luxembourgish"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Myanmar"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Tibetan"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Tagalog"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Malagasy"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Assamese"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Tatar"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Hawaiian"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Lingala"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Hausa"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Bashkir"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Javanese"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Sundanese"
-msgstr ""
-
-#: buzz/transcriber/transcriber.py
-msgid "Cantonese"
-msgstr ""
-
-#: buzz/transcriber/recording_transcriber.py buzz/model_loader.py
-msgid "A connection error occurred"
-msgstr ""
-
-#: buzz/transcriber/recording_transcriber.py
-msgid "Starting Whisper.cpp..."
-msgstr ""
-
-#: buzz/transcriber/recording_transcriber.py
-msgid "Starting transcription..."
-msgstr ""
-
-#: buzz/settings/shortcut.py
-msgid "Open Record Window"
-msgstr ""
-
-#: buzz/settings/shortcut.py
-msgid "Import File"
-msgstr ""
-
-#: buzz/settings/shortcut.py
-msgid "Open Preferences Window"
-msgstr ""
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Text"
-msgstr ""
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Translation"
-msgstr ""
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Timestamps"
-msgstr ""
-
-#: buzz/settings/shortcut.py
-msgid "Search Transcript"
-msgstr ""
-
-#: buzz/settings/shortcut.py
-msgid "Go to Next Transcript Search Result"
-msgstr ""
-
-#: buzz/settings/shortcut.py
-msgid "Go to Previous Transcript Search Result"
-msgstr ""
-
-#: buzz/settings/shortcut.py
-msgid "Scroll to Current Text"
-msgstr ""
-
-#: buzz/settings/shortcut.py
-msgid "Play/Pause Audio"
-msgstr ""
-
-#: buzz/settings/shortcut.py
-msgid "Replay Current Segment"
-msgstr ""
-
-#: buzz/settings/shortcut.py
-msgid "Toggle Playback Controls"
-msgstr ""
-
-#: buzz/settings/shortcut.py
-msgid "Decrease Segment Start Time"
-msgstr ""
-
-#: buzz/settings/shortcut.py
-msgid "Increase Segment Start Time"
-msgstr ""
-
-#: buzz/settings/shortcut.py
-msgid "Decrease Segment End Time"
-msgstr ""
-
-#: buzz/settings/shortcut.py
-msgid "Increase Segment End Time"
-msgstr ""
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append below"
-msgstr ""
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append above"
-msgstr ""
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append and correct"
-msgstr ""
-
-#: buzz/file_transcriber_queue_worker.py
-msgid ""
-"Speech extraction failed! Check your internet connection — a model may need "
-"to be downloaded."
-msgstr ""
diff --git a/buzz/locale/es_ES/LC_MESSAGES/buzz.po b/buzz/locale/es_ES/LC_MESSAGES/buzz.po
deleted file mode 100644
index f940aa34..00000000
--- a/buzz/locale/es_ES/LC_MESSAGES/buzz.po
+++ /dev/null
@@ -1,1747 +0,0 @@
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
-# This file is distributed under the same license as the PACKAGE package.
-# FIRST AUTHOR , YEAR.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: \n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2026-03-07 20:20+0200\n"
-"PO-Revision-Date: 2025-09-08 12:43+0200\n"
-"Last-Translator: Éric Duarte \n"
-"Language-Team: \n"
-"Language: es\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Generator: Poedit 3.7\n"
-
-#: buzz/widgets/import_url_dialog.py buzz/settings/shortcut.py
-msgid "Import URL"
-msgstr "URL de importación"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "https://example.com/audio.mp3"
-msgstr "https://ejemplo.com/audio.mp3"
-
-#: buzz/widgets/import_url_dialog.py
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-#: buzz/widgets/main_window.py
-msgid "Ok"
-msgstr "Ok"
-
-#: buzz/widgets/import_url_dialog.py
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-#: buzz/widgets/model_download_progress_dialog.py buzz/widgets/main_window.py
-msgid "Cancel"
-msgstr "Cancelar"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "URL:"
-msgstr "URL:"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "Invalid URL"
-msgstr "URL inválido"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "The URL you entered is invalid."
-msgstr "La URL que has introducido no es válida."
-
-# automatic translation
-#: buzz/widgets/presentation_window.py
-msgid "Live Transcript Presentation"
-msgstr "Presentación de transcripción en vivo"
-
-#: buzz/widgets/preferences_dialog/shortcuts_editor_preferences_widget.py
-msgid "Reset to Defaults"
-msgstr "Reestablecer los Valores por Defecto"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "English"
-msgstr "Inglés"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Catalan"
-msgstr "Catalán"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Danish"
-msgstr "Danés"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Dutch"
-msgstr "Holandés"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "German"
-msgstr "Alemán"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Spanish"
-msgstr "Español"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Italian"
-msgstr "Italiano"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Japanese"
-msgstr "Japonés"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Latvian"
-msgstr "Letón"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Polish"
-msgstr "Polaco"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Portuguese (Brazil)"
-msgstr "Portugués (Brasil)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Ukrainian"
-msgstr "Ucraniano"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Chinese (Simplified)"
-msgstr "Chino (simplificado)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Chinese (Traditional)"
-msgstr "Chino (tradicional)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Restart required!"
-msgstr "¡Es necesario reiniciar!"
-
-# automatic translation
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Ui Language"
-msgstr "Lenguaje de interfaz de usuario"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Font Size"
-msgstr "Tamaño de fuente"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Test"
-msgstr "Prueba"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API key"
-msgstr "Clave API de OpenAI"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI base url"
-msgstr "URL base de OpenAI"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API model"
-msgstr "Modelo de la API de OpenAI"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Default export file name"
-msgstr "Nombre de archivo de exportación predeterminado"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Enable live recording transcription export"
-msgstr "Habilitar la exportación de transcripción de grabación en vivo"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Browse"
-msgstr "Navegar"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Export folder"
-msgstr "Carpeta de exportación"
-
-# automatic translation
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Live recording mode"
-msgstr "Modo de grabación en directo"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Note: Live recording export settings will be moved to the Advanced Settings "
-"in the Live Recording screen in a future version."
-msgstr ""
-"Nota: Los ajustes de exportación de grabación en vivo se trasladarán a la "
-"Configuración avanzada en la pantalla de grabación en vivo en una versión "
-"futura."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Use 8-bit quantization to reduce memory usage"
-msgstr "Usar cuantización de 8 bits para reducir el uso de memoria"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Applies to Huggingface and Faster Whisper models. Reduces GPU memory usage "
-"but may slightly decrease transcription quality."
-msgstr ""
-"Se aplica a los modelos Huggingface y Faster Whisper. Reduce el uso de "
-"memoria de la GPU, pero puede disminuir ligeramente la calidad de la "
-"transcripción."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Reduce GPU RAM"
-msgstr "Reducir la RAM de la GPU"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Use only CPU and disable GPU acceleration"
-msgstr "Usa solo CPU y desactiva la aceleración de GPU"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Set this if larger models do not fit your GPU memory and Buzz crashes"
-msgstr ""
-"Configure esto si los modelos más grandes no se ajustan a la memoria de su "
-"GPU y Buzz se bloquea"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Disable GPU"
-msgstr "Desactivar GPU"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API Key Test"
-msgstr "Prueba de la clave API de OpenAI"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Your API key is valid. Buzz will use this key to perform Whisper API "
-"transcriptions and AI translations."
-msgstr ""
-"Tu clave API es válida. Buzz usará esta clave para realizar transcripciones "
-"de la API de Whisper y traducciones de IA."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Invalid API key"
-msgstr "Clave API no válida"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"API supports only base64 characters (A-Za-z0-9+/=_-). Other characters in "
-"API key may cause errors."
-msgstr ""
-"La API solo admite caracteres base64 (A-Za-z0-9+/=_-). Otros caracteres de "
-"la clave de API pueden causar errores."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Select Export Folder"
-msgstr "Seleccione Exportar carpeta"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"OpenAI API returned invalid response. Please check the API url or your key. "
-"Transcription and translation may still work if the API does not support key "
-"validation."
-msgstr ""
-"La API de OpenAI devolvió una respuesta no válida. Compruebe la URL de la "
-"API o su clave. Es posible que la transcripción y la traducción sigan "
-"funcionando si la API no admite la validación de claves."
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Enable folder watch"
-msgstr "Habilitar la inspección de carpetas"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Delete processed files"
-msgstr "Eliminar archivos procesados"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Input folder"
-msgstr "Carpeta de entrada"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Output folder"
-msgstr "Carpeta de salida"
-
-# automatic translation
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Select Input Folder"
-msgstr "Seleccione la carpeta de entrada"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Select Output Folder"
-msgstr "Seleccione la carpeta de salida"
-
-# automatic translation
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Preferences"
-msgstr "Preferencias"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "General"
-msgstr "General"
-
-# automatic translation
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Models"
-msgstr "Modelos"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Shortcuts"
-msgstr "Atajos"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Folder Watch"
-msgstr "Vigilancia de carpetas"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Group"
-msgstr "Grupo"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Huggingface ID of a Faster whisper model"
-msgstr "Identificación de un modelo Más rápido whisper"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download"
-msgstr "Descargar"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Show file location"
-msgstr "Mostrar ubicación de archivo"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Delete"
-msgstr "Eliminar"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Downloaded"
-msgstr "Descargado"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Available for Download"
-msgstr "Disponible para descarga"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download link to Whisper.cpp ggml model file"
-msgstr "Enlace de descarga a Whisper.cpp archivo de modelo ggml"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Delete Model"
-msgstr "Eliminar modelo"
-
-# automatic translation
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Are you sure you want to delete the selected model?"
-msgstr "¿Confirma que quiere eliminar el modelo seleccionado?"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download failed"
-msgstr "Descarga fallida"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcription_tasks_table_widget.py
-#: buzz/widgets/update_dialog.py buzz/widgets/main_window.py
-#: buzz/model_loader.py
-msgid "Error"
-msgstr "Error"
-
-# automatic translation
-#: buzz/widgets/record_button.py buzz/widgets/main_window_toolbar.py
-msgid "Record"
-msgstr "Grabar"
-
-# automatic translation
-#: buzz/widgets/record_button.py
-msgid "Stop"
-msgstr "Detener"
-
-# automatic translation
-#: buzz/widgets/transcriber/languages_combo_box.py
-#: buzz/transcriber/transcriber.py
-msgid "Detect Language"
-msgstr "Detectar idioma"
-
-#: buzz/widgets/transcriber/mms_language_line_edit.py
-msgid "e.g., eng, fra, deu"
-msgstr "p. ej., eng, fra, deu"
-
-#: buzz/widgets/transcriber/mms_language_line_edit.py
-msgid ""
-"Enter an ISO 639-3 language code (3 letters).\n"
-"Examples: eng (English), fra (French), deu (German),\n"
-"spa (Spanish), lav (Latvian)"
-msgstr ""
-"Introduzca un código de idioma ISO 639-3 (3 letras).\n"
-"Ejemplos: eng (inglés), fra (francés), deu (alemán),\n"
-"spa (español), lav (letón)"
-
-# automatic translation
-#: buzz/widgets/transcriber/file_transcriber_widget.py
-msgid "Run"
-msgstr "Ejecutar"
-
-# automatic translation
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Model:"
-msgstr "Modelo:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-#: buzz/transcriber/recording_transcriber.py
-msgid "First time use of a model may take up to several minutest to load."
-msgstr ""
-"El uso por primera vez de un modelo puede tardar varios minutos en cargarse."
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Api Key:"
-msgstr "Clave API:"
-
-# automatic translation
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Task:"
-msgstr "Tarea:"
-
-# automatic translation
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Language:"
-msgstr "Idioma:"
-
-#: buzz/widgets/transcriber/initial_prompt_text_edit.py
-msgid "Enter prompt..."
-msgstr "Introducir prompt..."
-
-# automatic translation
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Advanced Settings"
-msgstr "Configuración avanzada"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Speech recognition settings"
-msgstr "Configuración de reconocimiento de voz"
-
-# automatic translation
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Initial Prompt:"
-msgstr "Indicación inicial:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Translation settings"
-msgstr "Ajustes de traducción"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Enable AI translation"
-msgstr "Habilite la traducción con IA"
-
-# automatic translation
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "AI model:"
-msgstr "Modelo de IA:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid ""
-"Please translate each text sent to you from English to Spanish. Translation "
-"will be used in an automated system, please do not add any comments or "
-"notes, just the translation."
-msgstr ""
-"Por favor, traduce cada texto que se te envíe del inglés al español. La "
-"traducción se utilizará en un sistema automatizado, por favor no añadas "
-"comentarios ni notas, solo la traducción."
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Instructions for AI:"
-msgstr "Instrucciones para la IA:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Recording settings"
-msgstr "Ajustes de grabación"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Silence threshold:"
-msgstr "Umbral de silencio:"
-
-# automatic translation
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Live recording mode:"
-msgstr "Modo de grabación en directo:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Line separator:"
-msgstr "Separador de línea:"
-
-# automatic translation
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Transcription step:"
-msgstr "Paso de transcripción:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Hide unconfirmed"
-msgstr "Ocultar no confirmado"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Enable live recording export"
-msgstr "Habilitar la exportación de grabación en vivo"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export folder:"
-msgstr "Carpeta de exportación:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export file name:"
-msgstr "Nombre del archivo de exportación:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Text file (.txt)"
-msgstr "Archivo de texto (.txt)"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "CSV (.csv)"
-msgstr "CSV (.csv)"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export file type:"
-msgstr "Tipo de archivo de exportación:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid ""
-"Limit export entries\n"
-"(0 = export all):"
-msgstr ""
-"Limitar entradas de exportación\n"
-"(0 = exportar todo):"
-
-# automatic translation
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Word-level timings"
-msgstr "Tiempos a nivel de palabra"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Extract speech"
-msgstr "Extraer voz"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Export:"
-msgstr "Exportar:"
-
-#: buzz/widgets/transcriber/hugging_face_search_line_edit.py
-msgid "Huggingface ID of a model"
-msgstr "Huggingface ID de un modelo"
-
-#: buzz/widgets/transcriber/advanced_settings_button.py
-msgid "Advanced..."
-msgstr "Avanzado..."
-
-# automatic translation
-#: buzz/widgets/main_window_toolbar.py
-msgid "New File Transcription"
-msgstr "Nueva transcripción de archivos"
-
-# automatic translation
-#: buzz/widgets/main_window_toolbar.py
-msgid "New URL Transcription"
-msgstr "Nueva transcripción de URL"
-
-# automatic translation
-#: buzz/widgets/main_window_toolbar.py
-msgid "Open Transcript"
-msgstr "Abrir transcripción"
-
-# automatic translation
-#: buzz/widgets/main_window_toolbar.py buzz/settings/shortcut.py
-msgid "Cancel Transcription"
-msgstr "Cancelar transcripción"
-
-# automatic translation
-#: buzz/widgets/main_window_toolbar.py buzz/widgets/main_window.py
-#: buzz/settings/shortcut.py
-msgid "Clear History"
-msgstr "Vaciar historial"
-
-#: buzz/widgets/main_window_toolbar.py buzz/widgets/update_dialog.py
-msgid "Update Available"
-msgstr "Actualización disponible"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "In Progress"
-msgstr "En Progreso"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Completed"
-msgstr "Completado"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Failed"
-msgstr "Fallido"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Canceled"
-msgstr "Cancelado"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Queued"
-msgstr "En cola"
-
-# automatic translation
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "File Name / URL"
-msgstr "Nombre de archivo / URL"
-
-# automatic translation
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Model"
-msgstr "Modelo"
-
-# automatic translation
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Task"
-msgstr "Tarea"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Status"
-msgstr "Estado"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Date Completed"
-msgstr "Fecha de finalización"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Date Added"
-msgstr "Fecha de adición"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Notes"
-msgstr "Notas"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Reset Column Order"
-msgstr "Restablecer orden de columnas"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Restart Transcription"
-msgstr "Reiniciar transcripción"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Rename"
-msgstr "Renombrar"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Add/Edit Notes"
-msgstr "Añadir/Editar notas"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Rename Transcription"
-msgstr "Renombrar transcripción"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Enter new name:"
-msgstr "Introducir nuevo nombre:"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Enter some relevant notes for this transcription:"
-msgstr "Introduzca algunas notas relevantes para esta transcripción:"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Cannot Restart"
-msgstr "No se puede reiniciar"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Only failed or canceled transcriptions can be restarted."
-msgstr "Solo se pueden reiniciar las transcripciones fallidas o canceladas."
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Failed to restart transcription: {}"
-msgstr "No se pudo reiniciar la transcripción: {}"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid ""
-"Could not restart transcription: model not available and could not be "
-"downloaded."
-msgstr ""
-"No se pudo reiniciar la transcripción: modelo no disponible y no se pudo "
-"descargar."
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Could not restart transcription: transcriber worker not found."
-msgstr ""
-"No se pudo reiniciar la transcripción: no se encontró el trabajador del "
-"transcriptor."
-
-# automatic translation
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Live Recording"
-msgstr "Grabación en vivo"
-
-# automatic translation
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Click Record to begin..."
-msgstr "Pulse en Grabar para comenzar..."
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Waiting for AI translation..."
-msgstr "A la espera de la traducción de la IA..."
-
-# automatic translation
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Microphone:"
-msgstr "Micrófono:"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Show in new window"
-msgstr "Mostrar en nueva ventana"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Text Size:"
-msgstr "Tamaño del texto:"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Theme"
-msgstr "Tema"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Light"
-msgstr "Claro"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Dark"
-msgstr "Oscuro"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Custom"
-msgstr "Personalizado"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Text Color"
-msgstr "Color del texto"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Background Color"
-msgstr "Color de fondo"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Fullscreen"
-msgstr "Pantalla completa"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy"
-msgstr "Copiar"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy transcription to clipboard"
-msgstr "Copiar transcripción al portapapeles"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Nothing to copy!"
-msgstr "¡Nada que copiar!"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy failed"
-msgstr "Copia fallida"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copied!"
-msgstr "¡Copiado!"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Select Text Color"
-msgstr "Seleccionar color del texto"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Select Background Color"
-msgstr "Seleccionar color de fondo"
-
-# automatic translation
-#: buzz/widgets/recording_transcriber_widget.py
-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
-msgid ""
-"Please check your audio devices or check the application logs for more "
-"information."
-msgstr ""
-"Compruebe sus dispositivos de audio o consulte los registros de la "
-"aplicación para obtener más información."
-
-#: buzz/widgets/update_dialog.py
-msgid "A new version of Buzz is available!"
-msgstr "¡Hay una nueva versión de Buzz disponible!"
-
-#: buzz/widgets/update_dialog.py
-msgid "Current version:"
-msgstr "Versión actual:"
-
-#: buzz/widgets/update_dialog.py
-msgid "New version:"
-msgstr "Nueva versión:"
-
-#: buzz/widgets/update_dialog.py
-msgid "Release Notes:"
-msgstr "Notas de la versión:"
-
-#: buzz/widgets/update_dialog.py
-msgid "Download and Install"
-msgstr "Descargar e instalar"
-
-#: buzz/widgets/update_dialog.py
-msgid "No download URL available for your platform."
-msgstr "No hay URL de descarga disponible para su plataforma."
-
-#: buzz/widgets/update_dialog.py
-msgid "Downloading file {} of {}..."
-msgstr "Descargando archivo {} de {}..."
-
-#: buzz/widgets/update_dialog.py
-msgid "Downloading file {} of {} ({:.1f} MB / {:.1f} MB)..."
-msgstr "Descargando archivo {} de {} ({:.1f} MB / {:.1f} MB)..."
-
-#: buzz/widgets/update_dialog.py
-msgid "Download Failed"
-msgstr "Descarga fallida"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to download the update: {}"
-msgstr "Error al descargar la actualización: {}"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to save the installer: {}"
-msgstr "No se pudo guardar el instalador: {}"
-
-#: buzz/widgets/update_dialog.py
-msgid "Download complete!"
-msgstr "¡Descarga completa!"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to run the installer: {}"
-msgstr "No se pudo ejecutar el instalador: {}"
-
-# automatic translation
-#: buzz/widgets/about_dialog.py
-msgid "Check for updates"
-msgstr "Buscar actualizaciones"
-
-#: buzz/widgets/about_dialog.py
-msgid "Show logs"
-msgstr "Mostrar registros"
-
-# automatic translation
-#: buzz/widgets/about_dialog.py
-msgid "You're up to date!"
-msgstr "¡Estás al día!"
-
-#: buzz/widgets/audio_meter_widget.py
-msgid "Average volume"
-msgstr "Volumen promedio"
-
-#: buzz/widgets/audio_meter_widget.py
-msgid "Queue"
-msgstr "Cola"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-msgid "Start"
-msgstr "Inicio"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-msgid "End"
-msgstr "Fin"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Text"
-msgstr "Texto"
-
-# automatic translation
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Translation"
-msgstr "Traducción"
-
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-msgid "View"
-msgstr "Ver"
-
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-msgid "Timestamps"
-msgstr "Marcas de tiempo"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Export"
-msgstr "Exportar"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Translate"
-msgstr "Traducir"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Resize"
-msgstr "Cambiar el tamaño"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Identify Speakers"
-msgstr "Identificar hablantes"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Find"
-msgstr "Buscar"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Show/Hide Search Bar (Ctrl+F)"
-msgstr "Mostrar/Ocultar barra de búsqueda (Ctrl+F)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Find:"
-msgstr "Encontrar:"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Enter text to find..."
-msgstr "Introducir texto para encontrar..."
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Previous match (Shift+Enter)"
-msgstr "Coincidencia anterior (Mayús+Intro)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Next match (Ctrl+Enter)"
-msgstr "Siguiente coincidencia (Ctrl+Enter)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Clear"
-msgstr "Limpiar"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Playback Controls:"
-msgstr "Controles de reproducción:"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Loop Segment"
-msgstr "Segmento de bucle"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Enable/disable looping when clicking on transcript segments"
-msgstr ""
-"Activar/desactivar la reproducción en bucle al hacer clic en segmentos de la "
-"transcripción"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Follow Audio"
-msgstr "Seguir audio"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid ""
-"Enable/disable following the current audio position in the transcript. When "
-"enabled, automatically scrolls to current text."
-msgstr ""
-"Activa/desactiva el seguimiento de la posición actual del audio en la "
-"transcripción. Cuando está activado, se desplaza automáticamente al texto "
-"actual."
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Scroll to Current"
-msgstr "Desplácese hasta Actual"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Scroll to the currently spoken text"
-msgstr "Desplazarse hasta el texto hablado actualmente"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "1 of 100+ matches"
-msgstr "1 de 100+ coincidencias"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "1 of "
-msgstr "1 de "
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " matches"
-msgstr " coincidencias"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "No matches found"
-msgstr "No se encontraron coincidencias"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " of 100+ matches"
-msgstr " de 100+ coincidencias"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " of "
-msgstr " de "
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "API Key Required"
-msgstr "Clave de API requerida"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Please enter OpenAI API Key in preferences"
-msgstr "Ingrese la clave API de OpenAI en las preferencias"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend end time"
-msgstr "Extender tiempo final"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend endings by up to (seconds)"
-msgstr "Extender finales hasta (segundos)"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend endings"
-msgstr "Extender finales"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Resize Options"
-msgstr "Opciones de cambio de tamaño"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Desired subtitle length"
-msgstr "Longitud deseada de los subtítulos"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Available only if word level timings were disabled during transcription"
-msgstr ""
-"Disponible solo si los tiempos a nivel de palabra estaban desactivados "
-"durante la transcripción"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge Options"
-msgstr "Opciones de fusión"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge by gap"
-msgstr "Fusión por hueco"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Split by punctuation"
-msgstr "Dividido por puntuación"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Split by max length"
-msgstr "Dividido por la longitud máxima"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge"
-msgstr "Fusión"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Available only if word level timings were enabled during transcription"
-msgstr ""
-"Disponible solo si los tiempos a nivel de palabra estaban activados durante "
-"la transcripción"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid ""
-"Speaker identification is not available: failed to load required libraries."
-msgstr ""
-"La identificación de hablantes no está disponible: no se pudieron cargar las "
-"bibliotecas requeridas."
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "1/8 Collecting transcripts"
-msgstr "1/8 Recopilando transcripciones"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "2/8 Loading audio"
-msgstr "2/8 Cargando audio"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "3/8 Loading alignment model"
-msgstr "3/8 Cargando modelo de alineación"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "3/8 Loading alignment model (retrying with cache...)"
-msgstr "3/8 Cargando modelo de alineación (reintentando con caché...)"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid ""
-"Failed to load alignment model. Please check your internet connection and "
-"try again."
-msgstr ""
-"No se pudo cargar el modelo de alineación. Por favor, compruebe su conexión "
-"a internet e inténtelo de nuevo."
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "4/8 Processing audio"
-msgstr "4/8 Procesando audio"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "5/8 Preparing transcripts"
-msgstr "5/8 Preparando transcripciones"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "6/8 Identifying speakers"
-msgstr "6/8 Identificando hablantes"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "7/8 Mapping speakers to transcripts"
-msgstr "7/8 Asignando hablantes a transcripciones"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "8/8 Identification done"
-msgstr "8/8 Identificación completada"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "0/0 Error identifying speakers"
-msgstr "0/0 Error al identificar hablantes"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Step 1: Identify speakers"
-msgstr "Paso 1: Identificar hablantes"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Identify"
-msgstr "Identificar"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Ready to identify speakers"
-msgstr "Listo para identificar hablantes"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Audio file not found"
-msgstr "Archivo de audio no encontrado"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Step 2: Name speakers"
-msgstr "Paso 2: Nombrar hablantes"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Play sample"
-msgstr "Reproducir muestra"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Merge speaker sentences"
-msgstr "Fusionar frases del hablante"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Save"
-msgstr "Guardar"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Cancelling..."
-msgstr "Cancelando..."
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Cancelled"
-msgstr "Cancelado"
-
-# automatic translation
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Save File"
-msgstr "Guardar archivo"
-
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Text files"
-msgstr "Archivos de texto"
-
-#: buzz/widgets/model_download_progress_dialog.py
-msgid "Downloading model"
-msgstr "Descargando modelo"
-
-#: buzz/widgets/model_download_progress_dialog.py
-msgid "remaining"
-msgstr "restantes"
-
-# automatic translation
-#: buzz/widgets/menu_bar.py
-msgid "Import File..."
-msgstr "Importar archivo..."
-
-# automatic translation
-#: buzz/widgets/menu_bar.py
-msgid "Import URL..."
-msgstr "Importar URL..."
-
-# automatic translation
-#: buzz/widgets/menu_bar.py
-msgid "Import Folder..."
-msgstr "Importar carpeta..."
-
-#: buzz/widgets/menu_bar.py
-msgid "About"
-msgstr "Acerca de"
-
-# automatic translation
-#: buzz/widgets/menu_bar.py
-msgid "Preferences..."
-msgstr "Preferencias..."
-
-# automatic translation
-#: buzz/widgets/menu_bar.py
-msgid "Help"
-msgstr "Ayuda"
-
-# automatic translation
-#: buzz/widgets/menu_bar.py
-msgid "File"
-msgstr "Archivo"
-
-# automatic translation
-#: buzz/widgets/main_window.py
-msgid ""
-"Are you sure you want to delete the selected transcription(s)? This action "
-"cannot be undone."
-msgstr ""
-"¿Confirma que quiere eliminar las transcripciones seleccionadas? Esta acción "
-"no se puede deshacer."
-
-# automatic translation
-#: buzz/widgets/main_window.py
-msgid "Select audio file"
-msgstr "Seleccionar archivo de audio"
-
-# automatic translation
-#: buzz/widgets/main_window.py
-msgid "Select folder"
-msgstr "Seleccione la carpeta de entrada"
-
-#: buzz/widgets/main_window.py
-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
-#: buzz/transcriber/recording_transcriber.py
-msgid "Whisper server failed to start. Check logs for details."
-msgstr ""
-"El servidor Whisper no se pudo iniciar. Consulta los registros para obtener "
-"más detalles."
-
-#: buzz/transcriber/local_whisper_cpp_server_transcriber.py
-#: buzz/transcriber/recording_transcriber.py
-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 ""
-"El servidor Whisper no se pudo iniciar debido a la memoria insuficiente. "
-"Vuelva a intentarlo con un modelo más pequeño. Para forzar el modo de CPU, "
-"use la variable de entorno BUZZ_FORCE_CPU=TRUE."
-
-#: buzz/transcriber/transcriber.py
-msgid "Translate to English"
-msgstr "Traducir al Inglés"
-
-# automatic translation
-#: buzz/transcriber/transcriber.py
-msgid "Transcribe"
-msgstr "Transcribir"
-
-#: buzz/transcriber/transcriber.py
-msgid "Chinese"
-msgstr "China"
-
-#: buzz/transcriber/transcriber.py
-msgid "Russian"
-msgstr "Ruso"
-
-#: buzz/transcriber/transcriber.py
-msgid "Korean"
-msgstr "Coreano"
-
-#: buzz/transcriber/transcriber.py
-msgid "French"
-msgstr "Francés"
-
-#: buzz/transcriber/transcriber.py
-msgid "Portuguese"
-msgstr "Portugués"
-
-#: buzz/transcriber/transcriber.py
-msgid "Turkish"
-msgstr "Turco"
-
-#: buzz/transcriber/transcriber.py
-msgid "Arabic"
-msgstr "Árabe"
-
-#: buzz/transcriber/transcriber.py
-msgid "Swedish"
-msgstr "Sueco"
-
-#: buzz/transcriber/transcriber.py
-msgid "Indonesian"
-msgstr "Indones"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hindi"
-msgstr "Hindi"
-
-#: buzz/transcriber/transcriber.py
-msgid "Finnish"
-msgstr "Finlandés"
-
-#: buzz/transcriber/transcriber.py
-msgid "Vietnamese"
-msgstr "Vietnamita"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hebrew"
-msgstr "Hebreo"
-
-#: buzz/transcriber/transcriber.py
-msgid "Greek"
-msgstr "Griego"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malay"
-msgstr "Malayo"
-
-#: buzz/transcriber/transcriber.py
-msgid "Czech"
-msgstr "República Checa"
-
-#: buzz/transcriber/transcriber.py
-msgid "Romanian"
-msgstr "Rumano"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hungarian"
-msgstr "Húngaro"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tamil"
-msgstr "Tamil"
-
-#: buzz/transcriber/transcriber.py
-msgid "Norwegian"
-msgstr "Noruego"
-
-#: buzz/transcriber/transcriber.py
-msgid "Thai"
-msgstr "Tailandés"
-
-#: buzz/transcriber/transcriber.py
-msgid "Urdu"
-msgstr "Urdu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Croatian"
-msgstr "Croata"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bulgarian"
-msgstr "Búlgaro"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lithuanian"
-msgstr "Lituano"
-
-#: buzz/transcriber/transcriber.py
-msgid "Latin"
-msgstr "Latin"
-
-#: buzz/transcriber/transcriber.py
-msgid "Maori"
-msgstr "Maori"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malayalam"
-msgstr "Malayo"
-
-#: buzz/transcriber/transcriber.py
-msgid "Welsh"
-msgstr "Galés"
-
-#: buzz/transcriber/transcriber.py
-msgid "Slovak"
-msgstr "Eslovaco"
-
-#: buzz/transcriber/transcriber.py
-msgid "Telugu"
-msgstr "Telugu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Persian"
-msgstr "Persa"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bengali"
-msgstr "Bengalí"
-
-#: buzz/transcriber/transcriber.py
-msgid "Serbian"
-msgstr "Serbian"
-
-#: buzz/transcriber/transcriber.py
-msgid "Azerbaijani"
-msgstr "Azerbaiyano"
-
-#: buzz/transcriber/transcriber.py
-msgid "Slovenian"
-msgstr "Esloveno"
-
-#: buzz/transcriber/transcriber.py
-msgid "Kannada"
-msgstr "Kannada"
-
-#: buzz/transcriber/transcriber.py
-msgid "Estonian"
-msgstr "Estonio"
-
-#: buzz/transcriber/transcriber.py
-msgid "Macedonian"
-msgstr "Macedonio"
-
-#: buzz/transcriber/transcriber.py
-msgid "Breton"
-msgstr "Breton"
-
-#: buzz/transcriber/transcriber.py
-msgid "Basque"
-msgstr "Vasco"
-
-#: buzz/transcriber/transcriber.py
-msgid "Icelandic"
-msgstr "Islandés"
-
-#: buzz/transcriber/transcriber.py
-msgid "Armenian"
-msgstr "Ermeni"
-
-#: buzz/transcriber/transcriber.py
-msgid "Nepali"
-msgstr "Nepalí"
-
-#: buzz/transcriber/transcriber.py
-msgid "Mongolian"
-msgstr "Mongol"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bosnian"
-msgstr "Bosnio"
-
-#: buzz/transcriber/transcriber.py
-msgid "Kazakh"
-msgstr "Kazako"
-
-#: buzz/transcriber/transcriber.py
-msgid "Albanian"
-msgstr "Arnavut"
-
-#: buzz/transcriber/transcriber.py
-msgid "Swahili"
-msgstr "Suahili"
-
-#: buzz/transcriber/transcriber.py
-msgid "Galician"
-msgstr "Gallego"
-
-#: buzz/transcriber/transcriber.py
-msgid "Marathi"
-msgstr "Maratí"
-
-#: buzz/transcriber/transcriber.py
-msgid "Punjabi"
-msgstr "Punjabi"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sinhala"
-msgstr "Cingalés"
-
-#: buzz/transcriber/transcriber.py
-msgid "Khmer"
-msgstr "Khmer"
-
-#: buzz/transcriber/transcriber.py
-msgid "Shona"
-msgstr "Shona"
-
-#: buzz/transcriber/transcriber.py
-msgid "Yoruba"
-msgstr "Yoruba"
-
-#: buzz/transcriber/transcriber.py
-msgid "Somali"
-msgstr "Somalí"
-
-#: buzz/transcriber/transcriber.py
-msgid "Afrikaans"
-msgstr "Africaans"
-
-#: buzz/transcriber/transcriber.py
-msgid "Occitan"
-msgstr "Occitano"
-
-#: buzz/transcriber/transcriber.py
-msgid "Georgian"
-msgstr "Georgiano"
-
-#: buzz/transcriber/transcriber.py
-msgid "Belarusian"
-msgstr "Belorusia"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tajik"
-msgstr "Tajik"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sindhi"
-msgstr "Sindhi"
-
-#: buzz/transcriber/transcriber.py
-msgid "Gujarati"
-msgstr "Gujarati"
-
-#: buzz/transcriber/transcriber.py
-msgid "Amharic"
-msgstr "Amharca"
-
-#: buzz/transcriber/transcriber.py
-msgid "Yiddish"
-msgstr "Yiddish"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lao"
-msgstr "República Popular Democrática de Laos"
-
-#: buzz/transcriber/transcriber.py
-msgid "Uzbek"
-msgstr "Uzbeko"
-
-#: buzz/transcriber/transcriber.py
-msgid "Faroese"
-msgstr "Faroe"
-
-#: buzz/transcriber/transcriber.py
-msgid "Haitian Creole"
-msgstr "Haitian Creole"
-
-#: buzz/transcriber/transcriber.py
-msgid "Pashto"
-msgstr "Pastún"
-
-#: buzz/transcriber/transcriber.py
-msgid "Turkmen"
-msgstr "Turcomano"
-
-#: buzz/transcriber/transcriber.py
-msgid "Nynorsk"
-msgstr "Nynorsk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Maltese"
-msgstr "Maltés"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sanskrit"
-msgstr "Sánscrito"
-
-#: buzz/transcriber/transcriber.py
-msgid "Luxembourgish"
-msgstr "Luxemburgo"
-
-#: buzz/transcriber/transcriber.py
-msgid "Myanmar"
-msgstr "Myanmar"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tibetan"
-msgstr "Tibetano"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tagalog"
-msgstr "Tagalog"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malagasy"
-msgstr "Madagascar"
-
-#: buzz/transcriber/transcriber.py
-msgid "Assamese"
-msgstr "Asamés"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tatar"
-msgstr "Tátaro"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hawaiian"
-msgstr "Hawaiano"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lingala"
-msgstr "Lingala"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hausa"
-msgstr "Hausa"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bashkir"
-msgstr "Baskir"
-
-#: buzz/transcriber/transcriber.py
-msgid "Javanese"
-msgstr "Javanés"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sundanese"
-msgstr "Sundanés"
-
-#: buzz/transcriber/transcriber.py
-msgid "Cantonese"
-msgstr "Cantonés"
-
-#: buzz/transcriber/recording_transcriber.py buzz/model_loader.py
-msgid "A connection error occurred"
-msgstr "Se ha producido un error de conexión"
-
-#: buzz/transcriber/recording_transcriber.py
-msgid "Starting Whisper.cpp..."
-msgstr "Iniciando Whisper.cpp..."
-
-#: buzz/transcriber/recording_transcriber.py
-msgid "Starting transcription..."
-msgstr "Iniciando transcripción..."
-
-# automatic translation
-#: buzz/settings/shortcut.py
-msgid "Open Record Window"
-msgstr "Abrir ventana de grabación"
-
-# automatic translation
-#: buzz/settings/shortcut.py
-msgid "Import File"
-msgstr "Importar archivo"
-
-#: buzz/settings/shortcut.py
-msgid "Open Preferences Window"
-msgstr "Abrir ventana de preferencias"
-
-# automatic translation
-#: buzz/settings/shortcut.py
-msgid "View Transcript Text"
-msgstr "Ver el texto de la transcripción"
-
-# automatic translation
-#: buzz/settings/shortcut.py
-msgid "View Transcript Translation"
-msgstr "Ver la traducción de la transcripción"
-
-# automatic translation
-#: buzz/settings/shortcut.py
-msgid "View Transcript Timestamps"
-msgstr "Ver marcas de tiempo de la transcripción"
-
-# automatic translation
-#: buzz/settings/shortcut.py
-msgid "Search Transcript"
-msgstr "Buscar transcripción"
-
-#: buzz/settings/shortcut.py
-msgid "Go to Next Transcript Search Result"
-msgstr "Ir al siguiente resultado de búsqueda de transcripción"
-
-#: buzz/settings/shortcut.py
-msgid "Go to Previous Transcript Search Result"
-msgstr "Ir al resultado de búsqueda de transcripción anterior"
-
-#: buzz/settings/shortcut.py
-msgid "Scroll to Current Text"
-msgstr "Desplazarse al texto actual"
-
-#: buzz/settings/shortcut.py
-msgid "Play/Pause Audio"
-msgstr "Reproducir/Pausar audio"
-
-#: buzz/settings/shortcut.py
-msgid "Replay Current Segment"
-msgstr "Reproducir segmento actual"
-
-#: buzz/settings/shortcut.py
-msgid "Toggle Playback Controls"
-msgstr "Alternar controles de reproducción"
-
-#: buzz/settings/shortcut.py
-msgid "Decrease Segment Start Time"
-msgstr "Disminuir el tiempo de inicio del segmento"
-
-#: buzz/settings/shortcut.py
-msgid "Increase Segment Start Time"
-msgstr "Aumentar el tiempo de inicio del segmento"
-
-#: buzz/settings/shortcut.py
-msgid "Decrease Segment End Time"
-msgstr "Disminuir el tiempo de finalización del segmento"
-
-#: buzz/settings/shortcut.py
-msgid "Increase Segment End Time"
-msgstr "Aumentar el tiempo de finalización del segmento"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append below"
-msgstr "Añadir a continuación"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append above"
-msgstr "Añadir arriba"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append and correct"
-msgstr "Añadir y corregir"
-
-#: buzz/file_transcriber_queue_worker.py
-msgid ""
-"Speech extraction failed! Check your internet connection — a model may need "
-"to be downloaded."
-msgstr ""
-"¡Extracción de voz fallida! Compruebe su conexión a internet — es posible "
-"que sea necesario descargar un modelo."
-
-# automatic translation
-#~ msgid "Comma-separated, e.g. \"0.0, 0.2, 0.4, 0.6, 0.8, 1.0\""
-#~ msgstr "Separados por comas, p. ej., «0.0, 0.2, 0.4, 0.6, 0.8, 1.0»"
-
-# automatic translation
-#~ msgid "Temperature:"
-#~ msgstr "Temperatura:"
-
-#~ msgid "Please translate each text sent to you from English to Spanish."
-#~ msgstr ""
-#~ "Por favor, traduzca cada texto que se le envíe del inglés al español."
-
-#~ msgid "Translation error, see logs!"
-#~ msgstr "¡Error de traducción, consulte los registros!"
-
-#~ msgid "Snap permission notice"
-#~ msgstr "Aviso de permiso Snap"
-
-#~ msgid ""
-#~ "Detected missing permissions, please check that snap permissions have "
-#~ "been granted"
-#~ msgstr ""
-#~ "Se ha detectado que faltan permisos, compruebe que se han concedido los "
-#~ "permisos snap"
-
-#~ msgid ""
-#~ "To enable necessary permissions run the following commands in the terminal"
-#~ msgstr ""
-#~ "Para habilitar los permisos necesarios ejecute los siguientes comandos en "
-#~ "el terminal"
-
-#~ msgid "Close"
-#~ msgstr "Cerrar"
-
-#~ msgid "Enter instructions for AI on how to translate..."
-#~ msgstr "Introduzca instrucciones para la IA sobre cómo traducir..."
-
-#~ msgid "ID"
-#~ msgstr "Id."
-
-#~ msgid "Undo"
-#~ msgstr "Deshacer"
-
-#~ msgid "Redo"
-#~ msgstr "Rehacer"
diff --git a/buzz/locale/it_IT/LC_MESSAGES/buzz.po b/buzz/locale/it_IT/LC_MESSAGES/buzz.po
deleted file mode 100644
index 2c2ea56e..00000000
--- a/buzz/locale/it_IT/LC_MESSAGES/buzz.po
+++ /dev/null
@@ -1,1696 +0,0 @@
-# ITALIAN TRANSLATION.
-# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
-# This file is distributed under the same license as the PACKAGE package.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: buzz\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2026-03-07 20:20+0200\n"
-"PO-Revision-Date: 2026-01-25 21:42+0200\n"
-"Language-Team: (Italiano) Albano Battistella \n"
-"Language: it_IT\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-"X-Generator: Poedit 3.3\n"
-
-#: buzz/widgets/import_url_dialog.py buzz/settings/shortcut.py
-msgid "Import URL"
-msgstr "Importa URL"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "https://example.com/audio.mp3"
-msgstr "https://esempio.com/audio.mp3"
-
-#: buzz/widgets/import_url_dialog.py
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-#: buzz/widgets/main_window.py
-msgid "Ok"
-msgstr "Ok"
-
-#: buzz/widgets/import_url_dialog.py
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-#: buzz/widgets/model_download_progress_dialog.py buzz/widgets/main_window.py
-msgid "Cancel"
-msgstr "Annulla"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "URL:"
-msgstr "URL:"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "Invalid URL"
-msgstr "URL non valido"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "The URL you entered is invalid."
-msgstr "L'URL inserito non è valido."
-
-#: buzz/widgets/presentation_window.py
-msgid "Live Transcript Presentation"
-msgstr "Presentazione con trascrizione in diretta"
-
-#: buzz/widgets/preferences_dialog/shortcuts_editor_preferences_widget.py
-msgid "Reset to Defaults"
-msgstr "Ripristina impostazioni predefinite"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "English"
-msgstr "Inglese"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Catalan"
-msgstr "Catalano"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Danish"
-msgstr "Danese"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Dutch"
-msgstr "Olandese"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "German"
-msgstr "Tedesco"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Spanish"
-msgstr "Spagnolo"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Italian"
-msgstr "Italiano"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Japanese"
-msgstr "Giapponese"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Latvian"
-msgstr "Lettone"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Polish"
-msgstr "Polacco"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Portuguese (Brazil)"
-msgstr "Portoghese (Brasiliano)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Ukrainian"
-msgstr "Ucraino"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Chinese (Simplified)"
-msgstr "Cinese (semplificato)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Chinese (Traditional)"
-msgstr "Cinese (tradizionale)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Restart required!"
-msgstr "Riavvio richiesto!"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Ui Language"
-msgstr "Lingua UI:"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Font Size"
-msgstr "Dimensione del carattere"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Test"
-msgstr "Test"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API key"
-msgstr "Chiave API OpenAI"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI base url"
-msgstr "URL di base di OpenAI"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API model"
-msgstr "Modello API OpenAI"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Default export file name"
-msgstr "Nome file di esportazione predefinito"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Enable live recording transcription export"
-msgstr "Abilita l'esportazione della trascrizione della registrazione live"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Browse"
-msgstr "Sfoglia"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Export folder"
-msgstr "Esporta cartella"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Live recording mode"
-msgstr "Modalità di registrazione in diretta"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Note: Live recording export settings will be moved to the Advanced Settings "
-"in the Live Recording screen in a future version."
-msgstr ""
-"Nota: le impostazioni di esportazione della registrazione live verranno "
-"spostate nelle Impostazioni avanzate nella schermata di Registrazione live "
-"in una versione futura."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Use 8-bit quantization to reduce memory usage"
-msgstr ""
-"Utilizzare la quantizzazione a 8 bit per ridurre l'utilizzo della memoria"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Applies to Huggingface and Faster Whisper models. Reduces GPU memory usage "
-"but may slightly decrease transcription quality."
-msgstr ""
-"Si applica ai modelli Huggingface e Faster Whisper. Riduce l'utilizzo della "
-"memoria GPU ma potrebbe ridurre leggermente la qualità della trascrizione."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Reduce GPU RAM"
-msgstr "Ridurre la RAM della GPU"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Use only CPU and disable GPU acceleration"
-msgstr "Utilizza solo la CPU e disattiva l'accelerazione GPU"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Set this if larger models do not fit your GPU memory and Buzz crashes"
-msgstr ""
-"Imposta questa opzione se i modelli più grandi non si adattano alla memoria "
-"della tua GPU e Buzz si blocca"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Disable GPU"
-msgstr "Disabilita GPU"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API Key Test"
-msgstr "Test della chiave API OpenAI"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Your API key is valid. Buzz will use this key to perform Whisper API "
-"transcriptions and AI translations."
-msgstr ""
-"La tua chiave API è valida. Buzz utilizzerà questa chiave per eseguire le "
-"trascrizioni API Whisper e le traduzioni AI."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Invalid API key"
-msgstr "Chiave API non valida"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"API supports only base64 characters (A-Za-z0-9+/=_-). Other characters in "
-"API key may cause errors."
-msgstr ""
-"L'API supporta solo caratteri base64 (A-Za-z0-9+/=). Altri caratteri nella "
-"chiave API potrebbero causare errori."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Select Export Folder"
-msgstr "Seleziona la cartella di esportazione"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"OpenAI API returned invalid response. Please check the API url or your key. "
-"Transcription and translation may still work if the API does not support key "
-"validation."
-msgstr ""
-"L'API OpenAI ha restituito una risposta non valida. Controlla l'URL dell'API "
-"o la tua chiave.La trascrizione e la traduzione potrebbero comunque "
-"funzionare se l'API non supporta la convalida della chiave."
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Enable folder watch"
-msgstr "Abilita controllo cartelle"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Delete processed files"
-msgstr "Elimina file elaborati"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Input folder"
-msgstr "Cartella di input"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Output folder"
-msgstr "Cartella di output"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Select Input Folder"
-msgstr "Seleziona cartella di input"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Select Output Folder"
-msgstr "Seleziona cartella di output"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Preferences"
-msgstr "Preferenze"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "General"
-msgstr "Generale"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Models"
-msgstr "Modelli"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Shortcuts"
-msgstr "Scorciatoie"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Folder Watch"
-msgstr "Guarda cartella"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Group"
-msgstr "Gruppo"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Huggingface ID of a Faster whisper model"
-msgstr "ID Huggingface di un modello Whisper più veloce"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download"
-msgstr "Download"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Show file location"
-msgstr "Mostra la posizione del file"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Delete"
-msgstr "Elimina"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Downloaded"
-msgstr "Scaricato"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Available for Download"
-msgstr "Disponibile per il download"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download link to Whisper.cpp ggml model file"
-msgstr "Link per scaricare il file modello ggml Whisper.cpp"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Delete Model"
-msgstr "Elimina modello"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Are you sure you want to delete the selected model?"
-msgstr "Sei sicuro di voler eliminare il modello selezionato?"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download failed"
-msgstr "Download non riuscito"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcription_tasks_table_widget.py
-#: buzz/widgets/update_dialog.py buzz/widgets/main_window.py
-#: buzz/model_loader.py
-msgid "Error"
-msgstr "Errore"
-
-#: buzz/widgets/record_button.py buzz/widgets/main_window_toolbar.py
-msgid "Record"
-msgstr "Registra"
-
-#: buzz/widgets/record_button.py
-msgid "Stop"
-msgstr "Arresta"
-
-#: buzz/widgets/transcriber/languages_combo_box.py
-#: buzz/transcriber/transcriber.py
-msgid "Detect Language"
-msgstr "Rileva la lingua"
-
-#: buzz/widgets/transcriber/mms_language_line_edit.py
-msgid "e.g., eng, fra, deu"
-msgstr "ad es., eng, fra, deu"
-
-#: buzz/widgets/transcriber/mms_language_line_edit.py
-msgid ""
-"Enter an ISO 639-3 language code (3 letters).\n"
-"Examples: eng (English), fra (French), deu (German),\n"
-"spa (Spanish), lav (Latvian)"
-msgstr ""
-"Inserisci un codice lingua ISO 639-3 (3 lettere).\n"
-"Esempi: eng (inglese), fra (francese), deu (tedesco),\n"
-"spa (spagnolo), lav (lettone)"
-
-#: buzz/widgets/transcriber/file_transcriber_widget.py
-msgid "Run"
-msgstr "Avvia"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Model:"
-msgstr "Modello:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-#: buzz/transcriber/recording_transcriber.py
-msgid "First time use of a model may take up to several minutest to load."
-msgstr ""
-"Il caricamento di un modello al primo utilizzo potrebbe richiedere diversi "
-"minuti."
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Api Key:"
-msgstr "Chiave API:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Task:"
-msgstr "Compito:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Language:"
-msgstr "Lingua:"
-
-#: buzz/widgets/transcriber/initial_prompt_text_edit.py
-msgid "Enter prompt..."
-msgstr "Inserisci richiesta..."
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Advanced Settings"
-msgstr "Impostazion avanzate"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Speech recognition settings"
-msgstr "Impostazioni di riconoscimento vocale"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Initial Prompt:"
-msgstr "Domanda iniziale:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Translation settings"
-msgstr "Impostazioni di traduzione"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Enable AI translation"
-msgstr "Abilita la traduzione AI"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "AI model:"
-msgstr "Modello AI:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid ""
-"Please translate each text sent to you from English to Spanish. Translation "
-"will be used in an automated system, please do not add any comments or "
-"notes, just the translation."
-msgstr ""
-"Si prega di tradurre ogni testo inviato dall'inglese allo spagnolo. La "
-"traduzione verrà utilizzata in un sistema automatizzato, quindi non "
-"aggiungere commenti o note, solo la traduzione."
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Instructions for AI:"
-msgstr "Istruzioni per l'AI:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Recording settings"
-msgstr "Impostazioni di registrazione"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Silence threshold:"
-msgstr "Soglia del silenzio:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Live recording mode:"
-msgstr "Modalità di registrazione in diretta:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Line separator:"
-msgstr "Separatore di riga:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Transcription step:"
-msgstr "Passo di trascrizione:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Hide unconfirmed"
-msgstr "Nascondi non confermato"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Enable live recording export"
-msgstr "Abilita l'esportazione della registrazione live"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export folder:"
-msgstr "Cartella di esportazione:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export file name:"
-msgstr "Nome del file di esportazione:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Text file (.txt)"
-msgstr "File di testo (.txt)"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "CSV (.csv)"
-msgstr "CSV (.csv)"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export file type:"
-msgstr "Tipo di file di esportazione:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid ""
-"Limit export entries\n"
-"(0 = export all):"
-msgstr ""
-"Limita le voci di esportazione\n"
-"(0 = esporta tutto):"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Word-level timings"
-msgstr "Tempistiche a livello di parola"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Extract speech"
-msgstr "Estrai il parlato"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Export:"
-msgstr "Esporta:"
-
-#: buzz/widgets/transcriber/hugging_face_search_line_edit.py
-msgid "Huggingface ID of a model"
-msgstr "ID Huggingface di un modello"
-
-#: buzz/widgets/transcriber/advanced_settings_button.py
-msgid "Advanced..."
-msgstr "Avanzate..."
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "New File Transcription"
-msgstr "Nuova trascrizione del file"
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "New URL Transcription"
-msgstr "Nuova trascrizione URL"
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "Open Transcript"
-msgstr "Apri trascrizione"
-
-#: buzz/widgets/main_window_toolbar.py buzz/settings/shortcut.py
-msgid "Cancel Transcription"
-msgstr "Annulla trascrizione"
-
-#: buzz/widgets/main_window_toolbar.py buzz/widgets/main_window.py
-#: buzz/settings/shortcut.py
-msgid "Clear History"
-msgstr "Elimina la cronologia"
-
-#: buzz/widgets/main_window_toolbar.py buzz/widgets/update_dialog.py
-msgid "Update Available"
-msgstr "Aggiornamento Disponibile"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "In Progress"
-msgstr "In corso"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Completed"
-msgstr "Completato"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Failed"
-msgstr "Non riuscito"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Canceled"
-msgstr "Annullato"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Queued"
-msgstr "In coda"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "File Name / URL"
-msgstr "Nome file / URL"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Model"
-msgstr "Modello"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Task"
-msgstr "Compito"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Status"
-msgstr "Stato"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Date Completed"
-msgstr "Data completata"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Date Added"
-msgstr "Data aggiunta"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Notes"
-msgstr "Note"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Reset Column Order"
-msgstr "Ripristina ordine colonne"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Restart Transcription"
-msgstr "Riavvia trascrizione"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Rename"
-msgstr "Rinomina"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Add/Edit Notes"
-msgstr "Aggiungi/modifica note"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Rename Transcription"
-msgstr "Rinomina trascrizione"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Enter new name:"
-msgstr "Inserisci nuovo nome:"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Enter some relevant notes for this transcription:"
-msgstr "Inserisci alcune note rilevanti per questa trascrizione:"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Cannot Restart"
-msgstr "Impossibile riavviare"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Only failed or canceled transcriptions can be restarted."
-msgstr "È possibile riavviare solo le trascrizioni non riuscite o annullate."
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Failed to restart transcription: {}"
-msgstr "Impossibile riavviare la trascrizione: {}"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid ""
-"Could not restart transcription: model not available and could not be "
-"downloaded."
-msgstr ""
-"Impossibile riavviare la trascrizione: il modello non è disponibile e non "
-"può essere scaricato."
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Could not restart transcription: transcriber worker not found."
-msgstr "Impossibile riavviare la trascrizione: trascrittore non trovato."
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Live Recording"
-msgstr "Registrazione in diretta"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Click Record to begin..."
-msgstr "Fai clic su Registra per iniziare..."
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Waiting for AI translation..."
-msgstr "In attesa della traduzione AI..."
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Microphone:"
-msgstr "Microfono:"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Show in new window"
-msgstr "Mostra in una nuova finestra"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Text Size:"
-msgstr "Dimensione testo:"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Theme"
-msgstr "Tema"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Light"
-msgstr "Chiaro"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Dark"
-msgstr "Scuro"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Custom"
-msgstr "Personalizzato"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Text Color"
-msgstr "Colore del testo"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Background Color"
-msgstr "Colore dello sfondo"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Fullscreen"
-msgstr "Schermo intero"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy"
-msgstr "Copia"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy transcription to clipboard"
-msgstr "Copia la trascrizione negli appunti"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Nothing to copy!"
-msgstr "Nessun testo da copiare!"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy failed"
-msgstr "Copia non riuscita"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copied!"
-msgstr "Copiato!"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Select Text Color"
-msgstr "Seleziona il colore del testo"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Select Background Color"
-msgstr "Seleziona il colore di sfondo"
-
-#: buzz/widgets/recording_transcriber_widget.py
-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
-msgid ""
-"Please check your audio devices or check the application logs for more "
-"information."
-msgstr ""
-"Controlla i tuoi dispositivi audio o i registri dell'applicazione per "
-"maggiori informazioni."
-
-#: buzz/widgets/update_dialog.py
-msgid "A new version of Buzz is available!"
-msgstr "È disponibile una nuova versione di Buzz!"
-
-#: buzz/widgets/update_dialog.py
-msgid "Current version:"
-msgstr "Versione attuale:"
-
-#: buzz/widgets/update_dialog.py
-msgid "New version:"
-msgstr "Nuova versione:"
-
-#: buzz/widgets/update_dialog.py
-msgid "Release Notes:"
-msgstr "Note di rilascio:"
-
-#: buzz/widgets/update_dialog.py
-msgid "Download and Install"
-msgstr "Scarica e installa"
-
-#: buzz/widgets/update_dialog.py
-msgid "No download URL available for your platform."
-msgstr "Nessun URL di download disponibile per la tua piattaforma."
-
-#: buzz/widgets/update_dialog.py
-msgid "Downloading file {} of {}..."
-msgstr "Download del file {} di {}..."
-
-#: buzz/widgets/update_dialog.py
-msgid "Downloading file {} of {} ({:.1f} MB / {:.1f} MB)..."
-msgstr "Download del file {} di {} ({:.1f} MB / {:.1f} MB)..."
-
-#: buzz/widgets/update_dialog.py
-msgid "Download Failed"
-msgstr "Download fallito"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to download the update: {}"
-msgstr "Impossibile scaricare l'aggiornamento: {}"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to save the installer: {}"
-msgstr "Impossibile salvare il programma di installazione: {}"
-
-#: buzz/widgets/update_dialog.py
-msgid "Download complete!"
-msgstr "Download completato!"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to run the installer: {}"
-msgstr "Impossibile eseguire il programma di installazione: {}"
-
-#: buzz/widgets/about_dialog.py
-msgid "Check for updates"
-msgstr "Controlla gli aggiornamenti"
-
-#: buzz/widgets/about_dialog.py
-msgid "Show logs"
-msgstr "Mostra log"
-
-#: buzz/widgets/about_dialog.py
-msgid "You're up to date!"
-msgstr "Il programma è aggiornato!"
-
-#: buzz/widgets/audio_meter_widget.py
-msgid "Average volume"
-msgstr "Volume medio"
-
-#: buzz/widgets/audio_meter_widget.py
-msgid "Queue"
-msgstr "Coda"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-msgid "Start"
-msgstr "Inizio"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-msgid "End"
-msgstr "Fine"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Text"
-msgstr "Testo"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Translation"
-msgstr "Traduzione"
-
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-msgid "View"
-msgstr "Visualizza"
-
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-msgid "Timestamps"
-msgstr "Timestamp"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Export"
-msgstr "Esporta"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Translate"
-msgstr "Tradurre"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Resize"
-msgstr "Ridimensionare"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Identify Speakers"
-msgstr "Identificare i relatori"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Find"
-msgstr "Trova"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Show/Hide Search Bar (Ctrl+F)"
-msgstr "Mostra/Nascondi barra di ricerca (Ctrl+F)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Find:"
-msgstr "Trova:"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Enter text to find..."
-msgstr "Inserisci il testo per trovare..."
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Previous match (Shift+Enter)"
-msgstr "Corrispondenza precedente (Maiusc+Invio)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Next match (Ctrl+Enter)"
-msgstr "Prossima corrispondenza (Ctrl+Enter)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Clear"
-msgstr "Elimina"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Playback Controls:"
-msgstr "Controlli di riproduzione:"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Loop Segment"
-msgstr "Ciclo di segmento"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Enable/disable looping when clicking on transcript segments"
-msgstr ""
-"Abilita/disabilita il loop quando si fa clic sui segmenti della trascrizione"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Follow Audio"
-msgstr "Segui Audio"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid ""
-"Enable/disable following the current audio position in the transcript. When "
-"enabled, automatically scrolls to current text."
-msgstr ""
-"Abilita/disabilita la lettura della posizione audio corrente nella "
-"trascrizione. Quando abilitato, scorre automaticamente fino al testo "
-"corrente."
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Scroll to Current"
-msgstr "Scorri fino al Corrente"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Scroll to the currently spoken text"
-msgstr "Scorrere fino al testo attualmente pronunciato"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "1 of 100+ matches"
-msgstr "1 di 100+ corrispondenze"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "1 of "
-msgstr "1 di"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " matches"
-msgstr "corrispondenze"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "No matches found"
-msgstr "Nessuna corrispondenza trovata"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " of 100+ matches"
-msgstr " di oltre 100 corrispondenze"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " of "
-msgstr " di "
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "API Key Required"
-msgstr "Chiave API richiesta"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Please enter OpenAI API Key in preferences"
-msgstr "Inserisci la chiave API OpenAI nelle preferenze"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend end time"
-msgstr "Estendi l'orario di fine"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend endings by up to (seconds)"
-msgstr "Estendi le terminazioni fino a (secondi)"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend endings"
-msgstr "Estendere i finali"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Resize Options"
-msgstr "Opzioni di ridimensionamento"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Desired subtitle length"
-msgstr "Lunghezza desiderata dei sottotitoli"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Available only if word level timings were disabled during transcription"
-msgstr ""
-"Disponibile solo se i tempi a livello di parola sono stati disabilitati "
-"durante la trascrizione"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge Options"
-msgstr "Opzioni di unione"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge by gap"
-msgstr "Unito per spazio"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Split by punctuation"
-msgstr "Diviso per punteggiatura"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Split by max length"
-msgstr "Diviso per lunghezza massima"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge"
-msgstr "Unione"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Available only if word level timings were enabled during transcription"
-msgstr ""
-"Disponibile solo se i tempi a livello di parola sono stati abilitati durante "
-"la trascrizione"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid ""
-"Speaker identification is not available: failed to load required libraries."
-msgstr ""
-"L'identificazione del parlante non è disponibile: impossibile caricare le "
-"librerie richieste."
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "1/8 Collecting transcripts"
-msgstr "1/8 Raccolta delle trascrizioni"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "2/8 Loading audio"
-msgstr "2/8 Caricamento audio"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "3/8 Loading alignment model"
-msgstr "3/8 Modello di allineamento del carico"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "3/8 Loading alignment model (retrying with cache...)"
-msgstr ""
-"3/8 Caricamento del modello di allineamento (nuovo tentativo con la cache...)"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid ""
-"Failed to load alignment model. Please check your internet connection and "
-"try again."
-msgstr ""
-"Impossibile caricare il modello di allineamento. Controlla la tua "
-"connessione Internet e riprova."
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "4/8 Processing audio"
-msgstr "4/8 Elaborazione audio"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "5/8 Preparing transcripts"
-msgstr "5/8 Preparazione delle trascrizioni"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "6/8 Identifying speakers"
-msgstr "6/8 Identificazione dei parlanti"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "7/8 Mapping speakers to transcripts"
-msgstr "7/8 Mappatura dei parlanti sulle trascrizioni"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "8/8 Identification done"
-msgstr "8/8 Identificazione effettuata"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "0/0 Error identifying speakers"
-msgstr "0/0 Errore nell'identificazione dei parlanti"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Step 1: Identify speakers"
-msgstr "Fase 1: identificare i parlanti"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Identify"
-msgstr "Identificare"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Ready to identify speakers"
-msgstr "Pronto a identificare i parlanti"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Audio file not found"
-msgstr "File audio non trovato"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Step 2: Name speakers"
-msgstr "Fase 2: nomi dei parlanti"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Play sample"
-msgstr "Ascolta il campione"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Merge speaker sentences"
-msgstr "Unisci le frasi del parlante"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Save"
-msgstr "Salva"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Cancelling..."
-msgstr "Annullamento in corso..."
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Cancelled"
-msgstr "Annullato"
-
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Save File"
-msgstr "Salva file"
-
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Text files"
-msgstr "File di testo"
-
-#: buzz/widgets/model_download_progress_dialog.py
-msgid "Downloading model"
-msgstr "Download del modello"
-
-#: buzz/widgets/model_download_progress_dialog.py
-msgid "remaining"
-msgstr "rimanente"
-
-#: buzz/widgets/menu_bar.py
-msgid "Import File..."
-msgstr "Importa file.."
-
-#: buzz/widgets/menu_bar.py
-msgid "Import URL..."
-msgstr "Importa URL..."
-
-#: buzz/widgets/menu_bar.py
-msgid "Import Folder..."
-msgstr "Importa cartella..."
-
-#: buzz/widgets/menu_bar.py
-msgid "About"
-msgstr "Informazioni"
-
-#: buzz/widgets/menu_bar.py
-msgid "Preferences..."
-msgstr "Preferenze..."
-
-#: buzz/widgets/menu_bar.py
-msgid "Help"
-msgstr "Aiuto"
-
-#: buzz/widgets/menu_bar.py
-msgid "File"
-msgstr "File"
-
-#: buzz/widgets/main_window.py
-msgid ""
-"Are you sure you want to delete the selected transcription(s)? This action "
-"cannot be undone."
-msgstr ""
-"Sei certo di voler eliminare le trascrizioni selezionate? Questa azione non "
-"può essere annullata."
-
-#: buzz/widgets/main_window.py
-msgid "Select audio file"
-msgstr "Seleziona file audio"
-
-#: buzz/widgets/main_window.py
-msgid "Select folder"
-msgstr "Seleziona cartella di input"
-
-#: buzz/widgets/main_window.py
-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
-#: buzz/transcriber/recording_transcriber.py
-msgid "Whisper server failed to start. Check logs for details."
-msgstr ""
-"Impossibile avviare il server Whisper. Controllare i log per i dettagli."
-
-#: buzz/transcriber/local_whisper_cpp_server_transcriber.py
-#: buzz/transcriber/recording_transcriber.py
-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 ""
-"Impossibile avviare il server Whisper a causa di memoria insufficiente. "
-"Riprovare con un modello più piccolo. Per forzare la modalità CPU, "
-"utilizzare la variabile d'ambiente BUZZ_FORCE_CPU=TRUE"
-
-#: buzz/transcriber/transcriber.py
-msgid "Translate to English"
-msgstr "Traduci in inglese"
-
-#: buzz/transcriber/transcriber.py
-msgid "Transcribe"
-msgstr "Trascrivere"
-
-#: buzz/transcriber/transcriber.py
-msgid "Chinese"
-msgstr "Cinese"
-
-#: buzz/transcriber/transcriber.py
-msgid "Russian"
-msgstr "Russo"
-
-#: buzz/transcriber/transcriber.py
-msgid "Korean"
-msgstr "Coreano"
-
-#: buzz/transcriber/transcriber.py
-msgid "French"
-msgstr "Francese"
-
-#: buzz/transcriber/transcriber.py
-msgid "Portuguese"
-msgstr "Portoghese"
-
-#: buzz/transcriber/transcriber.py
-msgid "Turkish"
-msgstr "Turco"
-
-#: buzz/transcriber/transcriber.py
-msgid "Arabic"
-msgstr "Arabo"
-
-#: buzz/transcriber/transcriber.py
-msgid "Swedish"
-msgstr "Svedese"
-
-#: buzz/transcriber/transcriber.py
-msgid "Indonesian"
-msgstr "Indonesiano"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hindi"
-msgstr "Hindi"
-
-#: buzz/transcriber/transcriber.py
-msgid "Finnish"
-msgstr "Finlandese"
-
-#: buzz/transcriber/transcriber.py
-msgid "Vietnamese"
-msgstr "Vietnamita"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hebrew"
-msgstr "Ebraico"
-
-#: buzz/transcriber/transcriber.py
-msgid "Greek"
-msgstr "Greco"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malay"
-msgstr "Malese"
-
-#: buzz/transcriber/transcriber.py
-msgid "Czech"
-msgstr "Ceco"
-
-#: buzz/transcriber/transcriber.py
-msgid "Romanian"
-msgstr "Rumeno"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hungarian"
-msgstr "Ungherese"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tamil"
-msgstr "Tamil"
-
-#: buzz/transcriber/transcriber.py
-msgid "Norwegian"
-msgstr "Norvegese"
-
-#: buzz/transcriber/transcriber.py
-msgid "Thai"
-msgstr "Tailandese"
-
-#: buzz/transcriber/transcriber.py
-msgid "Urdu"
-msgstr "Urdu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Croatian"
-msgstr "Croato"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bulgarian"
-msgstr "Bulgaro"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lithuanian"
-msgstr "Lituano"
-
-#: buzz/transcriber/transcriber.py
-msgid "Latin"
-msgstr "Latino"
-
-#: buzz/transcriber/transcriber.py
-msgid "Maori"
-msgstr "Maori"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malayalam"
-msgstr "Malayalam"
-
-#: buzz/transcriber/transcriber.py
-msgid "Welsh"
-msgstr "Gallese"
-
-#: buzz/transcriber/transcriber.py
-msgid "Slovak"
-msgstr "Slovacco"
-
-#: buzz/transcriber/transcriber.py
-msgid "Telugu"
-msgstr "Telugu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Persian"
-msgstr "Persiano"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bengali"
-msgstr "Bengalese"
-
-#: buzz/transcriber/transcriber.py
-msgid "Serbian"
-msgstr "Serbo"
-
-#: buzz/transcriber/transcriber.py
-msgid "Azerbaijani"
-msgstr "Azerbaijani"
-
-#: buzz/transcriber/transcriber.py
-msgid "Slovenian"
-msgstr "Sloveno"
-
-#: buzz/transcriber/transcriber.py
-msgid "Kannada"
-msgstr "Kannada"
-
-#: buzz/transcriber/transcriber.py
-msgid "Estonian"
-msgstr "Estone"
-
-#: buzz/transcriber/transcriber.py
-msgid "Macedonian"
-msgstr "Macedone"
-
-#: buzz/transcriber/transcriber.py
-msgid "Breton"
-msgstr "Bretone"
-
-#: buzz/transcriber/transcriber.py
-msgid "Basque"
-msgstr "Basco"
-
-#: buzz/transcriber/transcriber.py
-msgid "Icelandic"
-msgstr "Islandese"
-
-#: buzz/transcriber/transcriber.py
-msgid "Armenian"
-msgstr "Armeno"
-
-#: buzz/transcriber/transcriber.py
-msgid "Nepali"
-msgstr "Nepalese"
-
-#: buzz/transcriber/transcriber.py
-msgid "Mongolian"
-msgstr "Mongola"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bosnian"
-msgstr "Bosniaco"
-
-#: buzz/transcriber/transcriber.py
-msgid "Kazakh"
-msgstr "kazako"
-
-#: buzz/transcriber/transcriber.py
-msgid "Albanian"
-msgstr "Albanese"
-
-#: buzz/transcriber/transcriber.py
-msgid "Swahili"
-msgstr "Swahili"
-
-#: buzz/transcriber/transcriber.py
-msgid "Galician"
-msgstr "Galiziano"
-
-#: buzz/transcriber/transcriber.py
-msgid "Marathi"
-msgstr "Marathi"
-
-#: buzz/transcriber/transcriber.py
-msgid "Punjabi"
-msgstr "Punjabi"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sinhala"
-msgstr "Singalese"
-
-#: buzz/transcriber/transcriber.py
-msgid "Khmer"
-msgstr "Khmer"
-
-#: buzz/transcriber/transcriber.py
-msgid "Shona"
-msgstr "Shona"
-
-#: buzz/transcriber/transcriber.py
-msgid "Yoruba"
-msgstr "Yoruba"
-
-#: buzz/transcriber/transcriber.py
-msgid "Somali"
-msgstr "Somalo"
-
-#: buzz/transcriber/transcriber.py
-msgid "Afrikaans"
-msgstr "Afrikaans"
-
-#: buzz/transcriber/transcriber.py
-msgid "Occitan"
-msgstr "Occitano"
-
-#: buzz/transcriber/transcriber.py
-msgid "Georgian"
-msgstr "Georgiano"
-
-#: buzz/transcriber/transcriber.py
-msgid "Belarusian"
-msgstr "Biellorusso"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tajik"
-msgstr "Tagiko"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sindhi"
-msgstr "Sindhi"
-
-#: buzz/transcriber/transcriber.py
-msgid "Gujarati"
-msgstr "Gujarati"
-
-#: buzz/transcriber/transcriber.py
-msgid "Amharic"
-msgstr "Amarico"
-
-#: buzz/transcriber/transcriber.py
-msgid "Yiddish"
-msgstr "Yiddish"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lao"
-msgstr "Lao"
-
-#: buzz/transcriber/transcriber.py
-msgid "Uzbek"
-msgstr "Uzbeko"
-
-#: buzz/transcriber/transcriber.py
-msgid "Faroese"
-msgstr "Faroese"
-
-#: buzz/transcriber/transcriber.py
-msgid "Haitian Creole"
-msgstr "Creolo haitiano"
-
-#: buzz/transcriber/transcriber.py
-msgid "Pashto"
-msgstr "Pashtu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Turkmen"
-msgstr "Turkmen"
-
-#: buzz/transcriber/transcriber.py
-msgid "Nynorsk"
-msgstr "Nynorsk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Maltese"
-msgstr "Maltese"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sanskrit"
-msgstr "Sanscrito"
-
-#: buzz/transcriber/transcriber.py
-msgid "Luxembourgish"
-msgstr "Lussemburghese"
-
-#: buzz/transcriber/transcriber.py
-msgid "Myanmar"
-msgstr "Birmano"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tibetan"
-msgstr "Tibetano"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tagalog"
-msgstr "Tagalog"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malagasy"
-msgstr "Malgascio"
-
-#: buzz/transcriber/transcriber.py
-msgid "Assamese"
-msgstr "Assamese"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tatar"
-msgstr "Tartaro"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hawaiian"
-msgstr "Hawaiano"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lingala"
-msgstr "Lingala"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hausa"
-msgstr "Hausa"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bashkir"
-msgstr "Baschiro"
-
-#: buzz/transcriber/transcriber.py
-msgid "Javanese"
-msgstr "Giavanese"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sundanese"
-msgstr "Sundanese"
-
-#: buzz/transcriber/transcriber.py
-msgid "Cantonese"
-msgstr "Cantonese"
-
-#: buzz/transcriber/recording_transcriber.py buzz/model_loader.py
-msgid "A connection error occurred"
-msgstr "Si è verificato un errore di connessione"
-
-#: buzz/transcriber/recording_transcriber.py
-msgid "Starting Whisper.cpp..."
-msgstr "Avvio di Whisper.cpp..."
-
-#: buzz/transcriber/recording_transcriber.py
-msgid "Starting transcription..."
-msgstr "Inizio trascrizione..."
-
-#: buzz/settings/shortcut.py
-msgid "Open Record Window"
-msgstr "Apri finestra di registrazione"
-
-#: buzz/settings/shortcut.py
-msgid "Import File"
-msgstr "Importa file"
-
-#: buzz/settings/shortcut.py
-msgid "Open Preferences Window"
-msgstr "Apri la finestra delle preferenze"
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Text"
-msgstr "Visualizza il testo della trascrizione"
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Translation"
-msgstr "Visualizza la trascrizione della traduzione"
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Timestamps"
-msgstr "Visualizza i timestamp della trascrizione"
-
-#: buzz/settings/shortcut.py
-msgid "Search Transcript"
-msgstr "Cerca trascrizione"
-
-#: buzz/settings/shortcut.py
-msgid "Go to Next Transcript Search Result"
-msgstr "Vai al risultato della ricerca della trascrizione successiva"
-
-#: buzz/settings/shortcut.py
-msgid "Go to Previous Transcript Search Result"
-msgstr "Vai al risultato della ricerca della trascrizione precedente"
-
-#: buzz/settings/shortcut.py
-msgid "Scroll to Current Text"
-msgstr "Scorri fino al testo corrente"
-
-#: buzz/settings/shortcut.py
-msgid "Play/Pause Audio"
-msgstr "Riproduci/Pausa audio"
-
-#: buzz/settings/shortcut.py
-msgid "Replay Current Segment"
-msgstr "Riproduci il segmento corrente"
-
-#: buzz/settings/shortcut.py
-msgid "Toggle Playback Controls"
-msgstr "Attiva/disattiva i controlli di riproduzione"
-
-#: buzz/settings/shortcut.py
-msgid "Decrease Segment Start Time"
-msgstr "Riduci l'ora di inizio del segmento"
-
-#: buzz/settings/shortcut.py
-msgid "Increase Segment Start Time"
-msgstr "Aumenta l'ora di inizio del segmento"
-
-#: buzz/settings/shortcut.py
-msgid "Decrease Segment End Time"
-msgstr "Diminuisci l'ora di fine del segmento"
-
-#: buzz/settings/shortcut.py
-msgid "Increase Segment End Time"
-msgstr "Aumenta l'ora di fine del segmento"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append below"
-msgstr "Aggiungere sotto"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append above"
-msgstr "Aggiungere sopra"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append and correct"
-msgstr "Aggiungere e correggere"
-
-#: buzz/file_transcriber_queue_worker.py
-msgid ""
-"Speech extraction failed! Check your internet connection — a model may need "
-"to be downloaded."
-msgstr ""
-"Estrazione del parlato non riuscita! Controlla la tua connessione Internet — "
-"potrebbe essere necessario scaricare un modello."
-
-#~ msgid "Comma-separated, e.g. \"0.0, 0.2, 0.4, 0.6, 0.8, 1.0\""
-#~ msgstr "Separate da virgola, esempio: \"0.0, 0.2, 0.4, 0.6, 0.8, 1.0\""
-
-#~ msgid "Temperature:"
-#~ msgstr "Temperatura:"
-
-#~ msgid "Please translate each text sent to you from English to Spanish."
-#~ msgstr ""
-#~ "Per favore, traduci ogni testo che ti viene inviato dall'inglese allo "
-#~ "spagnolo."
-
-#~ msgid "Translation error, see logs!"
-#~ msgstr "Errore di traduzione, controlla i log!"
-
-#~ msgid "Snap permission notice"
-#~ msgstr "Avviso di autorizzazione Snap"
-
-#~ msgid ""
-#~ "Detected missing permissions, please check that snap permissions have "
-#~ "been granted"
-#~ msgstr ""
-#~ "Rilevate autorizzazioni mancanti, verificare che le autorizzazioni snap "
-#~ "siano state concesse"
-
-#~ msgid ""
-#~ "To enable necessary permissions run the following commands in the terminal"
-#~ msgstr ""
-#~ "Per abilitare le autorizzazioni necessarie, eseguire i seguenti comandi "
-#~ "nel terminale"
-
-#~ msgid "Close"
-#~ msgstr "Chiudi"
-
-#~ msgid "Enter instructions for AI on how to translate..."
-#~ msgstr "Inserisci le istruzioni per l'IA su come tradurre..."
-
-#~ msgid "Enter target characters per subtitle:"
-#~ msgstr "Inserisci i caratteri di destinazione per sottotitolo:"
-
-#~ msgid "ID"
-#~ msgstr "ID"
-
-#~ msgid "Undo"
-#~ msgstr "Annulla"
-
-#~ msgid "Redo"
-#~ msgstr "Rifai"
-
-#~ msgid "Downloading model (0%, unknown time remaining)"
-#~ msgstr "Scaricando il modello (0\", tempo restante sconosciuto)"
diff --git a/buzz/locale/ja_JP/LC_MESSAGES/buzz.po b/buzz/locale/ja_JP/LC_MESSAGES/buzz.po
deleted file mode 100644
index 8cbc04d0..00000000
--- a/buzz/locale/ja_JP/LC_MESSAGES/buzz.po
+++ /dev/null
@@ -1,1661 +0,0 @@
-msgid ""
-msgstr ""
-"Project-Id-Version: \n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2026-03-07 20:20+0200\n"
-"PO-Revision-Date: \n"
-"Last-Translator: nunawa <71294849+nunawa@users.noreply.github.com>\n"
-"Language-Team: \n"
-"Language: ja_JP\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Generator: Poedit 3.5\n"
-
-#: buzz/widgets/import_url_dialog.py buzz/settings/shortcut.py
-msgid "Import URL"
-msgstr "URLをインポートする"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "https://example.com/audio.mp3"
-msgstr "https://example.com/audio.mp3"
-
-#: buzz/widgets/import_url_dialog.py
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-#: buzz/widgets/main_window.py
-msgid "Ok"
-msgstr "Ok"
-
-#: buzz/widgets/import_url_dialog.py
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-#: buzz/widgets/model_download_progress_dialog.py buzz/widgets/main_window.py
-msgid "Cancel"
-msgstr "キャンセル"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "URL:"
-msgstr "URL:"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "Invalid URL"
-msgstr "無効なURL"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "The URL you entered is invalid."
-msgstr "入力されたURLは無効です。"
-
-#: buzz/widgets/presentation_window.py
-msgid "Live Transcript Presentation"
-msgstr "ライブ文字起こしプレゼンテーション"
-
-#: buzz/widgets/preferences_dialog/shortcuts_editor_preferences_widget.py
-msgid "Reset to Defaults"
-msgstr "デフォルトに戻す"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "English"
-msgstr "英語"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Catalan"
-msgstr "カタルーニャ語"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Danish"
-msgstr "デンマーク語"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Dutch"
-msgstr "オランダ語"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "German"
-msgstr "ドイツ語"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Spanish"
-msgstr "スペイン語"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Italian"
-msgstr "イタリア語"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Japanese"
-msgstr "日本語"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Latvian"
-msgstr "ラトビア語"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Polish"
-msgstr "ポーランド語"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Portuguese (Brazil)"
-msgstr "ポルトガル語(ブラジル)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Ukrainian"
-msgstr "ウクライナ語"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Chinese (Simplified)"
-msgstr "中国語(簡体字)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Chinese (Traditional)"
-msgstr "中国語(繁体字)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Restart required!"
-msgstr "再起動が必要です!"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Ui Language"
-msgstr "UIの言語"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Font Size"
-msgstr "フォントサイズ"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Test"
-msgstr "テスト"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API key"
-msgstr "OpenAI APIキー"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI base url"
-msgstr "OpenAI ベースURL"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API model"
-msgstr "OpenAI APIモデル"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Default export file name"
-msgstr "デフォルトの出力ファイル名"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Enable live recording transcription export"
-msgstr "ライブ録音書き起こしの出力を有効にする"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Browse"
-msgstr "参照"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Export folder"
-msgstr "出力フォルダ"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Live recording mode"
-msgstr "ライブ録音モード"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Note: Live recording export settings will be moved to the Advanced Settings "
-"in the Live Recording screen in a future version."
-msgstr ""
-"注意:ライブ録音の出力設定は、将来のバージョンでライブ録音画面の詳細設定に移"
-"動されます。"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Use 8-bit quantization to reduce memory usage"
-msgstr "メモリ使用量を削減するために8ビット量子化を使用する"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Applies to Huggingface and Faster Whisper models. Reduces GPU memory usage "
-"but may slightly decrease transcription quality."
-msgstr ""
-"HuggingfaceおよびFaster Whisperモデルに適用されます。GPUメモリ使用量を削減し"
-"ますが、文字起こしの品質がわずかに低下する場合があります。"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Reduce GPU RAM"
-msgstr "GPU RAMを削減する"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Use only CPU and disable GPU acceleration"
-msgstr "CPUのみを使用してGPUアクセラレーションを無効にする"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Set this if larger models do not fit your GPU memory and Buzz crashes"
-msgstr ""
-"大きなモデルがGPUメモリに収まらずBuzzがクラッシュする場合に設定してください"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Disable GPU"
-msgstr "GPUを無効にする"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API Key Test"
-msgstr "OpenAI APIキー テスト"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Your API key is valid. Buzz will use this key to perform Whisper API "
-"transcriptions and AI translations."
-msgstr ""
-"あなたのAPIキーは有効です。Buzzはこのキーを使ってWhisper APIの書き起こしとAI"
-"翻訳を行います。"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Invalid API key"
-msgstr "無効なAPIキー"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"API supports only base64 characters (A-Za-z0-9+/=_-). Other characters in "
-"API key may cause errors."
-msgstr ""
-"APIはbase64文字(A-Za-z0-9+/=_-)のみをサポートしています。APIキーにその他の"
-"文字が含まれているとエラーが発生する場合があります。"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Select Export Folder"
-msgstr "出力フォルダを選択"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"OpenAI API returned invalid response. Please check the API url or your key. "
-"Transcription and translation may still work if the API does not support key "
-"validation."
-msgstr ""
-"OpenAI APIが無効な応答を返しました。APIのURLまたはキーを確認してください。API"
-"がキーの検証をサポートしていない場合でも、文字起こしや翻訳は動作する場合があ"
-"ります。"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Enable folder watch"
-msgstr "フォルダ監視を有効にする"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Delete processed files"
-msgstr "処理済みファイルを削除"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Input folder"
-msgstr "入力フォルダ"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Output folder"
-msgstr "出力フォルダ"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Select Input Folder"
-msgstr "入力フォルダを選択"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Select Output Folder"
-msgstr "出力フォルダを選択"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Preferences"
-msgstr "設定"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "General"
-msgstr "一般"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Models"
-msgstr "モデル"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Shortcuts"
-msgstr "ショートカット"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Folder Watch"
-msgstr "フォルダ監視"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Group"
-msgstr "グループ"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Huggingface ID of a Faster whisper model"
-msgstr "Faster whisperモデルのHuggingface ID"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download"
-msgstr "ダウンロード"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Show file location"
-msgstr "ファイルの場所を表示"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Delete"
-msgstr "削除"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Downloaded"
-msgstr "ダウンロード済み"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Available for Download"
-msgstr "ダウンロード可能"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download link to Whisper.cpp ggml model file"
-msgstr "Whisper.cpp ggmlモデルファイルのダウンロードリンク"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Delete Model"
-msgstr "モデルを削除"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Are you sure you want to delete the selected model?"
-msgstr "選択したモデルを本当に削除しますか?"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download failed"
-msgstr "ダウンロード失敗"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcription_tasks_table_widget.py
-#: buzz/widgets/update_dialog.py buzz/widgets/main_window.py
-#: buzz/model_loader.py
-msgid "Error"
-msgstr "エラー"
-
-#: buzz/widgets/record_button.py buzz/widgets/main_window_toolbar.py
-msgid "Record"
-msgstr "録音する"
-
-#: buzz/widgets/record_button.py
-msgid "Stop"
-msgstr "停止する"
-
-#: buzz/widgets/transcriber/languages_combo_box.py
-#: buzz/transcriber/transcriber.py
-msgid "Detect Language"
-msgstr "自動検出"
-
-#: buzz/widgets/transcriber/mms_language_line_edit.py
-msgid "e.g., eng, fra, deu"
-msgstr "例: eng, fra, deu"
-
-#: buzz/widgets/transcriber/mms_language_line_edit.py
-msgid ""
-"Enter an ISO 639-3 language code (3 letters).\n"
-"Examples: eng (English), fra (French), deu (German),\n"
-"spa (Spanish), lav (Latvian)"
-msgstr ""
-"ISO 639-3言語コード(3文字)を入力してください。\n"
-"例: eng(英語)、fra(フランス語)、deu(ドイツ語)、\n"
-"spa(スペイン語)、lav(ラトビア語)"
-
-#: buzz/widgets/transcriber/file_transcriber_widget.py
-msgid "Run"
-msgstr "実行"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Model:"
-msgstr "モデル:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-#: buzz/transcriber/recording_transcriber.py
-msgid "First time use of a model may take up to several minutest to load."
-msgstr "モデルの初回使用時は読み込みに数分かかる場合があります。"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Api Key:"
-msgstr "APIキー:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Task:"
-msgstr "タスク:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Language:"
-msgstr "言語:"
-
-#: buzz/widgets/transcriber/initial_prompt_text_edit.py
-msgid "Enter prompt..."
-msgstr "プロンプトを入力..."
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Advanced Settings"
-msgstr "高度な設定"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Speech recognition settings"
-msgstr "音声認識設定"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Initial Prompt:"
-msgstr "プロンプト:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Translation settings"
-msgstr "翻訳設定"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Enable AI translation"
-msgstr "AIによる翻訳を有効にする"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "AI model:"
-msgstr "AIのモデル:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid ""
-"Please translate each text sent to you from English to Spanish. Translation "
-"will be used in an automated system, please do not add any comments or "
-"notes, just the translation."
-msgstr ""
-"送信された各テキストを英語からスペイン語に翻訳してください。翻訳は自動化され"
-"たシステムで使用されます。コメントやメモは追加せず、翻訳のみを提供してくださ"
-"い。"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Instructions for AI:"
-msgstr "AIへの指示:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Recording settings"
-msgstr "録音設定"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Silence threshold:"
-msgstr "無音閾値:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Live recording mode:"
-msgstr "ライブ録音モード:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Line separator:"
-msgstr "行区切り:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Transcription step:"
-msgstr "トランスクリプションステップ:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Hide unconfirmed"
-msgstr "未確認を非表示"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Enable live recording export"
-msgstr "ライブ録音の出力を有効にする"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export folder:"
-msgstr "出力フォルダ:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export file name:"
-msgstr "出力ファイル名:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Text file (.txt)"
-msgstr "テキストファイル (.txt)"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "CSV (.csv)"
-msgstr "CSV (.csv)"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export file type:"
-msgstr "出力ファイル形式:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid ""
-"Limit export entries\n"
-"(0 = export all):"
-msgstr ""
-"出力エントリ数の制限\n"
-"(0 = すべて出力):"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Word-level timings"
-msgstr "単語レベルでのタイミング"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Extract speech"
-msgstr "音声を抽出する"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Export:"
-msgstr "出力形式:"
-
-#: buzz/widgets/transcriber/hugging_face_search_line_edit.py
-msgid "Huggingface ID of a model"
-msgstr "モデルのHuggingface ID"
-
-#: buzz/widgets/transcriber/advanced_settings_button.py
-msgid "Advanced..."
-msgstr "高度な設定..."
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "New File Transcription"
-msgstr "新しいファイルの文字起こし"
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "New URL Transcription"
-msgstr "新しいURLの文字起こし"
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "Open Transcript"
-msgstr "文字起こしを開く"
-
-#: buzz/widgets/main_window_toolbar.py buzz/settings/shortcut.py
-msgid "Cancel Transcription"
-msgstr "文字起こしをキャンセルする"
-
-#: buzz/widgets/main_window_toolbar.py buzz/widgets/main_window.py
-#: buzz/settings/shortcut.py
-msgid "Clear History"
-msgstr "履歴を削除する"
-
-#: buzz/widgets/main_window_toolbar.py buzz/widgets/update_dialog.py
-msgid "Update Available"
-msgstr "アップデートあり"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "In Progress"
-msgstr "進行中"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Completed"
-msgstr "完了"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Failed"
-msgstr "失敗"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Canceled"
-msgstr "キャンセル済み"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Queued"
-msgstr "キュー済み"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "File Name / URL"
-msgstr "ファイル名 / URL"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Model"
-msgstr "モデル"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Task"
-msgstr "タスク"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Status"
-msgstr "ステータス"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Date Completed"
-msgstr "完了日"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Date Added"
-msgstr "追加日"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Notes"
-msgstr "メモ"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Reset Column Order"
-msgstr "列の順序をリセット"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Restart Transcription"
-msgstr "文字起こしを再開する"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Rename"
-msgstr "名前を変更"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Add/Edit Notes"
-msgstr "メモを追加・編集"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Rename Transcription"
-msgstr "文字起こしの名前を変更"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Enter new name:"
-msgstr "新しい名前を入力してください:"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Enter some relevant notes for this transcription:"
-msgstr "この文字起こしに関するメモを入力してください:"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Cannot Restart"
-msgstr "再開できません"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Only failed or canceled transcriptions can be restarted."
-msgstr "再開できるのは失敗またはキャンセルされた文字起こしのみです。"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Failed to restart transcription: {}"
-msgstr "文字起こしの再開に失敗しました: {}"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid ""
-"Could not restart transcription: model not available and could not be "
-"downloaded."
-msgstr ""
-"文字起こしを再開できませんでした: モデルが利用できず、ダウンロードもできませ"
-"んでした。"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Could not restart transcription: transcriber worker not found."
-msgstr "文字起こしを再開できませんでした: 文字起こしワーカーが見つかりません。"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Live Recording"
-msgstr "ライブ録音"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Click Record to begin..."
-msgstr "クリックで録音を開始..."
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Waiting for AI translation..."
-msgstr "AI翻訳を待っています..."
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Microphone:"
-msgstr "マイク:"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Show in new window"
-msgstr "新しいウィンドウで表示"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Text Size:"
-msgstr "テキストサイズ:"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Theme"
-msgstr "テーマ"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Light"
-msgstr "ライト"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Dark"
-msgstr "ダーク"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Custom"
-msgstr "カスタム"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Text Color"
-msgstr "テキストの色"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Background Color"
-msgstr "背景の色"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Fullscreen"
-msgstr "全画面表示"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy"
-msgstr "コピー"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy transcription to clipboard"
-msgstr "文字起こしをクリップボードにコピー"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Nothing to copy!"
-msgstr "コピーするものがありません!"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy failed"
-msgstr "コピーに失敗しました"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copied!"
-msgstr "コピーしました!"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Select Text Color"
-msgstr "テキストの色を選択"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Select Background Color"
-msgstr "背景の色を選択"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "An error occurred while starting a new recording:"
-msgstr "新規録音開始時にエラーが発生しました:"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid ""
-"Please check your audio devices or check the application logs for more "
-"information."
-msgstr ""
-"オーディオデバイスを確認するか、詳細をアプリケーションのログで確認してくださ"
-"い。"
-
-#: buzz/widgets/update_dialog.py
-msgid "A new version of Buzz is available!"
-msgstr "Buzzの新しいバージョンが利用可能です!"
-
-#: buzz/widgets/update_dialog.py
-msgid "Current version:"
-msgstr "現在のバージョン:"
-
-#: buzz/widgets/update_dialog.py
-msgid "New version:"
-msgstr "新しいバージョン:"
-
-#: buzz/widgets/update_dialog.py
-msgid "Release Notes:"
-msgstr "リリースノート:"
-
-#: buzz/widgets/update_dialog.py
-msgid "Download and Install"
-msgstr "ダウンロードしてインストール"
-
-#: buzz/widgets/update_dialog.py
-msgid "No download URL available for your platform."
-msgstr "お使いのプラットフォーム向けのダウンロードURLがありません。"
-
-#: buzz/widgets/update_dialog.py
-msgid "Downloading file {} of {}..."
-msgstr "ファイル {} / {} をダウンロード中..."
-
-#: buzz/widgets/update_dialog.py
-msgid "Downloading file {} of {} ({:.1f} MB / {:.1f} MB)..."
-msgstr "ファイル {} / {} をダウンロード中 ({:.1f} MB / {:.1f} MB)..."
-
-#: buzz/widgets/update_dialog.py
-msgid "Download Failed"
-msgstr "ダウンロード失敗"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to download the update: {}"
-msgstr "アップデートのダウンロードに失敗しました: {}"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to save the installer: {}"
-msgstr "インストーラーの保存に失敗しました: {}"
-
-#: buzz/widgets/update_dialog.py
-msgid "Download complete!"
-msgstr "ダウンロード完了!"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to run the installer: {}"
-msgstr "インストーラーの実行に失敗しました: {}"
-
-#: buzz/widgets/about_dialog.py
-msgid "Check for updates"
-msgstr "アップデートを確認する"
-
-#: buzz/widgets/about_dialog.py
-msgid "Show logs"
-msgstr "ログを表示"
-
-#: buzz/widgets/about_dialog.py
-msgid "You're up to date!"
-msgstr "最新の状態です!"
-
-#: buzz/widgets/audio_meter_widget.py
-msgid "Average volume"
-msgstr "平均音量"
-
-#: buzz/widgets/audio_meter_widget.py
-msgid "Queue"
-msgstr "キュー"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-msgid "Start"
-msgstr "開始"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-msgid "End"
-msgstr "終了"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Text"
-msgstr "テキスト"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Translation"
-msgstr "翻訳"
-
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-msgid "View"
-msgstr "表示"
-
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-msgid "Timestamps"
-msgstr "タイムスタンプ"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Export"
-msgstr "出力"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Translate"
-msgstr "翻訳"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Resize"
-msgstr "リサイズ"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Identify Speakers"
-msgstr "話者を識別する"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Find"
-msgstr "検索"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Show/Hide Search Bar (Ctrl+F)"
-msgstr "検索バーの表示・非表示 (Ctrl+F)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Find:"
-msgstr "検索:"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Enter text to find..."
-msgstr "検索するテキストを入力..."
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Previous match (Shift+Enter)"
-msgstr "前の一致 (Shift+Enter)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Next match (Ctrl+Enter)"
-msgstr "次の一致 (Ctrl+Enter)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Clear"
-msgstr "クリア"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Playback Controls:"
-msgstr "再生コントロール:"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Loop Segment"
-msgstr "セグメントをループ"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Enable/disable looping when clicking on transcript segments"
-msgstr "文字起こしセグメントをクリックしたときのループを有効・無効にする"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Follow Audio"
-msgstr "音声に追従"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid ""
-"Enable/disable following the current audio position in the transcript. When "
-"enabled, automatically scrolls to current text."
-msgstr ""
-"文字起こし内で現在の音声位置への追従を有効・無効にします。有効にすると、現在"
-"のテキストに自動的にスクロールします。"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Scroll to Current"
-msgstr "現在位置にスクロール"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Scroll to the currently spoken text"
-msgstr "現在話されているテキストにスクロールする"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "1 of 100+ matches"
-msgstr "100件以上中の1件目"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "1 of "
-msgstr "1 / "
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " matches"
-msgstr " 件一致"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "No matches found"
-msgstr "一致する項目が見つかりません"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " of 100+ matches"
-msgstr " / 100件以上一致"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " of "
-msgstr " / "
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "API Key Required"
-msgstr "APIキーが必要"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Please enter OpenAI API Key in preferences"
-msgstr "設定画面でOpenAI APIキーを入力してください"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend end time"
-msgstr "終了時刻を延長"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend endings by up to (seconds)"
-msgstr "終了を最大(秒)まで延長"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend endings"
-msgstr "終了を延長"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Resize Options"
-msgstr "リサイズオプション"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Desired subtitle length"
-msgstr "希望する字幕の長さ"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Available only if word level timings were disabled during transcription"
-msgstr "文字起こし時に単語レベルのタイミングが無効だった場合のみ使用可能"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge Options"
-msgstr "結合オプション"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge by gap"
-msgstr "間隔で結合"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Split by punctuation"
-msgstr "句読点で分割"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Split by max length"
-msgstr "最大文字数で分割"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge"
-msgstr "結合"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Available only if word level timings were enabled during transcription"
-msgstr "文字起こし時に単語レベルのタイミングが有効だった場合のみ使用可能"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid ""
-"Speaker identification is not available: failed to load required libraries."
-msgstr "話者識別は利用できません: 必要なライブラリの読み込みに失敗しました。"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "1/8 Collecting transcripts"
-msgstr "1/8 文字起こしを収集中"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "2/8 Loading audio"
-msgstr "2/8 音声を読み込み中"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "3/8 Loading alignment model"
-msgstr "3/8 アライメントモデルを読み込み中"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "3/8 Loading alignment model (retrying with cache...)"
-msgstr "3/8 アライメントモデルを読み込み中(キャッシュで再試行中...)"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid ""
-"Failed to load alignment model. Please check your internet connection and "
-"try again."
-msgstr ""
-"アライメントモデルの読み込みに失敗しました。インターネット接続を確認して再度"
-"お試しください。"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "4/8 Processing audio"
-msgstr "4/8 音声を処理中"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "5/8 Preparing transcripts"
-msgstr "5/8 文字起こしを準備中"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "6/8 Identifying speakers"
-msgstr "6/8 話者を識別中"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "7/8 Mapping speakers to transcripts"
-msgstr "7/8 話者を文字起こしにマッピング中"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "8/8 Identification done"
-msgstr "8/8 識別完了"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "0/0 Error identifying speakers"
-msgstr "0/0 話者識別中にエラーが発生しました"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Step 1: Identify speakers"
-msgstr "ステップ1: 話者を識別する"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Identify"
-msgstr "識別"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Ready to identify speakers"
-msgstr "話者を識別する準備ができました"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Audio file not found"
-msgstr "音声ファイルが見つかりません"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Step 2: Name speakers"
-msgstr "ステップ2: 話者に名前を付ける"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Play sample"
-msgstr "サンプルを再生"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Merge speaker sentences"
-msgstr "話者の文を結合"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Save"
-msgstr "保存"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Cancelling..."
-msgstr "キャンセル中..."
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Cancelled"
-msgstr "キャンセル済み"
-
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Save File"
-msgstr "ファイルを保存"
-
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Text files"
-msgstr "テキストファイル"
-
-#: buzz/widgets/model_download_progress_dialog.py
-msgid "Downloading model"
-msgstr "モデルをダウンロード中"
-
-#: buzz/widgets/model_download_progress_dialog.py
-msgid "remaining"
-msgstr "残り"
-
-#: buzz/widgets/menu_bar.py
-msgid "Import File..."
-msgstr "ファイルをインポートする..."
-
-#: buzz/widgets/menu_bar.py
-msgid "Import URL..."
-msgstr "URLをインポートする..."
-
-#: buzz/widgets/menu_bar.py
-msgid "Import Folder..."
-msgstr "フォルダをインポートする..."
-
-#: buzz/widgets/menu_bar.py
-msgid "About"
-msgstr "About"
-
-#: buzz/widgets/menu_bar.py
-msgid "Preferences..."
-msgstr "設定..."
-
-#: buzz/widgets/menu_bar.py
-msgid "Help"
-msgstr "ヘルプ"
-
-#: buzz/widgets/menu_bar.py
-msgid "File"
-msgstr "ファイル"
-
-#: buzz/widgets/main_window.py
-msgid ""
-"Are you sure you want to delete the selected transcription(s)? This action "
-"cannot be undone."
-msgstr "本当に選択された文字起こしを削除しますか? この操作は元に戻せません。"
-
-#: buzz/widgets/main_window.py
-msgid "Select audio file"
-msgstr "音声ファイルを選択"
-
-#: buzz/widgets/main_window.py
-msgid "Select folder"
-msgstr "入力フォルダを選択"
-
-#: buzz/widgets/main_window.py
-msgid "Unable to save OpenAI API key to keyring"
-msgstr "OpenAI API キーをkeyringに保存できません"
-
-#: buzz/transcriber/local_whisper_cpp_server_transcriber.py
-#: buzz/transcriber/recording_transcriber.py
-msgid "Whisper server failed to start. Check logs for details."
-msgstr "Whisperサーバーの起動に失敗しました。詳細はログを確認してください。"
-
-#: buzz/transcriber/local_whisper_cpp_server_transcriber.py
-#: buzz/transcriber/recording_transcriber.py
-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サーバーの起動に失敗しました。より小さいモデルで再試行"
-"してください。CPUモードを強制するには環境変数BUZZ_FORCE_CPU=TRUEを使用してく"
-"ださい。"
-
-#: buzz/transcriber/transcriber.py
-msgid "Translate to English"
-msgstr "英語に翻訳"
-
-#: buzz/transcriber/transcriber.py
-msgid "Transcribe"
-msgstr "文字起こし"
-
-#: buzz/transcriber/transcriber.py
-msgid "Chinese"
-msgstr "中国語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Russian"
-msgstr "ロシア語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Korean"
-msgstr "韓国語"
-
-#: buzz/transcriber/transcriber.py
-msgid "French"
-msgstr "フランス語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Portuguese"
-msgstr "ポルトガル語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Turkish"
-msgstr "トルコ語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Arabic"
-msgstr "アラビア語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Swedish"
-msgstr "スウェーデン語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Indonesian"
-msgstr "インドネシア語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hindi"
-msgstr "ヒンディー語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Finnish"
-msgstr "フィンランド語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Vietnamese"
-msgstr "ベトナム語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hebrew"
-msgstr "ヘブライ語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Greek"
-msgstr "ギリシャ語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malay"
-msgstr "マレー語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Czech"
-msgstr "チェコ語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Romanian"
-msgstr "ルーマニア語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hungarian"
-msgstr "ハンガリー語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tamil"
-msgstr "タミル語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Norwegian"
-msgstr "ノルウェー語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Thai"
-msgstr "タイ語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Urdu"
-msgstr "ウルドゥー語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Croatian"
-msgstr "クロアチア語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bulgarian"
-msgstr "ブルガリア語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lithuanian"
-msgstr "リトアニア語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Latin"
-msgstr "ラテン語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Maori"
-msgstr "マオリ語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malayalam"
-msgstr "マラヤーラム語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Welsh"
-msgstr "ウェールズ語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Slovak"
-msgstr "スロバキア語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Telugu"
-msgstr "テルグ語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Persian"
-msgstr "ペルシャ語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bengali"
-msgstr "ベンガル語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Serbian"
-msgstr "セルビア語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Azerbaijani"
-msgstr "アゼルバイジャン語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Slovenian"
-msgstr "スロベニア語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Kannada"
-msgstr "カンナダ語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Estonian"
-msgstr "エストニア語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Macedonian"
-msgstr "マケドニア語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Breton"
-msgstr "ブルトン語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Basque"
-msgstr "バスク語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Icelandic"
-msgstr "アイスランド語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Armenian"
-msgstr "アルメニア語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Nepali"
-msgstr "ネパール語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Mongolian"
-msgstr "モンゴル語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bosnian"
-msgstr "ボスニア語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Kazakh"
-msgstr "カザフ語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Albanian"
-msgstr "アルバニア語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Swahili"
-msgstr "スワヒリ語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Galician"
-msgstr "ガリシア語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Marathi"
-msgstr "マラーティー語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Punjabi"
-msgstr "パンジャブ語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sinhala"
-msgstr "シンハラ語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Khmer"
-msgstr "クメール語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Shona"
-msgstr "ショナ語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Yoruba"
-msgstr "ヨルバ語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Somali"
-msgstr "ソマリ語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Afrikaans"
-msgstr "アフリカーンス語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Occitan"
-msgstr "オック語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Georgian"
-msgstr "ジョージア語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Belarusian"
-msgstr "ベラルーシ語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tajik"
-msgstr "タジク語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sindhi"
-msgstr "シンド語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Gujarati"
-msgstr "グジャラート語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Amharic"
-msgstr "アムハラ語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Yiddish"
-msgstr "イディッシュ語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lao"
-msgstr "ラオス語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Uzbek"
-msgstr "ウズベク語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Faroese"
-msgstr "フェロー語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Haitian Creole"
-msgstr "ハイチ・クレオール語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Pashto"
-msgstr "パシュトー語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Turkmen"
-msgstr "トルクメン語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Nynorsk"
-msgstr "ニーノシュク語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Maltese"
-msgstr "マルタ語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sanskrit"
-msgstr "サンスクリット語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Luxembourgish"
-msgstr "ルクセンブルク語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Myanmar"
-msgstr "ミャンマー語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tibetan"
-msgstr "チベット語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tagalog"
-msgstr "タガログ語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malagasy"
-msgstr "マラガシ語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Assamese"
-msgstr "アッサム語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tatar"
-msgstr "タタール語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hawaiian"
-msgstr "ハワイ語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lingala"
-msgstr "リンガラ語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hausa"
-msgstr "ハウサ語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bashkir"
-msgstr "バシキール語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Javanese"
-msgstr "ジャワ語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sundanese"
-msgstr "スンダ語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Cantonese"
-msgstr "広東語"
-
-#: buzz/transcriber/recording_transcriber.py buzz/model_loader.py
-msgid "A connection error occurred"
-msgstr "接続エラーが発生しました"
-
-#: buzz/transcriber/recording_transcriber.py
-msgid "Starting Whisper.cpp..."
-msgstr "Whisper.cppを起動中..."
-
-#: buzz/transcriber/recording_transcriber.py
-msgid "Starting transcription..."
-msgstr "文字起こしを開始中..."
-
-#: buzz/settings/shortcut.py
-msgid "Open Record Window"
-msgstr "録音画面を開く"
-
-#: buzz/settings/shortcut.py
-msgid "Import File"
-msgstr "ファイルをインポートする"
-
-#: buzz/settings/shortcut.py
-msgid "Open Preferences Window"
-msgstr "設定画面を開く"
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Text"
-msgstr "文字起こしを表示する"
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Translation"
-msgstr "文字起こしの翻訳を表示する"
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Timestamps"
-msgstr "文字起こしのタイムスタンプを表示する"
-
-#: buzz/settings/shortcut.py
-msgid "Search Transcript"
-msgstr "文字起こしを検索"
-
-#: buzz/settings/shortcut.py
-msgid "Go to Next Transcript Search Result"
-msgstr "次の文字起こし検索結果へ移動"
-
-#: buzz/settings/shortcut.py
-msgid "Go to Previous Transcript Search Result"
-msgstr "前の文字起こし検索結果へ移動"
-
-#: buzz/settings/shortcut.py
-msgid "Scroll to Current Text"
-msgstr "現在のテキストにスクロール"
-
-#: buzz/settings/shortcut.py
-msgid "Play/Pause Audio"
-msgstr "音声の再生・一時停止"
-
-#: buzz/settings/shortcut.py
-msgid "Replay Current Segment"
-msgstr "現在のセグメントを再再生"
-
-#: buzz/settings/shortcut.py
-msgid "Toggle Playback Controls"
-msgstr "再生コントロールの表示切替"
-
-#: buzz/settings/shortcut.py
-msgid "Decrease Segment Start Time"
-msgstr "セグメント開始時刻を早める"
-
-#: buzz/settings/shortcut.py
-msgid "Increase Segment Start Time"
-msgstr "セグメント開始時刻を遅らせる"
-
-#: buzz/settings/shortcut.py
-msgid "Decrease Segment End Time"
-msgstr "セグメント終了時刻を早める"
-
-#: buzz/settings/shortcut.py
-msgid "Increase Segment End Time"
-msgstr "セグメント終了時刻を遅らせる"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append below"
-msgstr "下に追加"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append above"
-msgstr "上に追加"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append and correct"
-msgstr "追加して修正"
-
-#: buzz/file_transcriber_queue_worker.py
-msgid ""
-"Speech extraction failed! Check your internet connection — a model may need "
-"to be downloaded."
-msgstr ""
-"音声抽出に失敗しました!インターネット接続を確認してください。モデルのダウン"
-"ロードが必要な場合があります。"
-
-#~ msgid "Comma-separated, e.g. \"0.0, 0.2, 0.4, 0.6, 0.8, 1.0\""
-#~ msgstr "コンマ区切り、例: \"0.0, 0.2, 0.4, 0.6, 0.8, 1.0\""
-
-#~ msgid "Temperature:"
-#~ msgstr "サンプリング温度:"
-
-#~ msgid "Please translate each text sent to you from English to Spanish."
-#~ msgstr "送られてくる各テキストを英語からスペイン語に翻訳してください。"
-
-#~ msgid "Translation error, see logs!"
-#~ msgstr "翻訳エラーが発生しました。ログを確認してください!"
-
-#~ msgid "Snap permission notice"
-#~ msgstr "Snap権限通知"
-
-#~ msgid ""
-#~ "Detected missing permissions, please check that snap permissions have "
-#~ "been granted"
-#~ msgstr ""
-#~ "不足している権限が検出されました。Snapパッケージに権限が付与されていること"
-#~ "を確認してください"
-
-#~ msgid ""
-#~ "To enable necessary permissions run the following commands in the terminal"
-#~ msgstr ""
-#~ "必要なパーミッションを有効にするには、ターミナルで以下のコマンドを実行して"
-#~ "ください"
-
-#~ msgid "Close"
-#~ msgstr "閉じる"
-
-#~ msgid "Enter instructions for AI on how to translate..."
-#~ msgstr "AIのための翻訳方法の指示を入力..."
-
-#~ msgid "Enter target characters per subtitle:"
-#~ msgstr "字幕の目標文字数を入力してください:"
diff --git a/buzz/locale/lv_LV/LC_MESSAGES/buzz.po b/buzz/locale/lv_LV/LC_MESSAGES/buzz.po
deleted file mode 100644
index 66860657..00000000
--- a/buzz/locale/lv_LV/LC_MESSAGES/buzz.po
+++ /dev/null
@@ -1,1672 +0,0 @@
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
-# This file is distributed under the same license as the PACKAGE package.
-# FIRST AUTHOR , YEAR.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: \n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2026-03-07 20:20+0200\n"
-"PO-Revision-Date: 2026-03-06 13:23+0200\n"
-"Last-Translator: \n"
-"Language-Team: \n"
-"Language: lv_LV\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Generator: Poedit 3.4.2\n"
-
-#: buzz/widgets/import_url_dialog.py buzz/settings/shortcut.py
-msgid "Import URL"
-msgstr "Importēt URL"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "https://example.com/audio.mp3"
-msgstr "https://example.com/audio.mp3"
-
-#: buzz/widgets/import_url_dialog.py
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-#: buzz/widgets/main_window.py
-msgid "Ok"
-msgstr "Labi"
-
-#: buzz/widgets/import_url_dialog.py
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-#: buzz/widgets/model_download_progress_dialog.py buzz/widgets/main_window.py
-msgid "Cancel"
-msgstr "Atcelt"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "URL:"
-msgstr "URL:"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "Invalid URL"
-msgstr "Adrese nav derīga"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "The URL you entered is invalid."
-msgstr "Jūsu ievadītā URL adrese nav derīga."
-
-#: buzz/widgets/presentation_window.py
-msgid "Live Transcript Presentation"
-msgstr "Dzīvais ieraksts"
-
-#: buzz/widgets/preferences_dialog/shortcuts_editor_preferences_widget.py
-msgid "Reset to Defaults"
-msgstr "Atjaunot noklusētos"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "English"
-msgstr "Angļu"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Catalan"
-msgstr "Katalāņu"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Danish"
-msgstr "Dāņu"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Dutch"
-msgstr "Holandiešu"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "German"
-msgstr "Vācu"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Spanish"
-msgstr "Spāņu"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Italian"
-msgstr "Itāļu"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Japanese"
-msgstr "Japāņu"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Latvian"
-msgstr "Latviešu"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Polish"
-msgstr "Poļu"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Portuguese (Brazil)"
-msgstr "Portugāļu (Brazīlijas)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Ukrainian"
-msgstr "Ukraiņu"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Chinese (Simplified)"
-msgstr "Ķīniešu (vienkāršotā)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Chinese (Traditional)"
-msgstr "Ķīniešu (tradicionālā)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Restart required!"
-msgstr "Jāpārstartē!"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Ui Language"
-msgstr "Programmas valoda"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Font Size"
-msgstr "Fonta izmērs"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Test"
-msgstr "Pārbaudīt"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API key"
-msgstr "OpenAI API atslēga"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI base url"
-msgstr "OpenAI adrese"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API model"
-msgstr "OpenAI modelis"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Default export file name"
-msgstr "Eksporta fails"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Enable live recording transcription export"
-msgstr "Eksportēt dzīvā ieraksta transkriptus"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Browse"
-msgstr "Izvēlēties"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Export folder"
-msgstr "Eksportēt mapē"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Live recording mode"
-msgstr "Dzīvā ieraksta režīms"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Note: Live recording export settings will be moved to the Advanced Settings "
-"in the Live Recording screen in a future version."
-msgstr ""
-"Piezīme: Dzīvā ieraksta iestatījumi nākotnes Buzz versijās tiks pārvietoti "
-"uz Papildu iestatījumu sadaļu Dzīvā ieraksta logā."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Use 8-bit quantization to reduce memory usage"
-msgstr "Izmantot 8bitu kvantizāciju, lai samazinātu nepieciešamo atmiņu"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Applies to Huggingface and Faster Whisper models. Reduces GPU memory usage "
-"but may slightly decrease transcription quality."
-msgstr ""
-"Izmantojams Huggingface un Faster whisper modeļiem, lai samazinātu "
-"nepieciešamo atmiņas daudzumu, nedaudz zaudējot atpazīšanas kvalitāti."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Reduce GPU RAM"
-msgstr "Optimizēt GPU atmiņu"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Use only CPU and disable GPU acceleration"
-msgstr "Izmantot tikai CPU un deaktivēt GPU paātrināšanu"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Set this if larger models do not fit your GPU memory and Buzz crashes"
-msgstr ""
-"Aktivizējiet šo, ja lielāki modeļi neietilpst jūsu video kartes atmiņā un "
-"Buzz avarē"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Disable GPU"
-msgstr "Deaktivēt GPU"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API Key Test"
-msgstr "OpenAI API atslēgas pārbaude"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Your API key is valid. Buzz will use this key to perform Whisper API "
-"transcriptions and AI translations."
-msgstr ""
-"Jūsu API atslēga ir derīga. Buzz izmantos to runas atpazīšanai ar Whisper "
-"API un tulkošanai."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Invalid API key"
-msgstr "Nederīga API atslēga"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"API supports only base64 characters (A-Za-z0-9+/=_-). Other characters in "
-"API key may cause errors."
-msgstr ""
-"API atbalsta tikai base64 simbolus (A-Za-z0-9+/=_-). Citi simboli API "
-"atslēgā var radīt kļūdas."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Select Export Folder"
-msgstr "Izvēlieties mapi kurā eksportēt"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"OpenAI API returned invalid response. Please check the API url or your key. "
-"Transcription and translation may still work if the API does not support key "
-"validation."
-msgstr ""
-"OpenAI API atbilde ir nederīga. Lūdzu pārbaudiet API Adresi un savu atslēgu. "
-"Atpazīšana un tulkošana joprojām var strādāt, ja API neatbalsta atslēgu "
-"pārbaudi."
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Enable folder watch"
-msgstr "Ieslēgt mapes vērošanu"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Delete processed files"
-msgstr "Dzēst apstrādātos failus"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Input folder"
-msgstr "Vērojamā mape"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Output folder"
-msgstr "Rezultātu mape"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Select Input Folder"
-msgstr "Izvēlieties vērojamo mapi"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Select Output Folder"
-msgstr "Izvēlieties rezultātu mapi"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Preferences"
-msgstr "Iestatījumi"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "General"
-msgstr "Vispārīgi"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Models"
-msgstr "Modeļi"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Shortcuts"
-msgstr "Īsinājumi"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Folder Watch"
-msgstr "Mapes vērošana"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Group"
-msgstr "Veids"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Huggingface ID of a Faster whisper model"
-msgstr "Faster whisper modeļa Huggingface ID"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download"
-msgstr "Lejupielādēt"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Show file location"
-msgstr "Rādīt faila atrašanās vietu"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Delete"
-msgstr "Dzēst"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Downloaded"
-msgstr "Lejupielādēts"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Available for Download"
-msgstr "Pieejams lejupielādei"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download link to Whisper.cpp ggml model file"
-msgstr "Whisper.cpp ggml modeļa datnes lejupielādes saite"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Delete Model"
-msgstr "Dzēst modeli"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Are you sure you want to delete the selected model?"
-msgstr "Vai tiešām dzēst izvēlēto modeli?"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download failed"
-msgstr "Lejupielāde neizdevās"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcription_tasks_table_widget.py
-#: buzz/widgets/update_dialog.py buzz/widgets/main_window.py
-#: buzz/model_loader.py
-msgid "Error"
-msgstr "Kļūda"
-
-#: buzz/widgets/record_button.py buzz/widgets/main_window_toolbar.py
-msgid "Record"
-msgstr "Ierakstīt"
-
-#: buzz/widgets/record_button.py
-msgid "Stop"
-msgstr "Apturēt"
-
-#: buzz/widgets/transcriber/languages_combo_box.py
-#: buzz/transcriber/transcriber.py
-msgid "Detect Language"
-msgstr "Noteikt valodu"
-
-#: buzz/widgets/transcriber/mms_language_line_edit.py
-msgid "e.g., eng, fra, deu"
-msgstr "piem. eng, fra, deu"
-
-#: buzz/widgets/transcriber/mms_language_line_edit.py
-msgid ""
-"Enter an ISO 639-3 language code (3 letters).\n"
-"Examples: eng (English), fra (French), deu (German),\n"
-"spa (Spanish), lav (Latvian)"
-msgstr ""
-"Ievadiet valodas ISO 639-3 kodu (3 burti).\n"
-"Piemēram: eng (Angļu), fra (Franču), deu (Vācu),\n"
-"spa (Spāņu), lav (Latviešu)"
-
-#: buzz/widgets/transcriber/file_transcriber_widget.py
-msgid "Run"
-msgstr "Apstrādāt"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Model:"
-msgstr "Modelis:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-#: buzz/transcriber/recording_transcriber.py
-msgid "First time use of a model may take up to several minutest to load."
-msgstr "Pirmā modeļa ielādes reize var aizņemt pat vairākas minūtes."
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Api Key:"
-msgstr "API atslēga:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Task:"
-msgstr "Uzdevums:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Language:"
-msgstr "Valoda:"
-
-#: buzz/widgets/transcriber/initial_prompt_text_edit.py
-msgid "Enter prompt..."
-msgstr "Ievadiet vaicājumu..."
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Advanced Settings"
-msgstr "Papildu iestatījumi"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Speech recognition settings"
-msgstr "Runas atpazīšanas iestatījumi"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Initial Prompt:"
-msgstr ""
-"Sākotnējais\n"
-"vaicājums:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Translation settings"
-msgstr "Tulkojuma iestatījumi"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Enable AI translation"
-msgstr "Tulkot ar MI"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "AI model:"
-msgstr "AI modelis:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid ""
-"Please translate each text sent to you from English to Spanish. Translation "
-"will be used in an automated system, please do not add any comments or "
-"notes, just the translation."
-msgstr ""
-"Lūdzu, iztulko katru nosūtīto tekstu no angļu valodas spāņu valodā. "
-"Tulkojums tiks izmantots automatizētā sistēmā, lūdzu, nepievieno nekādus "
-"komentārus vai piezīmes, tikai tulkojumu."
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Instructions for AI:"
-msgstr "Norādes MI:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Recording settings"
-msgstr "Dzīvā ieraksta iestatījumi"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Silence threshold:"
-msgstr "Klusuma slieksnis:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Live recording mode:"
-msgstr "Dzīvā ieraksta režīms:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Line separator:"
-msgstr "Rindiņu atdalītājs:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Transcription step:"
-msgstr "Atpazīšanas solis:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Hide unconfirmed"
-msgstr "Slēpt mainīgos fragmentus"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Enable live recording export"
-msgstr "Eksportēt dzīvā ieraksta transkriptus"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export folder:"
-msgstr "Eksportēt mapē:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export file name:"
-msgstr "Eksporta fails:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Text file (.txt)"
-msgstr "Teksta fails (.txt)"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "CSV (.csv)"
-msgstr "CSV (.csv)"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export file type:"
-msgstr "Eksporta faila tips:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid ""
-"Limit export entries\n"
-"(0 = export all):"
-msgstr ""
-"Eksportēt tikai pēdējos\n"
-"(0 = eksportēt visus):"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Word-level timings"
-msgstr "Dalīt pa vārdiem"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Extract speech"
-msgstr "Atdalīt runu"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Export:"
-msgstr "Eksportēt:"
-
-#: buzz/widgets/transcriber/hugging_face_search_line_edit.py
-msgid "Huggingface ID of a model"
-msgstr "Modeļa Huggingface ID"
-
-#: buzz/widgets/transcriber/advanced_settings_button.py
-msgid "Advanced..."
-msgstr "Papildu iestatījumi..."
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "New File Transcription"
-msgstr "Jauna faila atpazīšana"
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "New URL Transcription"
-msgstr "Jauna saites atpazīšana"
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "Open Transcript"
-msgstr "Atvērt transkriptu"
-
-#: buzz/widgets/main_window_toolbar.py buzz/settings/shortcut.py
-msgid "Cancel Transcription"
-msgstr "Atcelt atpazīšanu"
-
-#: buzz/widgets/main_window_toolbar.py buzz/widgets/main_window.py
-#: buzz/settings/shortcut.py
-msgid "Clear History"
-msgstr "Notīrīt vēsturi"
-
-#: buzz/widgets/main_window_toolbar.py buzz/widgets/update_dialog.py
-msgid "Update Available"
-msgstr "Pieejams atjauninājums"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "In Progress"
-msgstr "Apstrādā"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Completed"
-msgstr "Pabeigts"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Failed"
-msgstr "Neizdevās"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Canceled"
-msgstr "Atcelts"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Queued"
-msgstr "Ierindots"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "File Name / URL"
-msgstr "Fails / URL"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Model"
-msgstr "Modelis"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Task"
-msgstr "Uzdevums"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Status"
-msgstr "Statuss"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Date Completed"
-msgstr "Pabeigts"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Date Added"
-msgstr "Pievienots"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Notes"
-msgstr "Piezīmes"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Reset Column Order"
-msgstr "Atjaunot kolonas"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Restart Transcription"
-msgstr "Sāk atpazīšanu"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Rename"
-msgstr "Pārddēvēt"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Add/Edit Notes"
-msgstr "Rediģēt piezīmes"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Rename Transcription"
-msgstr "Pārdēvēt"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Enter new name:"
-msgstr "Ievadiet jauno nosaukumu šim atpazīšanas ierakstam:"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Enter some relevant notes for this transcription:"
-msgstr "Ievadiet noderīgas piezīmēs par šo ierakstu:"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Cannot Restart"
-msgstr "Neizdodas sākt"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Only failed or canceled transcriptions can be restarted."
-msgstr "Atkārtoti sākt var tikai kļūdainus vai atceltus ierakstus."
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Failed to restart transcription: {}"
-msgstr "Neizdevās sākt atpazīšanu: {}"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid ""
-"Could not restart transcription: model not available and could not be "
-"downloaded."
-msgstr ""
-"Neizdevās sākt atpazīšanu: modelis nav pieejams un to nevar lejupielādēt."
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Could not restart transcription: transcriber worker not found."
-msgstr "Neizdevās sākt atpazīšanu: Kļūda lietotnē, pārstartējiet."
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Live Recording"
-msgstr "Dzīvā ierakstīšana"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Click Record to begin..."
-msgstr "Klikšķiniet Ierakstīt, lai sāktu..."
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Waiting for AI translation..."
-msgstr "Gaida MI tulkojumu..."
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Microphone:"
-msgstr "Mikrofons:"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Show in new window"
-msgstr "Rādīt jaunā logā"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Text Size:"
-msgstr "Teksta izmērs:"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Theme"
-msgstr "Stils"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Light"
-msgstr "Gaišais"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Dark"
-msgstr "Tumšais"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Custom"
-msgstr "Pielāgots"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Text Color"
-msgstr "Teksta krāsa"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Background Color"
-msgstr "Fona krāsa"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Fullscreen"
-msgstr "Pilnekrāns"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy"
-msgstr "Kopēt"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy transcription to clipboard"
-msgstr "Kopēt tekstu starpliktuvē"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Nothing to copy!"
-msgstr "Nav nekā ko kopēt!"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy failed"
-msgstr "Kopešana neizdevās"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copied!"
-msgstr "Nokopēts!"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Select Text Color"
-msgstr "Izvēlieties teksta krāsu"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Select Background Color"
-msgstr "Izvēlieties fona krāsu"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "An error occurred while starting a new recording:"
-msgstr "Sākot jaunu ierakstu notikusi kļūda:"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid ""
-"Please check your audio devices or check the application logs for more "
-"information."
-msgstr ""
-"Lūdzu pārbaudiet savas audio ierīces vai pārbaudiet lietotnes ziņojumu "
-"žurnālus, lai iegūtu papildu informāciju."
-
-#: buzz/widgets/update_dialog.py
-msgid "A new version of Buzz is available!"
-msgstr "Pieejama jauna Buzz versija!"
-
-#: buzz/widgets/update_dialog.py
-msgid "Current version:"
-msgstr "Instalētā versija:"
-
-#: buzz/widgets/update_dialog.py
-msgid "New version:"
-msgstr "Jaunā versija:"
-
-#: buzz/widgets/update_dialog.py
-msgid "Release Notes:"
-msgstr "Jaunās versijas piezīmes:"
-
-#: buzz/widgets/update_dialog.py
-msgid "Download and Install"
-msgstr "Lejupielādēt un instalēt"
-
-#: buzz/widgets/update_dialog.py
-msgid "No download URL available for your platform."
-msgstr "Jūsu datoram nav pieejama atjauninājum versija."
-
-#: buzz/widgets/update_dialog.py
-msgid "Downloading file {} of {}..."
-msgstr "Lejupielādē {} no {}..."
-
-#: buzz/widgets/update_dialog.py
-msgid "Downloading file {} of {} ({:.1f} MB / {:.1f} MB)..."
-msgstr "Lejupielādē {} no {} ({:.1f} MB / {:.1f} MB)..."
-
-#: buzz/widgets/update_dialog.py
-msgid "Download Failed"
-msgstr "Lejupielāde neizdevās"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to download the update: {}"
-msgstr "Neizdevās lejupielādēt atjauninājumu: {}"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to save the installer: {}"
-msgstr "Neizdevās saglabāt atjauninājumu: {}"
-
-#: buzz/widgets/update_dialog.py
-msgid "Download complete!"
-msgstr "Lejupielāde pabeigta!"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to run the installer: {}"
-msgstr "Neizdevās sākt atjauninājumu: {}"
-
-#: buzz/widgets/about_dialog.py
-msgid "Check for updates"
-msgstr "Pārbaudīt atjauninājumus"
-
-#: buzz/widgets/about_dialog.py
-msgid "Show logs"
-msgstr "Parādīt sistēmas žurnālu"
-
-#: buzz/widgets/about_dialog.py
-msgid "You're up to date!"
-msgstr "Jums ir jaunākā versija!"
-
-#: buzz/widgets/audio_meter_widget.py
-msgid "Average volume"
-msgstr "Vidējais skaļums"
-
-#: buzz/widgets/audio_meter_widget.py
-msgid "Queue"
-msgstr "Rinda"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-msgid "Start"
-msgstr "Sākums"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-msgid "End"
-msgstr "Beigas"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Text"
-msgstr "Teksts"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Translation"
-msgstr "Tulkojums"
-
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-msgid "View"
-msgstr "Skats"
-
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-msgid "Timestamps"
-msgstr "Laiks"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Export"
-msgstr "Eksportēt"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Translate"
-msgstr "Tulkot"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Resize"
-msgstr "Mainīt garumu"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Identify Speakers"
-msgstr "Noteikt runātājus"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Find"
-msgstr "Meklēt"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Show/Hide Search Bar (Ctrl+F)"
-msgstr "Rādīt/Slēpt meklēšanas joslu (Ctrl+F)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Find:"
-msgstr "Meklēt:"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Enter text to find..."
-msgstr "Ievadiet meklējamo..."
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Previous match (Shift+Enter)"
-msgstr "Iepriekšējais rezultāts (Shift+Enter)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Next match (Ctrl+Enter)"
-msgstr "Nākamais rezultāts (Ctrl+Enter)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Clear"
-msgstr "Notīrīt"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Playback Controls:"
-msgstr "Atskaņošana:"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Loop Segment"
-msgstr "Atkārtot segmentu"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Enable/disable looping when clicking on transcript segments"
-msgstr "Nosaka vai atkārtot izvēlēto segmentu"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Follow Audio"
-msgstr "Sekot audio"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid ""
-"Enable/disable following the current audio position in the transcript. When "
-"enabled, automatically scrolls to current text."
-msgstr ""
-"Nosaka vai atskaņojot audio iezīmētajam segmentam vajadzētu automātiski "
-"sekot tam kas tiek atskaņots."
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Scroll to Current"
-msgstr "Pāriet uz tekošo"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Scroll to the currently spoken text"
-msgstr "Pāriet uz šobrīd atskaņojamo tesktu"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "1 of 100+ matches"
-msgstr "1 no 100+ "
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "1 of "
-msgstr "1 no "
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " matches"
-msgstr " "
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "No matches found"
-msgstr "Nekas nav atrasts"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " of 100+ matches"
-msgstr " no 100+"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " of "
-msgstr " no "
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "API Key Required"
-msgstr "API atslēgas kļūda"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Please enter OpenAI API Key in preferences"
-msgstr "Lūdzu ievadiet OpenAI API atslēgu iestatījumos"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend end time"
-msgstr "Pagarināt beigu laiku"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend endings by up to (seconds)"
-msgstr "Palielināt beigu laiku par (sekundes)"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend endings"
-msgstr "Palielināt"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Resize Options"
-msgstr "Garuma maiņas iestatījumi"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Desired subtitle length"
-msgstr "Vēlamais teksta garums"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Available only if word level timings were disabled during transcription"
-msgstr "Pieejami tikai, ierakstiem, kas atpazīti bez dalīšanas pa vārdiem"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge Options"
-msgstr "Apvienošanas iestatījumi"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge by gap"
-msgstr "Apvienot pēc attāluma"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Split by punctuation"
-msgstr "Dalīt pie pieturzīmēm"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Split by max length"
-msgstr "Dalīt pie maksimālā garuma"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge"
-msgstr "Apvienot"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Available only if word level timings were enabled during transcription"
-msgstr "Pieejami tikai, ierakstiem, kas atpazīti ar dalīšanu pa vārdiem"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid ""
-"Speaker identification is not available: failed to load required libraries."
-msgstr ""
-"Runātāju noteikšana nav pieejama, neizdevās ielādēt nepieciešamās "
-"bibliotēkas."
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "1/8 Collecting transcripts"
-msgstr "1/8 Apkopo transkripcijas"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "2/8 Loading audio"
-msgstr "2/8 Ielādē audio"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "3/8 Loading alignment model"
-msgstr "3/8 Ielādē identifikācijas modeli"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "3/8 Loading alignment model (retrying with cache...)"
-msgstr "3/8 Ielādē identifikācijas modeli (atkārto...)"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid ""
-"Failed to load alignment model. Please check your internet connection and "
-"try again."
-msgstr ""
-"Neizdevās ielādēt modeli. Lūdzu pārbaidiet savu interneta savienojumu un "
-"mēģiniet vēlreiz."
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "4/8 Processing audio"
-msgstr "4/8 Apstrādā audio"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "5/8 Preparing transcripts"
-msgstr "5/8 Sagatavo transkripcijas"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "6/8 Identifying speakers"
-msgstr "6/8 Nosaka runātājus"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "7/8 Mapping speakers to transcripts"
-msgstr "7/8 Marķē runātāju teikumus"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "8/8 Identification done"
-msgstr "8/8 Runātāju noteikšana pabeigta"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "0/0 Error identifying speakers"
-msgstr "0/0 Kļūda nosakot runātājus"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Step 1: Identify speakers"
-msgstr "1. solis: Runātāju noteikšana"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Identify"
-msgstr "Noteikt"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Ready to identify speakers"
-msgstr "Gatavs noteikt runātājus"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Audio file not found"
-msgstr "Audio datne nav atrasta"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Step 2: Name speakers"
-msgstr "2. solis: Runātāju identifikācija"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Play sample"
-msgstr "Atskaņot paraugu"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Merge speaker sentences"
-msgstr "Apvienot secīgus runātāja teikumus"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Save"
-msgstr "Saglabāt"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Cancelling..."
-msgstr "Atceļ..."
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Cancelled"
-msgstr "Atcelts"
-
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Save File"
-msgstr "Saglabāt failu"
-
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Text files"
-msgstr "Teksta faili"
-
-#: buzz/widgets/model_download_progress_dialog.py
-msgid "Downloading model"
-msgstr "Lejupielādē modeli"
-
-#: buzz/widgets/model_download_progress_dialog.py
-msgid "remaining"
-msgstr "..."
-
-#: buzz/widgets/menu_bar.py
-msgid "Import File..."
-msgstr "Importēt failu..."
-
-#: buzz/widgets/menu_bar.py
-msgid "Import URL..."
-msgstr "Importēt URL..."
-
-#: buzz/widgets/menu_bar.py
-msgid "Import Folder..."
-msgstr "Importēt mapi..."
-
-#: buzz/widgets/menu_bar.py
-msgid "About"
-msgstr "Par"
-
-#: buzz/widgets/menu_bar.py
-msgid "Preferences..."
-msgstr "Iestatījumi..."
-
-#: buzz/widgets/menu_bar.py
-msgid "Help"
-msgstr "Palīdzība"
-
-#: buzz/widgets/menu_bar.py
-msgid "File"
-msgstr "Fails"
-
-#: buzz/widgets/main_window.py
-msgid ""
-"Are you sure you want to delete the selected transcription(s)? This action "
-"cannot be undone."
-msgstr ""
-"Vai tiešām vēlaties dzēst izvēlētos transkriptus? Šī ir neatgriezeniska "
-"darbība."
-
-#: buzz/widgets/main_window.py
-msgid "Select audio file"
-msgstr "Izvēlieties audio failu"
-
-#: buzz/widgets/main_window.py
-msgid "Select folder"
-msgstr "Izvēlieties mapi"
-
-#: buzz/widgets/main_window.py
-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
-#: buzz/transcriber/recording_transcriber.py
-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
-#: buzz/transcriber/recording_transcriber.py
-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
-msgid "Translate to English"
-msgstr "Tulkot angliski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Transcribe"
-msgstr "Atpazīt"
-
-#: buzz/transcriber/transcriber.py
-msgid "Chinese"
-msgstr "Ķīniešu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Russian"
-msgstr "Krievu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Korean"
-msgstr "Korejiešu"
-
-#: buzz/transcriber/transcriber.py
-msgid "French"
-msgstr "Franču"
-
-#: buzz/transcriber/transcriber.py
-msgid "Portuguese"
-msgstr "Portugāļu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Turkish"
-msgstr "Turku"
-
-#: buzz/transcriber/transcriber.py
-msgid "Arabic"
-msgstr "Arābu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Swedish"
-msgstr "Zviedru"
-
-#: buzz/transcriber/transcriber.py
-msgid "Indonesian"
-msgstr "Indonēziešu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hindi"
-msgstr "Hindi"
-
-#: buzz/transcriber/transcriber.py
-msgid "Finnish"
-msgstr "Somu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Vietnamese"
-msgstr "Vjetnamiešu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hebrew"
-msgstr "Ebreju"
-
-#: buzz/transcriber/transcriber.py
-msgid "Greek"
-msgstr "Grieķu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malay"
-msgstr "Malajiešu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Czech"
-msgstr "Čehu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Romanian"
-msgstr "Rumāņu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hungarian"
-msgstr "Ungāru"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tamil"
-msgstr "Tamilu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Norwegian"
-msgstr "Norvēģu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Thai"
-msgstr "Taju"
-
-#: buzz/transcriber/transcriber.py
-msgid "Urdu"
-msgstr "Urdu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Croatian"
-msgstr "Horvātu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bulgarian"
-msgstr "Bulgāru"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lithuanian"
-msgstr "Lietuviešu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Latin"
-msgstr "Latīņu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Maori"
-msgstr "Maori"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malayalam"
-msgstr "Malajalu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Welsh"
-msgstr "Velsiešu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Slovak"
-msgstr "Slovāku"
-
-#: buzz/transcriber/transcriber.py
-msgid "Telugu"
-msgstr "Telugu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Persian"
-msgstr "Persiešu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bengali"
-msgstr "Bengāļu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Serbian"
-msgstr "Serbu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Azerbaijani"
-msgstr "Azerbaidžāņu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Slovenian"
-msgstr "Slovēņu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Kannada"
-msgstr "Kannada"
-
-#: buzz/transcriber/transcriber.py
-msgid "Estonian"
-msgstr "Igauņu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Macedonian"
-msgstr "Maķedoniešu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Breton"
-msgstr "Bretoņu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Basque"
-msgstr "Basku"
-
-#: buzz/transcriber/transcriber.py
-msgid "Icelandic"
-msgstr "Islandiešu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Armenian"
-msgstr "Armēņu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Nepali"
-msgstr "Nepāliešu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Mongolian"
-msgstr "Mongoļu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bosnian"
-msgstr "Bosniešu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Kazakh"
-msgstr "Kazahu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Albanian"
-msgstr "Albaņu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Swahili"
-msgstr "Svahili"
-
-#: buzz/transcriber/transcriber.py
-msgid "Galician"
-msgstr "Galisiešu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Marathi"
-msgstr "Maratu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Punjabi"
-msgstr "Pandžabu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sinhala"
-msgstr "Singalu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Khmer"
-msgstr "Khmeru"
-
-#: buzz/transcriber/transcriber.py
-msgid "Shona"
-msgstr "Shona"
-
-#: buzz/transcriber/transcriber.py
-msgid "Yoruba"
-msgstr "Joruba"
-
-#: buzz/transcriber/transcriber.py
-msgid "Somali"
-msgstr "Somāliešu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Afrikaans"
-msgstr "Afrikāņu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Occitan"
-msgstr "Okitāņu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Georgian"
-msgstr "Gruzīnu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Belarusian"
-msgstr "Baltkrievu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tajik"
-msgstr "Tadžiku"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sindhi"
-msgstr "Sindhu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Gujarati"
-msgstr "Gudžaratu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Amharic"
-msgstr "Amharu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Yiddish"
-msgstr "Jidiša"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lao"
-msgstr "Laosiešu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Uzbek"
-msgstr "Uzbeku"
-
-#: buzz/transcriber/transcriber.py
-msgid "Faroese"
-msgstr "Fēru"
-
-#: buzz/transcriber/transcriber.py
-msgid "Haitian Creole"
-msgstr "Haiti kreoliešu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Pashto"
-msgstr "Puštu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Turkmen"
-msgstr "Turkmēņu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Nynorsk"
-msgstr "Nynorsk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Maltese"
-msgstr "Maltiešu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sanskrit"
-msgstr "Sanskrita"
-
-#: buzz/transcriber/transcriber.py
-msgid "Luxembourgish"
-msgstr "Luksemburgu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Myanmar"
-msgstr "Mjanmas"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tibetan"
-msgstr "Tibetiešu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tagalog"
-msgstr "Tagalogu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malagasy"
-msgstr "Malagasu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Assamese"
-msgstr "Asamiešu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tatar"
-msgstr "Tatāru"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hawaiian"
-msgstr "Havajiešu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lingala"
-msgstr "Lingalu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hausa"
-msgstr "Hausu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bashkir"
-msgstr "Baškīru"
-
-#: buzz/transcriber/transcriber.py
-msgid "Javanese"
-msgstr "Japāņu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sundanese"
-msgstr "Sundāņu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Cantonese"
-msgstr "Kantonas"
-
-#: buzz/transcriber/recording_transcriber.py buzz/model_loader.py
-msgid "A connection error occurred"
-msgstr "Notika savienojuma kļūda"
-
-#: buzz/transcriber/recording_transcriber.py
-msgid "Starting Whisper.cpp..."
-msgstr "Palaiž Whisper.cpp..."
-
-#: buzz/transcriber/recording_transcriber.py
-msgid "Starting transcription..."
-msgstr "Sāk atpazīšanu..."
-
-#: buzz/settings/shortcut.py
-msgid "Open Record Window"
-msgstr "Atvērt ieraksta logu"
-
-#: buzz/settings/shortcut.py
-msgid "Import File"
-msgstr "Importēt failu"
-
-#: buzz/settings/shortcut.py
-msgid "Open Preferences Window"
-msgstr "Atvērt iestatījumu logu"
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Text"
-msgstr "Aplūkot atpazīto tekstu"
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Translation"
-msgstr "Aplūkot tulkojumu"
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Timestamps"
-msgstr "Aplūkot atpazīšanas laikus"
-
-#: buzz/settings/shortcut.py
-msgid "Search Transcript"
-msgstr "Meklēt tekstā"
-
-#: buzz/settings/shortcut.py
-msgid "Go to Next Transcript Search Result"
-msgstr "Pāriet uz nākamo meklēšanas rezultātu"
-
-#: buzz/settings/shortcut.py
-msgid "Go to Previous Transcript Search Result"
-msgstr "Pāriet uz iepriekšējo meklēšanas rezultātu"
-
-#: buzz/settings/shortcut.py
-msgid "Scroll to Current Text"
-msgstr "Pāriet uz atskaņojamo tesktu"
-
-#: buzz/settings/shortcut.py
-msgid "Play/Pause Audio"
-msgstr "Atskaņot/Apturēt audio"
-
-#: buzz/settings/shortcut.py
-msgid "Replay Current Segment"
-msgstr "Atskaņot segmentu no sākuma"
-
-#: buzz/settings/shortcut.py
-msgid "Toggle Playback Controls"
-msgstr "Pārslēgt atskaņošanas iespējas"
-
-#: buzz/settings/shortcut.py
-msgid "Decrease Segment Start Time"
-msgstr "Samazināt segmenta sākuma laiku"
-
-#: buzz/settings/shortcut.py
-msgid "Increase Segment Start Time"
-msgstr "Palielināt segmenta sākuma laiku"
-
-#: buzz/settings/shortcut.py
-msgid "Decrease Segment End Time"
-msgstr "Samazināt segmenta beigu laiku"
-
-#: buzz/settings/shortcut.py
-msgid "Increase Segment End Time"
-msgstr "Palielināt segmenta beigu laiku"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append below"
-msgstr "Jaunie teikumi apakšā"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append above"
-msgstr "Jaunie teikumi augšā"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append and correct"
-msgstr "Papildināt un labot esošo"
-
-#: buzz/file_transcriber_queue_worker.py
-msgid ""
-"Speech extraction failed! Check your internet connection — a model may need "
-"to be downloaded."
-msgstr ""
-"Runas atdalīšana neizdevās! Pārbaudiet interneta savienojumu, iespējams "
-"jālejupielādē modelis."
-
-#~ msgid "Comma-separated, e.g. \"0.0, 0.2, 0.4, 0.6, 0.8, 1.0\""
-#~ msgstr "Atdalīti ar komatu, piemēram, \"0.0, 0.2, 0.4, 0.6, 0.8, 1.0\""
-
-#~ msgid "Temperature:"
-#~ msgstr "Temperatūra:"
-
-#~ msgid "Please translate each text sent to you from English to Spanish."
-#~ msgstr "Lūdzu, iztulko katru tev atsūtīto tekstu no angļu valodas latviski."
-
-#~ msgid "Translation error, see logs!"
-#~ msgstr "Kļūda tulkojot, skatiet sistēmas žurnālu!"
-
-#~ msgid "Snap permission notice"
-#~ msgstr "Snap atļauju piezīme"
-
-#~ msgid ""
-#~ "Detected missing permissions, please check that snap permissions have "
-#~ "been granted"
-#~ msgstr ""
-#~ "Ne visi nepieciešamie moduļi darbojas korekti, iespējams nav piešķirtas "
-#~ "snap atļaujas"
-
-#~ msgid ""
-#~ "To enable necessary permissions run the following commands in the terminal"
-#~ msgstr "Lai piešķirtu nepieciešamās atļaujas izpildiet šīs komandas"
-
-#~ msgid "Close"
-#~ msgstr "Aizvērt"
-
-#~ msgid "Enter instructions for AI on how to translate..."
-#~ msgstr "Ievadiet tulkošanas norādes mākslīgajam intelektam..."
-
-#~ msgid "Enter target characters per subtitle:"
-#~ msgstr "Ievadiet vēlamo simbolu skaitu tekstā:"
diff --git a/buzz/locale/nl/LC_MESSAGES/buzz.po b/buzz/locale/nl/LC_MESSAGES/buzz.po
deleted file mode 100644
index 4d09b2c1..00000000
--- a/buzz/locale/nl/LC_MESSAGES/buzz.po
+++ /dev/null
@@ -1,1681 +0,0 @@
-# Dutch translations for PACKAGE package
-# Nederlandse vertalingen voor het pakket PACKAGE.
-# Copyright (C) 2025 THE PACKAGE'S COPYRIGHT HOLDER
-# This file is distributed under the same license as the PACKAGE package.
-# Automatically generated, 2025.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: \n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2026-03-07 20:20+0200\n"
-"PO-Revision-Date: 2025-03-20 18:30+0100\n"
-"Last-Translator: Heimen Stoffels \n"
-"Language-Team: none\n"
-"Language: nl\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-"X-Generator: Poedit 3.5\n"
-
-#: buzz/widgets/import_url_dialog.py buzz/settings/shortcut.py
-msgid "Import URL"
-msgstr "Url importeren"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "https://example.com/audio.mp3"
-msgstr "https://voorbeeld.nl/audio.mp3"
-
-#: buzz/widgets/import_url_dialog.py
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-#: buzz/widgets/main_window.py
-msgid "Ok"
-msgstr "Oké"
-
-#: buzz/widgets/import_url_dialog.py
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-#: buzz/widgets/model_download_progress_dialog.py buzz/widgets/main_window.py
-msgid "Cancel"
-msgstr "Annuleren"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "URL:"
-msgstr "Url:"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "Invalid URL"
-msgstr "Ongeldige url"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "The URL you entered is invalid."
-msgstr "De ingevoerde url is ongeldig."
-
-#: buzz/widgets/presentation_window.py
-msgid "Live Transcript Presentation"
-msgstr "Live transcriptie presenteren"
-
-#: buzz/widgets/preferences_dialog/shortcuts_editor_preferences_widget.py
-msgid "Reset to Defaults"
-msgstr "Standaardwaarden"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "English"
-msgstr "Engels"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Catalan"
-msgstr "Catalaans"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Danish"
-msgstr "Deens"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Dutch"
-msgstr "Nederlands"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "German"
-msgstr "Duits"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Spanish"
-msgstr "Spaans"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Italian"
-msgstr "Italiaans"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Japanese"
-msgstr "Japans"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Latvian"
-msgstr "Lets"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Polish"
-msgstr "Pools"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Portuguese (Brazil)"
-msgstr "Portugees (Brazilië)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Ukrainian"
-msgstr "Oekraïens"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Chinese (Simplified)"
-msgstr "Chinees (Vereenvoudigd)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Chinese (Traditional)"
-msgstr "Chinees (Traditioneel)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Restart required!"
-msgstr "Herstart vereist!"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Ui Language"
-msgstr "Programmataal"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Font Size"
-msgstr "Tekstgrootte"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Test"
-msgstr "Uitproberen"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API key"
-msgstr "OpenAI-api-sleutel"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI base url"
-msgstr "OpenAI-hoofd-url"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API model"
-msgstr "OpenAI-api-model"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Default export file name"
-msgstr "Standaardnaam van export"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Enable live recording transcription export"
-msgstr "Transcripties van opnames onmiddelijk exporteren"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Browse"
-msgstr "Bladeren"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Export folder"
-msgstr "Exportmap"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Live recording mode"
-msgstr "Live-opnamemodus"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Note: Live recording export settings will be moved to the Advanced Settings "
-"in the Live Recording screen in a future version."
-msgstr ""
-"Opmerking: de exportinstellingen voor live-opnames worden in een toekomstige "
-"versie verplaatst naar de geavanceerde instellingen in het scherm Live-"
-"opname."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Use 8-bit quantization to reduce memory usage"
-msgstr "8-bits kwantisering gebruiken om geheugengebruik te verminderen"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Applies to Huggingface and Faster Whisper models. Reduces GPU memory usage "
-"but may slightly decrease transcription quality."
-msgstr ""
-"Van toepassing op Huggingface- en Faster Whisper-modellen. Vermindert het "
-"GPU-geheugengebruik, maar kan de transcriptiekwaliteit licht verminderen."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Reduce GPU RAM"
-msgstr "GPU-geheugen verminderen"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Use only CPU and disable GPU acceleration"
-msgstr "Alleen CPU gebruiken en GPU-versnelling uitschakelen"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Set this if larger models do not fit your GPU memory and Buzz crashes"
-msgstr ""
-"Gebruik dit als grotere modellen niet in het GPU-geheugen passen en Buzz "
-"crasht"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Disable GPU"
-msgstr "GPU uitschakelen"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API Key Test"
-msgstr "OpenAI-api-sleuteltest"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Your API key is valid. Buzz will use this key to perform Whisper API "
-"transcriptions and AI translations."
-msgstr ""
-"De api-sleutel is geldig. Buzz zal deze sleutel gebruiken om transcripties "
-"en AI-vertalingen op te vragen bij Whisper."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Invalid API key"
-msgstr "Ongeldige api-sleutel"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"API supports only base64 characters (A-Za-z0-9+/=_-). Other characters in "
-"API key may cause errors."
-msgstr ""
-"De api ondersteunt alleen base64-tekens (A–Za–z0–9+/=_-). Andere tekens "
-"kunnen problemen veroorzaken."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Select Export Folder"
-msgstr "Kies een exportmap"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"OpenAI API returned invalid response. Please check the API url or your key. "
-"Transcription and translation may still work if the API does not support key "
-"validation."
-msgstr ""
-"De api gaf een ongeldig antwoord terug. Controleer de url of sleutel. "
-"Transcriptie en vertaling werkt mogelijk nog steeds als de api niet om "
-"sleutelverificatie vraagt."
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Enable folder watch"
-msgstr "Map bijhouden"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Delete processed files"
-msgstr "Verwerkte bestanden verwijderen"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Input folder"
-msgstr "Invoermap"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Output folder"
-msgstr "Uitvoermap"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Select Input Folder"
-msgstr "Kies een invoermap"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Select Output Folder"
-msgstr "Kies een uitvoermap"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Preferences"
-msgstr "Instellingen"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "General"
-msgstr "Algemeen"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Models"
-msgstr "Modellen"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Shortcuts"
-msgstr "Sneltoetsen"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Folder Watch"
-msgstr "Map bijhouden"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Group"
-msgstr "Groep"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Huggingface ID of a Faster whisper model"
-msgstr "Huggingface-id of een sneller Whisper-model"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download"
-msgstr "Downloaden"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Show file location"
-msgstr "Bestandslocatie tonen"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Delete"
-msgstr "Verwijderen"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Downloaded"
-msgstr "Gedownload"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Available for Download"
-msgstr "Beschikbaar"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download link to Whisper.cpp ggml model file"
-msgstr "Downloadlink van Whisper.cpp ggml-modelbestand"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Delete Model"
-msgstr "Model verwijderen"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Are you sure you want to delete the selected model?"
-msgstr "Weet u zeker dat u het gekozen model wilt verwijderen?"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download failed"
-msgstr "Het downloaden is mislukt"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcription_tasks_table_widget.py
-#: buzz/widgets/update_dialog.py buzz/widgets/main_window.py
-#: buzz/model_loader.py
-msgid "Error"
-msgstr "Foutmelding"
-
-#: buzz/widgets/record_button.py buzz/widgets/main_window_toolbar.py
-msgid "Record"
-msgstr "Opnemen"
-
-#: buzz/widgets/record_button.py
-msgid "Stop"
-msgstr "Stoppen"
-
-#: buzz/widgets/transcriber/languages_combo_box.py
-#: buzz/transcriber/transcriber.py
-msgid "Detect Language"
-msgstr "Taal herkennen"
-
-#: buzz/widgets/transcriber/mms_language_line_edit.py
-msgid "e.g., eng, fra, deu"
-msgstr "bijv. nld, fra, deu"
-
-#: buzz/widgets/transcriber/mms_language_line_edit.py
-msgid ""
-"Enter an ISO 639-3 language code (3 letters).\n"
-"Examples: eng (English), fra (French), deu (German),\n"
-"spa (Spanish), lav (Latvian)"
-msgstr ""
-"Voer een ISO 639-3-taalcode in (3 letters).\n"
-"Voorbeelden: nld (Nederlands), fra (Frans), deu (Duits),\n"
-"spa (Spaans), lav (Lets)"
-
-#: buzz/widgets/transcriber/file_transcriber_widget.py
-msgid "Run"
-msgstr "Uitvoeren"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Model:"
-msgstr "Model:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-#: buzz/transcriber/recording_transcriber.py
-msgid "First time use of a model may take up to several minutest to load."
-msgstr ""
-"Let op: de eerste keer kan het enkele minuten duren voordat het model "
-"geladen is."
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Api Key:"
-msgstr "Api-sleutel:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Task:"
-msgstr "Taak:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Language:"
-msgstr "Taal:"
-
-#: buzz/widgets/transcriber/initial_prompt_text_edit.py
-msgid "Enter prompt..."
-msgstr "Voer een tekst in…"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Advanced Settings"
-msgstr "Geavanceerde instellingen"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Speech recognition settings"
-msgstr "Spraakherkenningsinstellingen"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Initial Prompt:"
-msgstr "Hoofdinvoer:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Translation settings"
-msgstr "Vertaalinstellingen"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Enable AI translation"
-msgstr "AI-vertaling inschakelen"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "AI model:"
-msgstr "AI-model:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid ""
-"Please translate each text sent to you from English to Spanish. Translation "
-"will be used in an automated system, please do not add any comments or "
-"notes, just the translation."
-msgstr ""
-"Vertaal elke tekst die naar u wordt gestuurd van het Engels naar het Spaans. "
-"De vertaling wordt gebruikt in een geautomatiseerd systeem. Voeg geen "
-"opmerkingen of notities toe, alleen de vertaling."
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Instructions for AI:"
-msgstr "AI-instructies:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Recording settings"
-msgstr "Opname-instellingen"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Silence threshold:"
-msgstr "Stiltedrempel:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Live recording mode:"
-msgstr "Live-opnamemodus:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Line separator:"
-msgstr "Regelscheidingsteken:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Transcription step:"
-msgstr "Transcriptiestap:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Hide unconfirmed"
-msgstr "Verberg onbevestigd"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Enable live recording export"
-msgstr "Live-opname exporteren inschakelen"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export folder:"
-msgstr "Exportmap:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export file name:"
-msgstr "Exportbestandsnaam:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Text file (.txt)"
-msgstr "Tekstbestand (.txt)"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "CSV (.csv)"
-msgstr "CSV (.csv)"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export file type:"
-msgstr "Exportbestandstype:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid ""
-"Limit export entries\n"
-"(0 = export all):"
-msgstr ""
-"Exportitems beperken\n"
-"(0 = alles exporteren):"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Word-level timings"
-msgstr "Woordherkenningstimings"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Extract speech"
-msgstr "Spraak extraheren"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Export:"
-msgstr "Exporteren:"
-
-#: buzz/widgets/transcriber/hugging_face_search_line_edit.py
-msgid "Huggingface ID of a model"
-msgstr "Huggingface-id van een model"
-
-#: buzz/widgets/transcriber/advanced_settings_button.py
-msgid "Advanced..."
-msgstr "Geavanceerd…"
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "New File Transcription"
-msgstr "Nieuwe bestandstranscriptie"
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "New URL Transcription"
-msgstr "Nieuwe url-transcriptie"
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "Open Transcript"
-msgstr "Transcriptie openen"
-
-#: buzz/widgets/main_window_toolbar.py buzz/settings/shortcut.py
-msgid "Cancel Transcription"
-msgstr "Transcriptie wissen"
-
-#: buzz/widgets/main_window_toolbar.py buzz/widgets/main_window.py
-#: buzz/settings/shortcut.py
-msgid "Clear History"
-msgstr "Geschiedenis wissen"
-
-#: buzz/widgets/main_window_toolbar.py buzz/widgets/update_dialog.py
-msgid "Update Available"
-msgstr "Update Beschikbaar"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "In Progress"
-msgstr "In behandeling"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Completed"
-msgstr "Afgerond"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Failed"
-msgstr "Mislukt"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Canceled"
-msgstr "Afgebroken"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Queued"
-msgstr "In wachtrij"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "File Name / URL"
-msgstr "Bestandsnaam/Url"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Model"
-msgstr "Model"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Task"
-msgstr "Taak"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Status"
-msgstr "Status"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Date Completed"
-msgstr "Afgerond op"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Date Added"
-msgstr "Toegevoegd op"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Notes"
-msgstr "Notities"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Reset Column Order"
-msgstr "Kolomvolgorde herstellen"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Restart Transcription"
-msgstr "Transcriptie opnieuw starten"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Rename"
-msgstr "Hernoemen"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Add/Edit Notes"
-msgstr "Notities toevoegen/bewerken"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Rename Transcription"
-msgstr "Transcriptie hernoemen"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Enter new name:"
-msgstr "Voer een nieuwe naam in:"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Enter some relevant notes for this transcription:"
-msgstr "Voer relevante notities in voor deze transcriptie:"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Cannot Restart"
-msgstr "Kan niet opnieuw starten"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Only failed or canceled transcriptions can be restarted."
-msgstr ""
-"Alleen mislukte of afgebroken transcripties kunnen opnieuw worden gestart."
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Failed to restart transcription: {}"
-msgstr "Transcriptie opnieuw starten mislukt: {}"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid ""
-"Could not restart transcription: model not available and could not be "
-"downloaded."
-msgstr ""
-"Transcriptie kon niet opnieuw worden gestart: model niet beschikbaar en kon "
-"niet worden gedownload."
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Could not restart transcription: transcriber worker not found."
-msgstr ""
-"Transcriptie kon niet opnieuw worden gestart: transcriptieproces niet "
-"gevonden."
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Live Recording"
-msgstr "Live-opname"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Click Record to begin..."
-msgstr "Klik op de opnameknop om te beginnen…"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Waiting for AI translation..."
-msgstr "Bezig met wachten op AI-vertaling…"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Microphone:"
-msgstr "Microfoon:"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Show in new window"
-msgstr "In nieuw venster tonen"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Text Size:"
-msgstr "Tekstgrootte:"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Theme"
-msgstr "Thema"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Light"
-msgstr "Licht"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Dark"
-msgstr "Donker"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Custom"
-msgstr "Aangepast"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Text Color"
-msgstr "Tekstkleur"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Background Color"
-msgstr "Achtergrondkleur"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Fullscreen"
-msgstr "Volledig scherm"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy"
-msgstr "Kopiëren"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy transcription to clipboard"
-msgstr "Transcriptie naar klembord kopiëren"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Nothing to copy!"
-msgstr "Niets om te kopiëren!"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy failed"
-msgstr "Kopiëren mislukt"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copied!"
-msgstr "Gekopieerd!"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Select Text Color"
-msgstr "Kies een tekstkleur"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Select Background Color"
-msgstr "Kies een achtergrondkleur"
-
-#: buzz/widgets/recording_transcriber_widget.py
-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
-msgid ""
-"Please check your audio devices or check the application logs for more "
-"information."
-msgstr "Controleer uw geluidsapparatuur of het programmalogboek."
-
-#: buzz/widgets/update_dialog.py
-msgid "A new version of Buzz is available!"
-msgstr "Er is een nieuwe versie van Buzz beschikbaar!"
-
-#: buzz/widgets/update_dialog.py
-msgid "Current version:"
-msgstr "Huidige versie:"
-
-#: buzz/widgets/update_dialog.py
-msgid "New version:"
-msgstr "Versie:"
-
-#: buzz/widgets/update_dialog.py
-msgid "Release Notes:"
-msgstr "Release-opmerkingen:"
-
-#: buzz/widgets/update_dialog.py
-msgid "Download and Install"
-msgstr "Downloaden en installeren"
-
-#: buzz/widgets/update_dialog.py
-msgid "No download URL available for your platform."
-msgstr "Geen download-URL beschikbaar voor uw platform."
-
-#: buzz/widgets/update_dialog.py
-msgid "Downloading file {} of {}..."
-msgstr "Bestand {} van {} downloaden..."
-
-#: buzz/widgets/update_dialog.py
-msgid "Downloading file {} of {} ({:.1f} MB / {:.1f} MB)..."
-msgstr "Bestand {} van {} downloaden ({:.1f} MB / {:.1f} MB)..."
-
-#: buzz/widgets/update_dialog.py
-msgid "Download Failed"
-msgstr "Download mislukt"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to download the update: {}"
-msgstr "Het downloaden van de update is mislukt: {}"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to save the installer: {}"
-msgstr "Kan het installatieprogramma niet opslaan: {}"
-
-#: buzz/widgets/update_dialog.py
-msgid "Download complete!"
-msgstr "Download voltooid!"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to run the installer: {}"
-msgstr "Kan het installatieprogramma niet uitvoeren: {}"
-
-#: buzz/widgets/about_dialog.py
-msgid "Check for updates"
-msgstr "Controleren op updates"
-
-#: buzz/widgets/about_dialog.py
-msgid "Show logs"
-msgstr "Logboeken tonen"
-
-#: buzz/widgets/about_dialog.py
-msgid "You're up to date!"
-msgstr "De software is actueel!"
-
-#: buzz/widgets/audio_meter_widget.py
-msgid "Average volume"
-msgstr "Gemiddeld volume"
-
-#: buzz/widgets/audio_meter_widget.py
-msgid "Queue"
-msgstr "Wachtrij"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-msgid "Start"
-msgstr "Begin"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-msgid "End"
-msgstr "Einde"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Text"
-msgstr "Tekst"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Translation"
-msgstr "Vertaling"
-
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-msgid "View"
-msgstr "Bekijken"
-
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-msgid "Timestamps"
-msgstr "Tijdstippen"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Export"
-msgstr "Exporteren"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Translate"
-msgstr "Vertalen"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Resize"
-msgstr "Grootte"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Identify Speakers"
-msgstr "Sprekers identificeren"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Find"
-msgstr "Zoeken"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Show/Hide Search Bar (Ctrl+F)"
-msgstr "Zoekbalk tonen/verbergen (Ctrl+F)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Find:"
-msgstr "Zoeken:"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Enter text to find..."
-msgstr "Voer te zoeken tekst in…"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Previous match (Shift+Enter)"
-msgstr "Vorige overeenkomst (Shift+Enter)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Next match (Ctrl+Enter)"
-msgstr "Volgende overeenkomst (Ctrl+Enter)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Clear"
-msgstr "Wissen"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Playback Controls:"
-msgstr "Afspeelbediening:"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Loop Segment"
-msgstr "Fragment herhalen"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Enable/disable looping when clicking on transcript segments"
-msgstr "Herhaling in- of uitschakelen bij het klikken op transcriptfragmenten"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Follow Audio"
-msgstr "Audio volgen"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid ""
-"Enable/disable following the current audio position in the transcript. When "
-"enabled, automatically scrolls to current text."
-msgstr ""
-"De huidige audiopositie in de transcriptie in- of uitschakelen. Wanneer "
-"ingeschakeld, wordt automatisch naar de huidige tekst gescrold."
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Scroll to Current"
-msgstr "Naar huidig fragment scrollen"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Scroll to the currently spoken text"
-msgstr "Naar de momenteel gesproken tekst scrollen"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "1 of 100+ matches"
-msgstr "1 van 100+ overeenkomsten"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "1 of "
-msgstr "1 van "
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " matches"
-msgstr " overeenkomsten"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "No matches found"
-msgstr "Geen overeenkomsten gevonden"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " of 100+ matches"
-msgstr " van 100+ overeenkomsten"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " of "
-msgstr " van "
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "API Key Required"
-msgstr "Api-sleutel vereist"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Please enter OpenAI API Key in preferences"
-msgstr "Voer de OpenAI-api-sleutel in in de instellingen"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend end time"
-msgstr "Eindtijd verlengen"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend endings by up to (seconds)"
-msgstr "Eindes verlengen met maximaal (seconden)"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend endings"
-msgstr "Eindes verlengen"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Resize Options"
-msgstr "Grootteopties"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Desired subtitle length"
-msgstr "Voorkeurslengte van ondertiteling"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Available only if word level timings were disabled during transcription"
-msgstr ""
-"Alleen beschikbaar als timings op woordniveau zijn uitgeschakeld tijdens "
-"transcriptie"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge Options"
-msgstr "Samenvoegopties"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge by gap"
-msgstr "Samenvoegen op basis van tussenruimte"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Split by punctuation"
-msgstr "Splitsen op basis van leestekens"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Split by max length"
-msgstr "Splitsen op basis van max. lengte"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge"
-msgstr "Samenvoegen"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Available only if word level timings were enabled during transcription"
-msgstr ""
-"Alleen beschikbaar als timings op woordniveau zijn ingeschakeld tijdens "
-"transcriptie"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid ""
-"Speaker identification is not available: failed to load required libraries."
-msgstr ""
-"Sprekeridentificatie is niet beschikbaar: het laden van de vereiste "
-"bibliotheken is mislukt."
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "1/8 Collecting transcripts"
-msgstr "1/8 Transcripties verzamelen"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "2/8 Loading audio"
-msgstr "2/8 Audio laden"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "3/8 Loading alignment model"
-msgstr "3/8 Uitlijningsmodel laden"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "3/8 Loading alignment model (retrying with cache...)"
-msgstr "3/8 Uitlijningsmodel laden (opnieuw proberen met cache…)"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid ""
-"Failed to load alignment model. Please check your internet connection and "
-"try again."
-msgstr ""
-"Het laden van het uitlijningsmodel is mislukt. Controleer uw "
-"internetverbinding en probeer het opnieuw."
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "4/8 Processing audio"
-msgstr "4/8 Audio verwerken"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "5/8 Preparing transcripts"
-msgstr "5/8 Transcripties voorbereiden"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "6/8 Identifying speakers"
-msgstr "6/8 Sprekers identificeren"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "7/8 Mapping speakers to transcripts"
-msgstr "7/8 Sprekers koppelen aan transcripties"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "8/8 Identification done"
-msgstr "8/8 Identificatie voltooid"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "0/0 Error identifying speakers"
-msgstr "0/0 Fout bij het identificeren van sprekers"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Step 1: Identify speakers"
-msgstr "Stap 1: Sprekers identificeren"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Identify"
-msgstr "Identificeren"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Ready to identify speakers"
-msgstr "Klaar om sprekers te identificeren"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Audio file not found"
-msgstr "Audiobestand niet gevonden"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Step 2: Name speakers"
-msgstr "Stap 2: Sprekers een naam geven"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Play sample"
-msgstr "Fragment afspelen"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Merge speaker sentences"
-msgstr "Zinnen van sprekers samenvoegen"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Save"
-msgstr "Opslaan"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Cancelling..."
-msgstr "Bezig met annuleren…"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Cancelled"
-msgstr "Afgebroken"
-
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Save File"
-msgstr "Bestand opslaan"
-
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Text files"
-msgstr "Tekstbestanden"
-
-#: buzz/widgets/model_download_progress_dialog.py
-msgid "Downloading model"
-msgstr "Bezig met ophalen van model…"
-
-#: buzz/widgets/model_download_progress_dialog.py
-msgid "remaining"
-msgstr "resterend"
-
-#: buzz/widgets/menu_bar.py
-msgid "Import File..."
-msgstr "Bestand importeren…"
-
-#: buzz/widgets/menu_bar.py
-msgid "Import URL..."
-msgstr "Url importeren…"
-
-#: buzz/widgets/menu_bar.py
-msgid "Import Folder..."
-msgstr "Map importeren…"
-
-#: buzz/widgets/menu_bar.py
-msgid "About"
-msgstr "Over"
-
-#: buzz/widgets/menu_bar.py
-msgid "Preferences..."
-msgstr "Instellingen…"
-
-#: buzz/widgets/menu_bar.py
-msgid "Help"
-msgstr "Hulp"
-
-#: buzz/widgets/menu_bar.py
-msgid "File"
-msgstr "Bestand"
-
-#: buzz/widgets/main_window.py
-msgid ""
-"Are you sure you want to delete the selected transcription(s)? This action "
-"cannot be undone."
-msgstr ""
-"Weet u zeker dat u de gekozen transcriptie(s) wilt verwijderen? Deze actie "
-"is onomkeerbaar."
-
-#: buzz/widgets/main_window.py
-msgid "Select audio file"
-msgstr "Kies een audiobestand"
-
-#: buzz/widgets/main_window.py
-msgid "Select folder"
-msgstr "Kies een invoermap"
-
-#: buzz/widgets/main_window.py
-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
-#: buzz/transcriber/recording_transcriber.py
-msgid "Whisper server failed to start. Check logs for details."
-msgstr ""
-"De Whisper-server kon niet worden gestart. Raadpleeg de logboeken voor meer "
-"informatie."
-
-#: buzz/transcriber/local_whisper_cpp_server_transcriber.py
-#: buzz/transcriber/recording_transcriber.py
-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 ""
-"De Whisper-server kon niet worden gestart wegens onvoldoende geheugen. "
-"Probeer het opnieuw met een kleiner model. Gebruik de omgevingsvariabele "
-"BUZZ_FORCE_CPU=TRUE om CPU-modus te forceren."
-
-#: buzz/transcriber/transcriber.py
-msgid "Translate to English"
-msgstr "Naar het Engels vertalen"
-
-#: buzz/transcriber/transcriber.py
-msgid "Transcribe"
-msgstr "Transcriberen"
-
-#: buzz/transcriber/transcriber.py
-msgid "Chinese"
-msgstr "Chinees"
-
-#: buzz/transcriber/transcriber.py
-msgid "Russian"
-msgstr "Russisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Korean"
-msgstr "Koreaans"
-
-#: buzz/transcriber/transcriber.py
-msgid "French"
-msgstr "Frans"
-
-#: buzz/transcriber/transcriber.py
-msgid "Portuguese"
-msgstr "Portugees"
-
-#: buzz/transcriber/transcriber.py
-msgid "Turkish"
-msgstr "Turks"
-
-#: buzz/transcriber/transcriber.py
-msgid "Arabic"
-msgstr "Arabisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Swedish"
-msgstr "Zweeds"
-
-#: buzz/transcriber/transcriber.py
-msgid "Indonesian"
-msgstr "Indonesisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hindi"
-msgstr "Hindi"
-
-#: buzz/transcriber/transcriber.py
-msgid "Finnish"
-msgstr "Fins"
-
-#: buzz/transcriber/transcriber.py
-msgid "Vietnamese"
-msgstr "Vietnamees"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hebrew"
-msgstr "Hebreeuws"
-
-#: buzz/transcriber/transcriber.py
-msgid "Greek"
-msgstr "Grieks"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malay"
-msgstr "Maleis"
-
-#: buzz/transcriber/transcriber.py
-msgid "Czech"
-msgstr "Tsjechisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Romanian"
-msgstr "Roemeens"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hungarian"
-msgstr "Hongaars"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tamil"
-msgstr "Tamil"
-
-#: buzz/transcriber/transcriber.py
-msgid "Norwegian"
-msgstr "Noors"
-
-#: buzz/transcriber/transcriber.py
-msgid "Thai"
-msgstr "Thai"
-
-#: buzz/transcriber/transcriber.py
-msgid "Urdu"
-msgstr "Urdu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Croatian"
-msgstr "Kroatisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bulgarian"
-msgstr "Bulgaars"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lithuanian"
-msgstr "Litouws"
-
-#: buzz/transcriber/transcriber.py
-msgid "Latin"
-msgstr "Latijn"
-
-#: buzz/transcriber/transcriber.py
-msgid "Maori"
-msgstr "Maori"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malayalam"
-msgstr "Malayalam"
-
-#: buzz/transcriber/transcriber.py
-msgid "Welsh"
-msgstr "Welsh"
-
-#: buzz/transcriber/transcriber.py
-msgid "Slovak"
-msgstr "Slowaaks"
-
-#: buzz/transcriber/transcriber.py
-msgid "Telugu"
-msgstr "Telugu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Persian"
-msgstr "Perzisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bengali"
-msgstr "Bengaals"
-
-#: buzz/transcriber/transcriber.py
-msgid "Serbian"
-msgstr "Servisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Azerbaijani"
-msgstr "Azerbeidzjaans"
-
-#: buzz/transcriber/transcriber.py
-msgid "Slovenian"
-msgstr "Sloveens"
-
-#: buzz/transcriber/transcriber.py
-msgid "Kannada"
-msgstr "Kannada"
-
-#: buzz/transcriber/transcriber.py
-msgid "Estonian"
-msgstr "Ests"
-
-#: buzz/transcriber/transcriber.py
-msgid "Macedonian"
-msgstr "Macedonisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Breton"
-msgstr "Bretons"
-
-#: buzz/transcriber/transcriber.py
-msgid "Basque"
-msgstr "Baskisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Icelandic"
-msgstr "IJslands"
-
-#: buzz/transcriber/transcriber.py
-msgid "Armenian"
-msgstr "Armeens"
-
-#: buzz/transcriber/transcriber.py
-msgid "Nepali"
-msgstr "Nepalees"
-
-#: buzz/transcriber/transcriber.py
-msgid "Mongolian"
-msgstr "Mongools"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bosnian"
-msgstr "Bosnisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Kazakh"
-msgstr "Kazachs"
-
-#: buzz/transcriber/transcriber.py
-msgid "Albanian"
-msgstr "Albanees"
-
-#: buzz/transcriber/transcriber.py
-msgid "Swahili"
-msgstr "Swahili"
-
-#: buzz/transcriber/transcriber.py
-msgid "Galician"
-msgstr "Galicisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Marathi"
-msgstr "Marathi"
-
-#: buzz/transcriber/transcriber.py
-msgid "Punjabi"
-msgstr "Punjabi"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sinhala"
-msgstr "Singalees"
-
-#: buzz/transcriber/transcriber.py
-msgid "Khmer"
-msgstr "Khmer"
-
-#: buzz/transcriber/transcriber.py
-msgid "Shona"
-msgstr "Shona"
-
-#: buzz/transcriber/transcriber.py
-msgid "Yoruba"
-msgstr "Yoruba"
-
-#: buzz/transcriber/transcriber.py
-msgid "Somali"
-msgstr "Somalisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Afrikaans"
-msgstr "Afrikaans"
-
-#: buzz/transcriber/transcriber.py
-msgid "Occitan"
-msgstr "Occitaans"
-
-#: buzz/transcriber/transcriber.py
-msgid "Georgian"
-msgstr "Georgisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Belarusian"
-msgstr "Belarussisch (Wit-Russisch)"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tajik"
-msgstr "Tadzjieks"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sindhi"
-msgstr "Sindhi"
-
-#: buzz/transcriber/transcriber.py
-msgid "Gujarati"
-msgstr "Gujarati"
-
-#: buzz/transcriber/transcriber.py
-msgid "Amharic"
-msgstr "Amhaars"
-
-#: buzz/transcriber/transcriber.py
-msgid "Yiddish"
-msgstr "Jiddisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lao"
-msgstr "Laotiaans"
-
-#: buzz/transcriber/transcriber.py
-msgid "Uzbek"
-msgstr "Oezbeeks"
-
-#: buzz/transcriber/transcriber.py
-msgid "Faroese"
-msgstr "Faeröers"
-
-#: buzz/transcriber/transcriber.py
-msgid "Haitian Creole"
-msgstr "Haïtiaans-Creools"
-
-#: buzz/transcriber/transcriber.py
-msgid "Pashto"
-msgstr "Pashto"
-
-#: buzz/transcriber/transcriber.py
-msgid "Turkmen"
-msgstr "Turkmeens"
-
-#: buzz/transcriber/transcriber.py
-msgid "Nynorsk"
-msgstr "Nynorsk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Maltese"
-msgstr "Maltees"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sanskrit"
-msgstr "Sanskriet"
-
-#: buzz/transcriber/transcriber.py
-msgid "Luxembourgish"
-msgstr "Luxemburgs"
-
-#: buzz/transcriber/transcriber.py
-msgid "Myanmar"
-msgstr "Myanmar"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tibetan"
-msgstr "Tibetaans"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tagalog"
-msgstr "Tagalog"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malagasy"
-msgstr "Malagassisch"
-
-#: buzz/transcriber/transcriber.py
-msgid "Assamese"
-msgstr "Assamees"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tatar"
-msgstr "Tataars"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hawaiian"
-msgstr "Hawaïaans"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lingala"
-msgstr "Lingala"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hausa"
-msgstr "Hausa"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bashkir"
-msgstr "Bashkir"
-
-#: buzz/transcriber/transcriber.py
-msgid "Javanese"
-msgstr "Javaans"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sundanese"
-msgstr "Soedanees"
-
-#: buzz/transcriber/transcriber.py
-msgid "Cantonese"
-msgstr "Kantonees"
-
-#: buzz/transcriber/recording_transcriber.py buzz/model_loader.py
-msgid "A connection error occurred"
-msgstr "Er is een verbindingsfout opgetreden"
-
-#: buzz/transcriber/recording_transcriber.py
-msgid "Starting Whisper.cpp..."
-msgstr "Whisper.cpp wordt gestart…"
-
-#: buzz/transcriber/recording_transcriber.py
-msgid "Starting transcription..."
-msgstr "Transcriptie wordt gestart…"
-
-#: buzz/settings/shortcut.py
-msgid "Open Record Window"
-msgstr "Opnamevenster openen"
-
-#: buzz/settings/shortcut.py
-msgid "Import File"
-msgstr "Bestand importeren"
-
-#: buzz/settings/shortcut.py
-msgid "Open Preferences Window"
-msgstr "Instellingenvenster openen"
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Text"
-msgstr "Transcriptie bekijken"
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Translation"
-msgstr "Getranscribeerde vertaling bekijken"
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Timestamps"
-msgstr "Getranscribeerde tijdstippen bekijken"
-
-#: buzz/settings/shortcut.py
-msgid "Search Transcript"
-msgstr "Transcriptie doorzoeken"
-
-#: buzz/settings/shortcut.py
-msgid "Go to Next Transcript Search Result"
-msgstr "Naar volgend zoekresultaat in transcriptie gaan"
-
-#: buzz/settings/shortcut.py
-msgid "Go to Previous Transcript Search Result"
-msgstr "Naar vorig zoekresultaat in transcriptie gaan"
-
-#: buzz/settings/shortcut.py
-msgid "Scroll to Current Text"
-msgstr "Naar huidige tekst scrollen"
-
-#: buzz/settings/shortcut.py
-msgid "Play/Pause Audio"
-msgstr "Audio afspelen/pauzeren"
-
-#: buzz/settings/shortcut.py
-msgid "Replay Current Segment"
-msgstr "Huidig fragment opnieuw afspelen"
-
-#: buzz/settings/shortcut.py
-msgid "Toggle Playback Controls"
-msgstr "Afspeelbediening in-/uitschakelen"
-
-#: buzz/settings/shortcut.py
-msgid "Decrease Segment Start Time"
-msgstr "Begintijd van fragment verkleinen"
-
-#: buzz/settings/shortcut.py
-msgid "Increase Segment Start Time"
-msgstr "Begintijd van fragment vergroten"
-
-#: buzz/settings/shortcut.py
-msgid "Decrease Segment End Time"
-msgstr "Eindtijd van fragment verkleinen"
-
-#: buzz/settings/shortcut.py
-msgid "Increase Segment End Time"
-msgstr "Eindtijd van fragment vergroten"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append below"
-msgstr "Onderaan toevoegen"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append above"
-msgstr "Bovenaan toevoegen"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append and correct"
-msgstr "Toevoegen en corrigeren"
-
-#: buzz/file_transcriber_queue_worker.py
-msgid ""
-"Speech extraction failed! Check your internet connection — a model may need "
-"to be downloaded."
-msgstr ""
-"Spraakextractie mislukt! Controleer uw internetverbinding — mogelijk moet er "
-"een model worden gedownload."
-
-#~ msgid "Comma-separated, e.g. \"0.0, 0.2, 0.4, 0.6, 0.8, 1.0\""
-#~ msgstr "Kommagescheiden, bijv. '0.0, 0.2, 0.4, 0.6, 0.8, 1.0'"
-
-#~ msgid "Temperature:"
-#~ msgstr "Temperatuur:"
-
-#~ msgid "Please translate each text sent to you from English to Spanish."
-#~ msgstr ""
-#~ "Vertaal elke tekst die naar u wordt verzonden van het Engels naar het "
-#~ "Spaans."
-
-#~ msgid "Translation error, see logs!"
-#~ msgstr "Vertaalfout, raadpleeg de logboeken!"
-
-#~ msgid "Snap permission notice"
-#~ msgstr "Snap-rechten"
-
-#~ msgid ""
-#~ "Detected missing permissions, please check that snap permissions have "
-#~ "been granted"
-#~ msgstr ""
-#~ "Er ontbreken toegangsrechten - controleer of ze daadwerkelijk allemaal "
-#~ "zijn toegekend"
-
-#~ msgid ""
-#~ "To enable necessary permissions run the following commands in the terminal"
-#~ msgstr ""
-#~ "De rechten kunnen met behulp van deze terminalopdrachten worden verleend"
-
-#~ msgid "Close"
-#~ msgstr "Sluiten"
-
-#~ msgid "Enter instructions for AI on how to translate..."
-#~ msgstr "Voer vertaalinstructies in…"
diff --git a/buzz/locale/pl_PL/LC_MESSAGES/buzz.po b/buzz/locale/pl_PL/LC_MESSAGES/buzz.po
deleted file mode 100644
index 251a1264..00000000
--- a/buzz/locale/pl_PL/LC_MESSAGES/buzz.po
+++ /dev/null
@@ -1,1665 +0,0 @@
-# POLISH TRANSLATION.
-# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
-# This file is distributed under the same license as the PACKAGE package.
-# FIRST AUTHOR , YEAR.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: \n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2026-03-07 20:20+0200\n"
-"PO-Revision-Date: 2024-03-17 20:50+0200\n"
-"Last-Translator: \n"
-"Language-Team: \n"
-"Language: pl_PL\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Generator: Poedit 3.2.2\n"
-
-#: buzz/widgets/import_url_dialog.py buzz/settings/shortcut.py
-msgid "Import URL"
-msgstr "Importuj URL"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "https://example.com/audio.mp3"
-msgstr "https://przyklad.pl/audio.mp3"
-
-#: buzz/widgets/import_url_dialog.py
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-#: buzz/widgets/main_window.py
-msgid "Ok"
-msgstr "OK"
-
-#: buzz/widgets/import_url_dialog.py
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-#: buzz/widgets/model_download_progress_dialog.py buzz/widgets/main_window.py
-msgid "Cancel"
-msgstr "Anuluj"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "URL:"
-msgstr "URL:"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "Invalid URL"
-msgstr "Nieprawidłowy URL"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "The URL you entered is invalid."
-msgstr "Wprowadzony URL nie jest prawidłowy"
-
-#: buzz/widgets/presentation_window.py
-msgid "Live Transcript Presentation"
-msgstr "Prezentacja transkrypcji na żywo"
-
-#: buzz/widgets/preferences_dialog/shortcuts_editor_preferences_widget.py
-msgid "Reset to Defaults"
-msgstr "Przywróć ustawienia domyślne"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "English"
-msgstr "Angielski"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Catalan"
-msgstr "Kataloński"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Danish"
-msgstr "Duński"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Dutch"
-msgstr "Niderlandzki"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "German"
-msgstr "Niemiecki"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Spanish"
-msgstr "Hiszpański"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Italian"
-msgstr "Włoski"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Japanese"
-msgstr "Japoński"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Latvian"
-msgstr "Łotewski"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Polish"
-msgstr "Polski"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Portuguese (Brazil)"
-msgstr "Portugalski (Brazylia)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Ukrainian"
-msgstr "Ukraiński"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Chinese (Simplified)"
-msgstr "Chiński (uproszczony)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Chinese (Traditional)"
-msgstr "Chiński (tradycyjny)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Restart required!"
-msgstr "Wymagane ponowne uruchomienie!"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Ui Language"
-msgstr "Język interfejsu"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Font Size"
-msgstr "Rozmiar czcionki"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Test"
-msgstr "Testuj"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API key"
-msgstr "Klucz API OpenAI"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI base url"
-msgstr "Podstawowy adres URL OpenAI"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API model"
-msgstr "Model API OpenAI"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Default export file name"
-msgstr "Domyślna nazwa pliku eksportu"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Enable live recording transcription export"
-msgstr "Włącz eksport transkrypcji nagrania na żywo"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Browse"
-msgstr "Przeglądaj"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Export folder"
-msgstr "Folder eksportu"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Live recording mode"
-msgstr "Tryb nagrywania na żywo"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Note: Live recording export settings will be moved to the Advanced Settings "
-"in the Live Recording screen in a future version."
-msgstr ""
-"Uwaga: Ustawienia eksportu nagrywania na żywo zostaną przeniesione do "
-"Ustawień zaawansowanych ekranu nagrywania na żywo w przyszłej wersji."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Use 8-bit quantization to reduce memory usage"
-msgstr "Użyj kwantyzacji 8-bitowej, aby zmniejszyć zużycie pamięci"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Applies to Huggingface and Faster Whisper models. Reduces GPU memory usage "
-"but may slightly decrease transcription quality."
-msgstr ""
-"Dotyczy modeli Huggingface i Faster Whisper. Zmniejsza zużycie pamięci GPU, "
-"ale może nieznacznie obniżyć jakość transkrypcji."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Reduce GPU RAM"
-msgstr "Zmniejsz pamięć RAM GPU"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Use only CPU and disable GPU acceleration"
-msgstr "Używaj tylko CPU i wyłącz akcelerację GPU"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Set this if larger models do not fit your GPU memory and Buzz crashes"
-msgstr ""
-"Ustaw to, jeśli większe modele nie mieszczą się w pamięci GPU i Buzz się "
-"zawiesza"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Disable GPU"
-msgstr "Wyłącz GPU"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API Key Test"
-msgstr "Test klucza API OpenAI"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Your API key is valid. Buzz will use this key to perform Whisper API "
-"transcriptions and AI translations."
-msgstr ""
-"Twój klucz API jest prawidłowy. Buzz użyje tego klucza do wykonywania "
-"transkrypcji przez Whisper API oraz tłumaczeń AI."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Invalid API key"
-msgstr "Nieprawidłowy klucz API"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"API supports only base64 characters (A-Za-z0-9+/=_-). Other characters in "
-"API key may cause errors."
-msgstr ""
-"API obsługuje tylko znaki base64 (A-Za-z0-9+/=_-). Inne znaki w kluczu API "
-"mogą powodować błędy."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Select Export Folder"
-msgstr "Wybierz folder eksportu"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"OpenAI API returned invalid response. Please check the API url or your key. "
-"Transcription and translation may still work if the API does not support key "
-"validation."
-msgstr ""
-"OpenAI API zwróciło nieprawidłową odpowiedź. Sprawdź adres URL API lub swój "
-"klucz. Transkrypcja i tłumaczenie mogą nadal działać, jeśli API nie "
-"obsługuje weryfikacji klucza."
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Enable folder watch"
-msgstr "Włącz obserwację folderu"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Delete processed files"
-msgstr "Usuń przetworzonych pliki"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Input folder"
-msgstr "Folder wejściowy"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Output folder"
-msgstr "Folder wyjściowy"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Select Input Folder"
-msgstr "Wybierz folder wejściowy"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Select Output Folder"
-msgstr "Wybierz folder wyjściowy"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Preferences"
-msgstr "Ustawienia"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "General"
-msgstr "Główne"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Models"
-msgstr "Modele"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Shortcuts"
-msgstr "Skróty"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Folder Watch"
-msgstr "Przeglądanie folderu"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Group"
-msgstr "Grupa"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Huggingface ID of a Faster whisper model"
-msgstr "ID modelu Faster Whisper z Huggingface"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download"
-msgstr "Pobierz"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Show file location"
-msgstr "Pokaż lokalizacje pliku"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Delete"
-msgstr "Usuń"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Downloaded"
-msgstr "Pobrany"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Available for Download"
-msgstr "Dostępne do pobrania"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download link to Whisper.cpp ggml model file"
-msgstr "Link do pobrania pliku modelu Whisper.cpp ggml"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Delete Model"
-msgstr "Usuń model"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Are you sure you want to delete the selected model?"
-msgstr "Czy na pewno chcesz usunąć wybrany model?"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download failed"
-msgstr "Pobieranie nie powiodło się"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcription_tasks_table_widget.py
-#: buzz/widgets/update_dialog.py buzz/widgets/main_window.py
-#: buzz/model_loader.py
-msgid "Error"
-msgstr "Błąd"
-
-#: buzz/widgets/record_button.py buzz/widgets/main_window_toolbar.py
-msgid "Record"
-msgstr "Nagraj"
-
-#: buzz/widgets/record_button.py
-msgid "Stop"
-msgstr "Zatrzymaj"
-
-#: buzz/widgets/transcriber/languages_combo_box.py
-#: buzz/transcriber/transcriber.py
-msgid "Detect Language"
-msgstr "Wykryj język"
-
-#: buzz/widgets/transcriber/mms_language_line_edit.py
-msgid "e.g., eng, fra, deu"
-msgstr "np. eng, fra, deu"
-
-#: buzz/widgets/transcriber/mms_language_line_edit.py
-msgid ""
-"Enter an ISO 639-3 language code (3 letters).\n"
-"Examples: eng (English), fra (French), deu (German),\n"
-"spa (Spanish), lav (Latvian)"
-msgstr ""
-"Wprowadź kod języka ISO 639-3 (3 litery).\n"
-"Przykłady: eng (angielski), fra (francuski), deu (niemiecki),\n"
-"spa (hiszpański), lav (łotewski)"
-
-#: buzz/widgets/transcriber/file_transcriber_widget.py
-msgid "Run"
-msgstr "Rozpocznij"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Model:"
-msgstr "Model:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-#: buzz/transcriber/recording_transcriber.py
-msgid "First time use of a model may take up to several minutest to load."
-msgstr "Pierwsze użycie modelu może potrwać kilka minut podczas ładowania."
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Api Key:"
-msgstr "Klucz API:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Task:"
-msgstr "Zadanie:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Language:"
-msgstr "Język:"
-
-#: buzz/widgets/transcriber/initial_prompt_text_edit.py
-msgid "Enter prompt..."
-msgstr "Wprowadź instrukcje..."
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Advanced Settings"
-msgstr "Ustawienia zaawansowane"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Speech recognition settings"
-msgstr "Ustawienia rozpoznawania mowy"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Initial Prompt:"
-msgstr "Wstępne instrukcje:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Translation settings"
-msgstr "Ustawienia tłumaczenia"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Enable AI translation"
-msgstr "Włącz tłumaczenie AI"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "AI model:"
-msgstr "Model AI:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid ""
-"Please translate each text sent to you from English to Spanish. Translation "
-"will be used in an automated system, please do not add any comments or "
-"notes, just the translation."
-msgstr ""
-"Proszę przetłumaczyć każdy przesłany tekst z języka angielskiego na "
-"hiszpański. Tłumaczenie będzie używane w systemie automatycznym, dlatego "
-"prosimy nie dodawać żadnych komentarzy ani uwag, tylko samo tłumaczenie."
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Instructions for AI:"
-msgstr "Instrukcje dla AI:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Recording settings"
-msgstr "Ustawienia nagrywania"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Silence threshold:"
-msgstr "Próg ciszy:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Live recording mode:"
-msgstr "Tryb nagrywania na żywo:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Line separator:"
-msgstr "Separator wierszy:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Transcription step:"
-msgstr "Krok transkrypcji:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Hide unconfirmed"
-msgstr "Ukryj niepotwierdzone"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Enable live recording export"
-msgstr "Włącz eksport nagrywania na żywo"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export folder:"
-msgstr "Folder eksportu:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export file name:"
-msgstr "Nazwa pliku eksportu:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Text file (.txt)"
-msgstr "Plik tekstowy (.txt)"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "CSV (.csv)"
-msgstr "CSV (.csv)"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export file type:"
-msgstr "Typ pliku eksportu:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid ""
-"Limit export entries\n"
-"(0 = export all):"
-msgstr ""
-"Ogranicz wpisy eksportu\n"
-"(0 = eksportuj wszystkie):"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Word-level timings"
-msgstr "Znaczniki dla słów"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Extract speech"
-msgstr "Wyodrębnij mowę"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Export:"
-msgstr "Eksportuj:"
-
-#: buzz/widgets/transcriber/hugging_face_search_line_edit.py
-msgid "Huggingface ID of a model"
-msgstr "ID modelu z Huggingface"
-
-#: buzz/widgets/transcriber/advanced_settings_button.py
-msgid "Advanced..."
-msgstr "Zaawansowane..."
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "New File Transcription"
-msgstr "Nowa transkrypcja pliku"
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "New URL Transcription"
-msgstr "Nowa transkrypcja z URL"
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "Open Transcript"
-msgstr "Otwórz transkrypt"
-
-#: buzz/widgets/main_window_toolbar.py buzz/settings/shortcut.py
-msgid "Cancel Transcription"
-msgstr "Anuluj transkrypcję"
-
-#: buzz/widgets/main_window_toolbar.py buzz/widgets/main_window.py
-#: buzz/settings/shortcut.py
-msgid "Clear History"
-msgstr "Wyczyść historię"
-
-#: buzz/widgets/main_window_toolbar.py buzz/widgets/update_dialog.py
-msgid "Update Available"
-msgstr "Dostępna aktualizacja"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "In Progress"
-msgstr "W toku"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Completed"
-msgstr "Ukończono"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Failed"
-msgstr "Nie powiodło się"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Canceled"
-msgstr "Anulowano"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Queued"
-msgstr "Kolejka"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "File Name / URL"
-msgstr "Nazwa pliku / URL"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Model"
-msgstr "Model"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Task"
-msgstr "Zadanie"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Status"
-msgstr "Status"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Date Completed"
-msgstr "Data ukończenia"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Date Added"
-msgstr "Data dodania"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Notes"
-msgstr "Notatki"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Reset Column Order"
-msgstr "Resetuj kolejność kolumn"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Restart Transcription"
-msgstr "Uruchom ponownie transkrypcję"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Rename"
-msgstr "Zmień nazwę"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Add/Edit Notes"
-msgstr "Dodaj/edytuj notatki"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Rename Transcription"
-msgstr "Zmień nazwę transkrypcji"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Enter new name:"
-msgstr "Wprowadź nową nazwę:"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Enter some relevant notes for this transcription:"
-msgstr "Wprowadź odpowiednie notatki do tej transkrypcji:"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Cannot Restart"
-msgstr "Nie można ponownie uruchomić"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Only failed or canceled transcriptions can be restarted."
-msgstr ""
-"Tylko nieudane lub anulowane transkrypcje mogą być uruchomione ponownie."
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Failed to restart transcription: {}"
-msgstr "Nie udało się uruchomić ponownie transkrypcji: {}"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid ""
-"Could not restart transcription: model not available and could not be "
-"downloaded."
-msgstr ""
-"Nie można uruchomić ponownie transkrypcji: model jest niedostępny i nie "
-"można go pobrać."
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Could not restart transcription: transcriber worker not found."
-msgstr ""
-"Nie można uruchomić ponownie transkrypcji: nie znaleziono procesu "
-"transkrypcji."
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Live Recording"
-msgstr "Nagrywanie na żywo"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Click Record to begin..."
-msgstr "Naciśnij Nagraj, aby zacząć..."
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Waiting for AI translation..."
-msgstr "Oczekiwanie na tłumaczenie AI..."
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Microphone:"
-msgstr "Mikrofon:"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Show in new window"
-msgstr "Pokaż w nowym oknie"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Text Size:"
-msgstr "Rozmiar tekstu:"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Theme"
-msgstr "Motyw"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Light"
-msgstr "Jasny"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Dark"
-msgstr "Ciemny"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Custom"
-msgstr "Niestandardowy"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Text Color"
-msgstr "Kolor tekstu"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Background Color"
-msgstr "Kolor tła"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Fullscreen"
-msgstr "Pełny ekran"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy"
-msgstr "Kopiuj"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy transcription to clipboard"
-msgstr "Kopiuj transkrypcję do schowka"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Nothing to copy!"
-msgstr "Nie ma nic do skopiowania!"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy failed"
-msgstr "Kopiowanie nie powiodło się"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copied!"
-msgstr "Skopiowano!"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Select Text Color"
-msgstr "Wybierz kolor tekstu"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Select Background Color"
-msgstr "Wybierz kolor tła"
-
-#: buzz/widgets/recording_transcriber_widget.py
-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
-msgid ""
-"Please check your audio devices or check the application logs for more "
-"information."
-msgstr ""
-"Sprawdź urządzenia audio lub przejrzyj logi aplikacji, by uzyskać więcej "
-"informacji."
-
-#: buzz/widgets/update_dialog.py
-msgid "A new version of Buzz is available!"
-msgstr "Dostępna jest nowa wersja Buzz!"
-
-#: buzz/widgets/update_dialog.py
-msgid "Current version:"
-msgstr "Aktualna wersja:"
-
-#: buzz/widgets/update_dialog.py
-msgid "New version:"
-msgstr "Nowa wersja:"
-
-#: buzz/widgets/update_dialog.py
-msgid "Release Notes:"
-msgstr "Informacje o wydaniu:"
-
-#: buzz/widgets/update_dialog.py
-msgid "Download and Install"
-msgstr "Pobierz i zainstaluj"
-
-#: buzz/widgets/update_dialog.py
-msgid "No download URL available for your platform."
-msgstr "Brak adresu URL do pobrania dla Twojej platformy."
-
-#: buzz/widgets/update_dialog.py
-msgid "Downloading file {} of {}..."
-msgstr "Pobieranie pliku {} z {}..."
-
-#: buzz/widgets/update_dialog.py
-msgid "Downloading file {} of {} ({:.1f} MB / {:.1f} MB)..."
-msgstr "Pobieranie pliku {} z {} ({:.1f} MB / {:.1f} MB)..."
-
-#: buzz/widgets/update_dialog.py
-msgid "Download Failed"
-msgstr "Pobieranie nie powiodło się"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to download the update: {}"
-msgstr "Nie udało się pobrać aktualizacji: {}"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to save the installer: {}"
-msgstr "Nie udało się zapisać instalatora: {}"
-
-#: buzz/widgets/update_dialog.py
-msgid "Download complete!"
-msgstr "Pobieranie zakończone!"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to run the installer: {}"
-msgstr "Nie udało się uruchomić instalatora: {}"
-
-#: buzz/widgets/about_dialog.py
-msgid "Check for updates"
-msgstr "Sprawdź aktualizacje"
-
-#: buzz/widgets/about_dialog.py
-msgid "Show logs"
-msgstr "Pokaż logi"
-
-#: buzz/widgets/about_dialog.py
-msgid "You're up to date!"
-msgstr "Posiadasz najnowszą wersję!"
-
-#: buzz/widgets/audio_meter_widget.py
-msgid "Average volume"
-msgstr "Średnia głośność"
-
-#: buzz/widgets/audio_meter_widget.py
-msgid "Queue"
-msgstr "Kolejka"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-msgid "Start"
-msgstr "Rozpocznij"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-msgid "End"
-msgstr "Zakończ"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Text"
-msgstr "Tekst"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Translation"
-msgstr "Tłumaczenie"
-
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-msgid "View"
-msgstr "Widok"
-
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-msgid "Timestamps"
-msgstr "Znaczniki czasu"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Export"
-msgstr "Eksportuj"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Translate"
-msgstr "Tłumacz"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Resize"
-msgstr "Zmień rozmiar"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Identify Speakers"
-msgstr "Identyfikuj mówców"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Find"
-msgstr "Znajdź"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Show/Hide Search Bar (Ctrl+F)"
-msgstr "Pokaż/ukryj pasek wyszukiwania (Ctrl+F)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Find:"
-msgstr "Znajdź:"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Enter text to find..."
-msgstr "Wprowadź tekst do wyszukania..."
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Previous match (Shift+Enter)"
-msgstr "Poprzedni wynik (Shift+Enter)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Next match (Ctrl+Enter)"
-msgstr "Następny wynik (Ctrl+Enter)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Clear"
-msgstr "Wyczyść"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Playback Controls:"
-msgstr "Sterowanie odtwarzaniem:"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Loop Segment"
-msgstr "Zapętlaj fragment"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Enable/disable looping when clicking on transcript segments"
-msgstr "Włącz/wyłącz zapętlanie po kliknięciu na fragmenty transkrypcji"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Follow Audio"
-msgstr "Podążaj za dźwiękiem"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid ""
-"Enable/disable following the current audio position in the transcript. When "
-"enabled, automatically scrolls to current text."
-msgstr ""
-"Włącz/wyłącz śledzenie bieżącej pozycji audio w transkrypcji. Gdy włączone, "
-"automatycznie przewija do bieżącego tekstu."
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Scroll to Current"
-msgstr "Przewiń do bieżącego"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Scroll to the currently spoken text"
-msgstr "Przewiń do aktualnie mówionego tekstu"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "1 of 100+ matches"
-msgstr "1 z 100+ wyników"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "1 of "
-msgstr "1 z "
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " matches"
-msgstr " wyników"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "No matches found"
-msgstr "Nie znaleziono wyników"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " of 100+ matches"
-msgstr " z 100+ wyników"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " of "
-msgstr " z "
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "API Key Required"
-msgstr "Wymagany klucz API"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Please enter OpenAI API Key in preferences"
-msgstr "Wprowadź klucz API OpenAI w ustawieniach"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend end time"
-msgstr "Wydłuż czas końcowy"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend endings by up to (seconds)"
-msgstr "Wydłuż zakończenia o maksymalnie (sekundy)"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend endings"
-msgstr "Wydłuż zakończenia"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Resize Options"
-msgstr "Opcje zmiany rozmiaru"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Desired subtitle length"
-msgstr "Żądana długość napisów"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Available only if word level timings were disabled during transcription"
-msgstr ""
-"Dostępne tylko jeśli znaczniki dla słów były wyłączone podczas transkrypcji"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge Options"
-msgstr "Opcje scalania"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge by gap"
-msgstr "Scal według przerwy"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Split by punctuation"
-msgstr "Podziel według interpunkcji"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Split by max length"
-msgstr "Podziel według maksymalnej długości"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge"
-msgstr "Scal"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Available only if word level timings were enabled during transcription"
-msgstr ""
-"Dostępne tylko jeśli znaczniki dla słów były włączone podczas transkrypcji"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid ""
-"Speaker identification is not available: failed to load required libraries."
-msgstr ""
-"Identyfikacja mówcy jest niedostępna: nie udało się załadować wymaganych "
-"bibliotek."
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "1/8 Collecting transcripts"
-msgstr "1/8 Zbieranie transkrypcji"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "2/8 Loading audio"
-msgstr "2/8 Ładowanie audio"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "3/8 Loading alignment model"
-msgstr "3/8 Ładowanie modelu wyrównania"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "3/8 Loading alignment model (retrying with cache...)"
-msgstr ""
-"3/8 Ładowanie modelu wyrównania (ponowna próba z pamięcią podręczną...)"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid ""
-"Failed to load alignment model. Please check your internet connection and "
-"try again."
-msgstr ""
-"Nie udało się załadować modelu wyrównania. Sprawdź połączenie internetowe i "
-"spróbuj ponownie."
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "4/8 Processing audio"
-msgstr "4/8 Przetwarzanie audio"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "5/8 Preparing transcripts"
-msgstr "5/8 Przygotowywanie transkrypcji"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "6/8 Identifying speakers"
-msgstr "6/8 Identyfikowanie mówców"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "7/8 Mapping speakers to transcripts"
-msgstr "7/8 Przypisywanie mówców do transkrypcji"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "8/8 Identification done"
-msgstr "8/8 Identyfikacja zakończona"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "0/0 Error identifying speakers"
-msgstr "0/0 Błąd podczas identyfikacji mówców"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Step 1: Identify speakers"
-msgstr "Krok 1: Identyfikuj mówców"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Identify"
-msgstr "Identyfikuj"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Ready to identify speakers"
-msgstr "Gotowy do identyfikacji mówców"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Audio file not found"
-msgstr "Nie znaleziono pliku audio"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Step 2: Name speakers"
-msgstr "Krok 2: Nazwij mówców"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Play sample"
-msgstr "Odtwórz próbkę"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Merge speaker sentences"
-msgstr "Scal zdania mówcy"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Save"
-msgstr "Zapisz"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Cancelling..."
-msgstr "Anulowanie..."
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Cancelled"
-msgstr "Anulowano"
-
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Save File"
-msgstr "Zapisz plik"
-
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Text files"
-msgstr "Pliki tekstowe"
-
-#: buzz/widgets/model_download_progress_dialog.py
-msgid "Downloading model"
-msgstr "Pobieranie modelu"
-
-#: buzz/widgets/model_download_progress_dialog.py
-msgid "remaining"
-msgstr "pozostało"
-
-#: buzz/widgets/menu_bar.py
-msgid "Import File..."
-msgstr "Importuj plik..."
-
-#: buzz/widgets/menu_bar.py
-msgid "Import URL..."
-msgstr "Importuj URL..."
-
-#: buzz/widgets/menu_bar.py
-msgid "Import Folder..."
-msgstr "Importuj folder..."
-
-#: buzz/widgets/menu_bar.py
-msgid "About"
-msgstr "O programie"
-
-#: buzz/widgets/menu_bar.py
-msgid "Preferences..."
-msgstr "Ustawienia..."
-
-#: buzz/widgets/menu_bar.py
-msgid "Help"
-msgstr "Pomoc"
-
-#: buzz/widgets/menu_bar.py
-msgid "File"
-msgstr "Plik"
-
-#: buzz/widgets/main_window.py
-msgid ""
-"Are you sure you want to delete the selected transcription(s)? This action "
-"cannot be undone."
-msgstr ""
-"Czy na pewno chcesz usunąć zaznaczone transkrypcje? Tej operacji nie można "
-"cofnąć."
-
-#: buzz/widgets/main_window.py
-msgid "Select audio file"
-msgstr "Wybierz plik audio"
-
-#: buzz/widgets/main_window.py
-msgid "Select folder"
-msgstr "Wybierz folder"
-
-#: buzz/widgets/main_window.py
-msgid "Unable to save OpenAI API key to keyring"
-msgstr "Nie można zapisać klucza API OpenAI w breloku kluczy"
-
-#: buzz/transcriber/local_whisper_cpp_server_transcriber.py
-#: buzz/transcriber/recording_transcriber.py
-msgid "Whisper server failed to start. Check logs for details."
-msgstr "Serwer Whisper nie uruchomił się. Sprawdź logi, aby uzyskać szczegóły."
-
-#: buzz/transcriber/local_whisper_cpp_server_transcriber.py
-#: buzz/transcriber/recording_transcriber.py
-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 ""
-"Serwer Whisper nie uruchomił się z powodu niewystarczającej pamięci. Spróbuj "
-"ponownie z mniejszym modelem. Aby wymusić tryb CPU, użyj zmiennej "
-"środowiskowej BUZZ_FORCE_CPU=TRUE."
-
-#: buzz/transcriber/transcriber.py
-msgid "Translate to English"
-msgstr "Przetłumacz na angielski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Transcribe"
-msgstr "Transkrybuj"
-
-#: buzz/transcriber/transcriber.py
-msgid "Chinese"
-msgstr "Chiński"
-
-#: buzz/transcriber/transcriber.py
-msgid "Russian"
-msgstr "Rosyjski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Korean"
-msgstr "Koreański"
-
-#: buzz/transcriber/transcriber.py
-msgid "French"
-msgstr "Francuski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Portuguese"
-msgstr "Portugalski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Turkish"
-msgstr "Turecki"
-
-#: buzz/transcriber/transcriber.py
-msgid "Arabic"
-msgstr "Arabski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Swedish"
-msgstr "Szwedzki"
-
-#: buzz/transcriber/transcriber.py
-msgid "Indonesian"
-msgstr "Indonezyjski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hindi"
-msgstr "Hindi"
-
-#: buzz/transcriber/transcriber.py
-msgid "Finnish"
-msgstr "Fiński"
-
-#: buzz/transcriber/transcriber.py
-msgid "Vietnamese"
-msgstr "Wietnamski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hebrew"
-msgstr "Hebrajski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Greek"
-msgstr "Grecki"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malay"
-msgstr "Malajski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Czech"
-msgstr "Czeski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Romanian"
-msgstr "Rumuński"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hungarian"
-msgstr "Węgierski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tamil"
-msgstr "Tamilski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Norwegian"
-msgstr "Norweski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Thai"
-msgstr "Tajski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Urdu"
-msgstr "Urdu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Croatian"
-msgstr "Chorwacki"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bulgarian"
-msgstr "Bułgarski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lithuanian"
-msgstr "Litewski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Latin"
-msgstr "Łacina"
-
-#: buzz/transcriber/transcriber.py
-msgid "Maori"
-msgstr "Maoryski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malayalam"
-msgstr "Malajalam"
-
-#: buzz/transcriber/transcriber.py
-msgid "Welsh"
-msgstr "Walijski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Slovak"
-msgstr "Słowacki"
-
-#: buzz/transcriber/transcriber.py
-msgid "Telugu"
-msgstr "Telugu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Persian"
-msgstr "Perski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bengali"
-msgstr "Bengalski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Serbian"
-msgstr "Serbski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Azerbaijani"
-msgstr "Azerbejdżański"
-
-#: buzz/transcriber/transcriber.py
-msgid "Slovenian"
-msgstr "Słoweński"
-
-#: buzz/transcriber/transcriber.py
-msgid "Kannada"
-msgstr "Kannada"
-
-#: buzz/transcriber/transcriber.py
-msgid "Estonian"
-msgstr "Estoński"
-
-#: buzz/transcriber/transcriber.py
-msgid "Macedonian"
-msgstr "Macedoński"
-
-#: buzz/transcriber/transcriber.py
-msgid "Breton"
-msgstr "Bretoński"
-
-#: buzz/transcriber/transcriber.py
-msgid "Basque"
-msgstr "Baskijski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Icelandic"
-msgstr "Islandzki"
-
-#: buzz/transcriber/transcriber.py
-msgid "Armenian"
-msgstr "Ormiański"
-
-#: buzz/transcriber/transcriber.py
-msgid "Nepali"
-msgstr "Nepalski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Mongolian"
-msgstr "Mongolski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bosnian"
-msgstr "Bośniacki"
-
-#: buzz/transcriber/transcriber.py
-msgid "Kazakh"
-msgstr "Kazachski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Albanian"
-msgstr "Albański"
-
-#: buzz/transcriber/transcriber.py
-msgid "Swahili"
-msgstr "Suahili"
-
-#: buzz/transcriber/transcriber.py
-msgid "Galician"
-msgstr "Galicyjski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Marathi"
-msgstr "Marathi"
-
-#: buzz/transcriber/transcriber.py
-msgid "Punjabi"
-msgstr "Pendżabski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sinhala"
-msgstr "Syngaleski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Khmer"
-msgstr "Khmerski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Shona"
-msgstr "Shona"
-
-#: buzz/transcriber/transcriber.py
-msgid "Yoruba"
-msgstr "Joruba"
-
-#: buzz/transcriber/transcriber.py
-msgid "Somali"
-msgstr "Somalijski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Afrikaans"
-msgstr "Afrikaans"
-
-#: buzz/transcriber/transcriber.py
-msgid "Occitan"
-msgstr "Oksytański"
-
-#: buzz/transcriber/transcriber.py
-msgid "Georgian"
-msgstr "Gruziński"
-
-#: buzz/transcriber/transcriber.py
-msgid "Belarusian"
-msgstr "Białoruski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tajik"
-msgstr "Tadżycki"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sindhi"
-msgstr "Sindhi"
-
-#: buzz/transcriber/transcriber.py
-msgid "Gujarati"
-msgstr "Gudżarati"
-
-#: buzz/transcriber/transcriber.py
-msgid "Amharic"
-msgstr "Amharski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Yiddish"
-msgstr "Jidysz"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lao"
-msgstr "Laotański"
-
-#: buzz/transcriber/transcriber.py
-msgid "Uzbek"
-msgstr "Uzbecki"
-
-#: buzz/transcriber/transcriber.py
-msgid "Faroese"
-msgstr "Farerski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Haitian Creole"
-msgstr "Kreolski haitański"
-
-#: buzz/transcriber/transcriber.py
-msgid "Pashto"
-msgstr "Paszto"
-
-#: buzz/transcriber/transcriber.py
-msgid "Turkmen"
-msgstr "Turkmeński"
-
-#: buzz/transcriber/transcriber.py
-msgid "Nynorsk"
-msgstr "Nynorsk"
-
-#: buzz/transcriber/transcriber.py
-msgid "Maltese"
-msgstr "Maltański"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sanskrit"
-msgstr "Sanskryt"
-
-#: buzz/transcriber/transcriber.py
-msgid "Luxembourgish"
-msgstr "Luksemburski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Myanmar"
-msgstr "Birmański"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tibetan"
-msgstr "Tybetański"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tagalog"
-msgstr "Tagalog"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malagasy"
-msgstr "Malgaski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Assamese"
-msgstr "Asamski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tatar"
-msgstr "Tatarski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hawaiian"
-msgstr "Hawajski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lingala"
-msgstr "Lingala"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hausa"
-msgstr "Hausa"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bashkir"
-msgstr "Baszkirski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Javanese"
-msgstr "Jawajski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sundanese"
-msgstr "Sundajski"
-
-#: buzz/transcriber/transcriber.py
-msgid "Cantonese"
-msgstr "Kantoński"
-
-#: buzz/transcriber/recording_transcriber.py buzz/model_loader.py
-msgid "A connection error occurred"
-msgstr "Wystąpił błąd połączenia"
-
-#: buzz/transcriber/recording_transcriber.py
-msgid "Starting Whisper.cpp..."
-msgstr "Uruchamianie Whisper.cpp..."
-
-#: buzz/transcriber/recording_transcriber.py
-msgid "Starting transcription..."
-msgstr "Rozpoczynanie transkrypcji..."
-
-#: buzz/settings/shortcut.py
-msgid "Open Record Window"
-msgstr "Otwórz okno nagrywania"
-
-#: buzz/settings/shortcut.py
-msgid "Import File"
-msgstr "Importuj plik"
-
-#: buzz/settings/shortcut.py
-msgid "Open Preferences Window"
-msgstr "Otwórz okno ustawień"
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Text"
-msgstr "Wyświetl tekst transkrypcji"
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Translation"
-msgstr "Wyświetl tłumaczenie transkrypcji"
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Timestamps"
-msgstr "Wyświetl znaczniki czasu transkrypcji"
-
-#: buzz/settings/shortcut.py
-msgid "Search Transcript"
-msgstr "Wyszukaj w transkrypcji"
-
-#: buzz/settings/shortcut.py
-msgid "Go to Next Transcript Search Result"
-msgstr "Przejdź do następnego wyniku wyszukiwania"
-
-#: buzz/settings/shortcut.py
-msgid "Go to Previous Transcript Search Result"
-msgstr "Przejdź do poprzedniego wyniku wyszukiwania"
-
-#: buzz/settings/shortcut.py
-msgid "Scroll to Current Text"
-msgstr "Przewiń do bieżącego tekstu"
-
-#: buzz/settings/shortcut.py
-msgid "Play/Pause Audio"
-msgstr "Odtwórz/wstrzymaj audio"
-
-#: buzz/settings/shortcut.py
-msgid "Replay Current Segment"
-msgstr "Odtwórz ponownie bieżący fragment"
-
-#: buzz/settings/shortcut.py
-msgid "Toggle Playback Controls"
-msgstr "Przełącz sterowanie odtwarzaniem"
-
-#: buzz/settings/shortcut.py
-msgid "Decrease Segment Start Time"
-msgstr "Zmniejsz czas początku fragmentu"
-
-#: buzz/settings/shortcut.py
-msgid "Increase Segment Start Time"
-msgstr "Zwiększ czas początku fragmentu"
-
-#: buzz/settings/shortcut.py
-msgid "Decrease Segment End Time"
-msgstr "Zmniejsz czas końca fragmentu"
-
-#: buzz/settings/shortcut.py
-msgid "Increase Segment End Time"
-msgstr "Zwiększ czas końca fragmentu"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append below"
-msgstr "Dodaj poniżej"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append above"
-msgstr "Dodaj powyżej"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append and correct"
-msgstr "Dodaj i popraw"
-
-#: buzz/file_transcriber_queue_worker.py
-msgid ""
-"Speech extraction failed! Check your internet connection — a model may need "
-"to be downloaded."
-msgstr ""
-"Wyodrębnianie mowy nie powiodło się! Sprawdź połączenie internetowe — może "
-"być konieczne pobranie modelu."
-
-#~ msgid "Comma-separated, e.g. \"0.0, 0.2, 0.4, 0.6, 0.8, 1.0\""
-#~ msgstr "Oddzielone przecinkiem, np. \"0.0, 0.2, 0.4, 0.6, 0.8, 1.0\""
-
-#~ msgid "Temperature:"
-#~ msgstr "Temperatura:"
-
-#~ msgid "Please translate each text sent to you from English to Spanish."
-#~ msgstr ""
-#~ "Proszę przetłumacz każdy przesłany tekst z angielskiego na hiszpański."
-
-#~ msgid "Translation error, see logs!"
-#~ msgstr "Błąd tłumaczenia, sprawdź logi!"
-
-#~ msgid "ID"
-#~ msgstr "ID"
-
-#~ msgid "Undo"
-#~ msgstr "Cofnij"
-
-#~ msgid "Redo"
-#~ msgstr "Ponów"
-
-#~ msgid "Downloading model (0%, unknown time remaining)"
-#~ msgstr "Pobieranie modelu (0%, pozostały czas nieznany)"
diff --git a/buzz/locale/pt_BR/LC_MESSAGES/buzz.po b/buzz/locale/pt_BR/LC_MESSAGES/buzz.po
deleted file mode 100644
index b9760b12..00000000
--- a/buzz/locale/pt_BR/LC_MESSAGES/buzz.po
+++ /dev/null
@@ -1,1680 +0,0 @@
-# Portuguese-Brazilian Translation for Buzz Project.
-# Copyright (C) 2025
-# This file is distributed under the same license as the package Buzz.
-# Paulo Schopf , 2025.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: Buzz\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2026-03-07 20:20+0200\n"
-"PO-Revision-Date: 2025-11-01 17:43-0300\n"
-"Last-Translator: Paulo Schopf \n"
-"Language-Team: none\n"
-"Language: pt_BR\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Generator: Poedit 3.6\n"
-
-#: buzz/widgets/import_url_dialog.py buzz/settings/shortcut.py
-msgid "Import URL"
-msgstr "Importar URL"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "https://example.com/audio.mp3"
-msgstr "https://exemplo.com/audio.mp3"
-
-#: buzz/widgets/import_url_dialog.py
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-#: buzz/widgets/main_window.py
-msgid "Ok"
-msgstr "Ok"
-
-#: buzz/widgets/import_url_dialog.py
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-#: buzz/widgets/model_download_progress_dialog.py buzz/widgets/main_window.py
-msgid "Cancel"
-msgstr "Cancelar"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "URL:"
-msgstr "URL:"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "Invalid URL"
-msgstr "URL inválida"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "The URL you entered is invalid."
-msgstr "A URL inserida é inválida."
-
-#: buzz/widgets/presentation_window.py
-msgid "Live Transcript Presentation"
-msgstr "Apresentação de Transcrição ao Vivo"
-
-#: buzz/widgets/preferences_dialog/shortcuts_editor_preferences_widget.py
-msgid "Reset to Defaults"
-msgstr "Redefinir para o Padrão"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "English"
-msgstr "Inglês"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Catalan"
-msgstr "Catalão"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Danish"
-msgstr "Dinamarquês"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Dutch"
-msgstr "Holandês"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "German"
-msgstr "Alemão"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Spanish"
-msgstr "Espanhol"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Italian"
-msgstr "Italiano"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Japanese"
-msgstr "Japonês"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Latvian"
-msgstr "Letão"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Polish"
-msgstr "Polonês"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Portuguese (Brazil)"
-msgstr "Português (Brasil)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Ukrainian"
-msgstr "Ucraniano"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Chinese (Simplified)"
-msgstr "Chinês (Simplificado)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Chinese (Traditional)"
-msgstr "Chinês (Tradicional)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Restart required!"
-msgstr "Reinicialização necessária!"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Ui Language"
-msgstr "Idioma da Interface"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Font Size"
-msgstr "Tamanho da Fonte"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Test"
-msgstr "Testar"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API key"
-msgstr "Chave API da OpenAI"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI base url"
-msgstr "URL base da OpenAI"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API model"
-msgstr "Modelo de API da OpenAI"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Default export file name"
-msgstr "Nome padrão do arquivo de exportação"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Enable live recording transcription export"
-msgstr "Habilitar exportação da transcrição ao vivo"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Browse"
-msgstr "Procurar"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Export folder"
-msgstr "Pasta de exportação"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Live recording mode"
-msgstr "Modo de gravação ao vivo"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Note: Live recording export settings will be moved to the Advanced Settings "
-"in the Live Recording screen in a future version."
-msgstr ""
-"Nota: As configurações de exportação de gravação ao vivo serão movidas para "
-"as Configurações Avançadas na tela de Gravação ao Vivo em uma versão futura."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Use 8-bit quantization to reduce memory usage"
-msgstr "Usar quantização de 8 bits para reduzir o uso de memória"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Applies to Huggingface and Faster Whisper models. Reduces GPU memory usage "
-"but may slightly decrease transcription quality."
-msgstr ""
-"Aplica-se aos modelos Huggingface e Faster Whisper. Reduz o uso de memória "
-"da GPU, mas pode diminuir ligeiramente a qualidade da transcrição."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Reduce GPU RAM"
-msgstr "Reduzir RAM da GPU"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Use only CPU and disable GPU acceleration"
-msgstr "Usar somente a CPU e desabilitar aceleração por GPU"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Set this if larger models do not fit your GPU memory and Buzz crashes"
-msgstr ""
-"Marque isso se modelos maiores não couberem na memória da GPU e o Buzz travar"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Disable GPU"
-msgstr "Desabilitar GPU"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API Key Test"
-msgstr "Teste da Chave API OpenAI"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Your API key is valid. Buzz will use this key to perform Whisper API "
-"transcriptions and AI translations."
-msgstr ""
-"Sua chave API é válida. O Buzz usará esta chave para realizar transcrições "
-"API Whisper e traduções de IA."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Invalid API key"
-msgstr "Chave API inválida"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"API supports only base64 characters (A-Za-z0-9+/=_-). Other characters in "
-"API key may cause errors."
-msgstr ""
-"A API suporta apenas caracteres base64 (A-Za-z0-9+/=_-). Outros caracteres "
-"na chave API podem causar erros."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Select Export Folder"
-msgstr "Selecionar Pasta de Exportação"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"OpenAI API returned invalid response. Please check the API url or your key. "
-"Transcription and translation may still work if the API does not support key "
-"validation."
-msgstr ""
-"A API OpenAI retornou uma resposta inválida. Verifique a URL da API ou sua "
-"chave. A transcrição e tradução ainda podem funcionar se a API não suportar "
-"validação de chave."
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Enable folder watch"
-msgstr "Habilitar monitoramento de pasta"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Delete processed files"
-msgstr "Excluir arquivos processados"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Input folder"
-msgstr "Pasta de entrada"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Output folder"
-msgstr "Pasta de saída"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Select Input Folder"
-msgstr "Selecionar Pasta de Entrada"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Select Output Folder"
-msgstr "Selecionar Pasta de Saída"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Preferences"
-msgstr "Preferências"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "General"
-msgstr "Geral"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Models"
-msgstr "Modelos"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Shortcuts"
-msgstr "Atalhos"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Folder Watch"
-msgstr "Monitorar Pasta"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Group"
-msgstr "Grupo"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Huggingface ID of a Faster whisper model"
-msgstr "ID Huggingface de um modelo Faster Whisper"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download"
-msgstr "Baixar"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Show file location"
-msgstr "Mostrar local do arquivo"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Delete"
-msgstr "Excluir"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Downloaded"
-msgstr "Baixado"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Available for Download"
-msgstr "Disponível para Download"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download link to Whisper.cpp ggml model file"
-msgstr "Link para o arquivo de modelo Whisper.cpp ggml"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Delete Model"
-msgstr "Excluir Modelo"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Are you sure you want to delete the selected model?"
-msgstr "Tem certeza que deseja excluir o modelo selecionado?"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download failed"
-msgstr "Falha ao baixar"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcription_tasks_table_widget.py
-#: buzz/widgets/update_dialog.py buzz/widgets/main_window.py
-#: buzz/model_loader.py
-msgid "Error"
-msgstr "Erro"
-
-#: buzz/widgets/record_button.py buzz/widgets/main_window_toolbar.py
-msgid "Record"
-msgstr "Gravar"
-
-#: buzz/widgets/record_button.py
-msgid "Stop"
-msgstr "Parar"
-
-#: buzz/widgets/transcriber/languages_combo_box.py
-#: buzz/transcriber/transcriber.py
-msgid "Detect Language"
-msgstr "Detectar Idioma"
-
-#: buzz/widgets/transcriber/mms_language_line_edit.py
-msgid "e.g., eng, fra, deu"
-msgstr "ex.: eng, fra, deu"
-
-#: buzz/widgets/transcriber/mms_language_line_edit.py
-msgid ""
-"Enter an ISO 639-3 language code (3 letters).\n"
-"Examples: eng (English), fra (French), deu (German),\n"
-"spa (Spanish), lav (Latvian)"
-msgstr ""
-"Insira um código de idioma ISO 639-3 (3 letras).\n"
-"Exemplos: eng (Inglês), fra (Francês), deu (Alemão),\n"
-"spa (Espanhol), lav (Letão)"
-
-#: buzz/widgets/transcriber/file_transcriber_widget.py
-msgid "Run"
-msgstr "Executar"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Model:"
-msgstr "Modelo:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-#: buzz/transcriber/recording_transcriber.py
-msgid "First time use of a model may take up to several minutest to load."
-msgstr "O primeiro uso de um modelo pode levar vários minutos para carregar."
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Api Key:"
-msgstr "Chave API:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Task:"
-msgstr "Tarefa:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Language:"
-msgstr "Idioma:"
-
-#: buzz/widgets/transcriber/initial_prompt_text_edit.py
-msgid "Enter prompt..."
-msgstr "Digite um prompt..."
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Advanced Settings"
-msgstr "Configurações Avançadas"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Speech recognition settings"
-msgstr "Configurações de reconhecimento de fala"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Initial Prompt:"
-msgstr "Prompt Inicial:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Translation settings"
-msgstr "Configurações de tradução"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Enable AI translation"
-msgstr "Habilitar tradução por IA"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "AI model:"
-msgstr "Modelo de IA:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid ""
-"Please translate each text sent to you from English to Spanish. Translation "
-"will be used in an automated system, please do not add any comments or "
-"notes, just the translation."
-msgstr ""
-"Por favor, traduza cada texto enviado a você do inglês para o espanhol. A "
-"tradução será usada em um sistema automatizado, portanto, não adicione "
-"comentários ou notas, apenas a tradução."
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Instructions for AI:"
-msgstr "Instruções para a IA:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Recording settings"
-msgstr "Configurações de gravação"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Silence threshold:"
-msgstr "Limiar de silêncio:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Live recording mode:"
-msgstr "Modo de gravação ao vivo:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Line separator:"
-msgstr "Separador de linha:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Transcription step:"
-msgstr "Etapa de transcrição:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Hide unconfirmed"
-msgstr "Ocultar não confirmado"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Enable live recording export"
-msgstr "Habilitar exportação de gravação ao vivo"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export folder:"
-msgstr "Pasta de exportação:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export file name:"
-msgstr "Nome do arquivo de exportação:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Text file (.txt)"
-msgstr "Arquivo de texto (.txt)"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "CSV (.csv)"
-msgstr "CSV (.csv)"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export file type:"
-msgstr "Tipo de arquivo de exportação:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid ""
-"Limit export entries\n"
-"(0 = export all):"
-msgstr ""
-"Limitar entradas de exportação\n"
-"(0 = exportar tudo):"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Word-level timings"
-msgstr "Tempos em nível de palavra"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Extract speech"
-msgstr "Extrair fala"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Export:"
-msgstr "Exportar:"
-
-#: buzz/widgets/transcriber/hugging_face_search_line_edit.py
-msgid "Huggingface ID of a model"
-msgstr "ID Huggingface de um modelo"
-
-#: buzz/widgets/transcriber/advanced_settings_button.py
-msgid "Advanced..."
-msgstr "Avançado..."
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "New File Transcription"
-msgstr "Nova Transcrição de Arquivo"
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "New URL Transcription"
-msgstr "Nova Transcrição de URL"
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "Open Transcript"
-msgstr "Abrir Transcrição"
-
-#: buzz/widgets/main_window_toolbar.py buzz/settings/shortcut.py
-msgid "Cancel Transcription"
-msgstr "Cancelar Transcrição"
-
-#: buzz/widgets/main_window_toolbar.py buzz/widgets/main_window.py
-#: buzz/settings/shortcut.py
-msgid "Clear History"
-msgstr "Limpar Histórico"
-
-#: buzz/widgets/main_window_toolbar.py buzz/widgets/update_dialog.py
-msgid "Update Available"
-msgstr "Atualização Disponível"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "In Progress"
-msgstr "Em Progresso"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Completed"
-msgstr "Concluído"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Failed"
-msgstr "Falhou"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Canceled"
-msgstr "Cancelado"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Queued"
-msgstr "Na fila"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "File Name / URL"
-msgstr "Nome do Arquivo / URL"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Model"
-msgstr "Modelo"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Task"
-msgstr "Tarefa"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Status"
-msgstr "Status"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Date Completed"
-msgstr "Data de Conclusão"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Date Added"
-msgstr "Data de Adição"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Notes"
-msgstr "Notas"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Reset Column Order"
-msgstr "Redefinir Ordem das Colunas"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Restart Transcription"
-msgstr "Reiniciar Transcrição"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Rename"
-msgstr "Renomear"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Add/Edit Notes"
-msgstr "Adicionar/Editar Notas"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Rename Transcription"
-msgstr "Renomear Transcrição"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Enter new name:"
-msgstr "Digite o novo nome:"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Enter some relevant notes for this transcription:"
-msgstr "Digite algumas notas relevantes para esta transcrição:"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Cannot Restart"
-msgstr "Não é possível reiniciar"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Only failed or canceled transcriptions can be restarted."
-msgstr "Somente transcrições com falha ou canceladas podem ser reiniciadas."
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Failed to restart transcription: {}"
-msgstr "Falha ao reiniciar a transcrição: {}"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid ""
-"Could not restart transcription: model not available and could not be "
-"downloaded."
-msgstr ""
-"Não foi possível reiniciar a transcrição: o modelo não está disponível e não "
-"pôde ser baixado."
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Could not restart transcription: transcriber worker not found."
-msgstr ""
-"Não foi possível reiniciar a transcrição: trabalhador de transcrição não "
-"encontrado."
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Live Recording"
-msgstr "Gravação ao Vivo"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Click Record to begin..."
-msgstr "Clique em Gravar para começar..."
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Waiting for AI translation..."
-msgstr "Aguardando tradução da IA..."
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Microphone:"
-msgstr "Microfone:"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Show in new window"
-msgstr "Mostrar em nova janela"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Text Size:"
-msgstr "Tamanho do Texto:"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Theme"
-msgstr "Tema"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Light"
-msgstr "Claro"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Dark"
-msgstr "Escuro"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Custom"
-msgstr "Personalizado"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Text Color"
-msgstr "Cor do Texto"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Background Color"
-msgstr "Cor de Fundo"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Fullscreen"
-msgstr "Tela Cheia"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy"
-msgstr "Copiar"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy transcription to clipboard"
-msgstr "Copiar transcrição para a área de transferência"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Nothing to copy!"
-msgstr "Nada para copiar!"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy failed"
-msgstr "Falha ao copiar"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copied!"
-msgstr "Copiado!"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Select Text Color"
-msgstr "Selecionar Cor do Texto"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Select Background Color"
-msgstr "Selecionar Cor de Fundo"
-
-#: buzz/widgets/recording_transcriber_widget.py
-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
-msgid ""
-"Please check your audio devices or check the application logs for more "
-"information."
-msgstr ""
-"Verifique seus dispositivos de áudio ou os logs do aplicativo para mais "
-"informações."
-
-#: buzz/widgets/update_dialog.py
-msgid "A new version of Buzz is available!"
-msgstr "Uma nova versão do Buzz está disponível!"
-
-#: buzz/widgets/update_dialog.py
-msgid "Current version:"
-msgstr "Versão atual:"
-
-#: buzz/widgets/update_dialog.py
-msgid "New version:"
-msgstr "Nova versão:"
-
-#: buzz/widgets/update_dialog.py
-msgid "Release Notes:"
-msgstr "Notas de Versão:"
-
-#: buzz/widgets/update_dialog.py
-msgid "Download and Install"
-msgstr "Baixar e instalar"
-
-#: buzz/widgets/update_dialog.py
-msgid "No download URL available for your platform."
-msgstr "Nenhuma URL de download disponível para sua plataforma."
-
-#: buzz/widgets/update_dialog.py
-msgid "Downloading file {} of {}..."
-msgstr "Baixando arquivo {} de {}..."
-
-#: buzz/widgets/update_dialog.py
-msgid "Downloading file {} of {} ({:.1f} MB / {:.1f} MB)..."
-msgstr "Baixando arquivo {} de {} ({:.1f} MB / {:.1f} MB)..."
-
-#: buzz/widgets/update_dialog.py
-msgid "Download Failed"
-msgstr "Falha no download"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to download the update: {}"
-msgstr "Falha ao baixar a atualização: {}"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to save the installer: {}"
-msgstr "Falha ao salvar o instalador: {}"
-
-#: buzz/widgets/update_dialog.py
-msgid "Download complete!"
-msgstr "Download concluído!"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to run the installer: {}"
-msgstr "Falha ao executar o instalador: {}"
-
-#: buzz/widgets/about_dialog.py
-msgid "Check for updates"
-msgstr "Verificar atualizações"
-
-#: buzz/widgets/about_dialog.py
-msgid "Show logs"
-msgstr "Mostrar logs"
-
-#: buzz/widgets/about_dialog.py
-msgid "You're up to date!"
-msgstr "Você está atualizado!"
-
-#: buzz/widgets/audio_meter_widget.py
-msgid "Average volume"
-msgstr "Volume médio"
-
-#: buzz/widgets/audio_meter_widget.py
-msgid "Queue"
-msgstr "Fila"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-msgid "Start"
-msgstr "Início"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-msgid "End"
-msgstr "Fim"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Text"
-msgstr "Texto"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Translation"
-msgstr "Tradução"
-
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-msgid "View"
-msgstr "Visualizar"
-
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-msgid "Timestamps"
-msgstr "Marcações de tempo"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Export"
-msgstr "Exportar"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Translate"
-msgstr "Traduzir"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Resize"
-msgstr "Redimensionar"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Identify Speakers"
-msgstr "Identificar Falantes"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Find"
-msgstr "Procurar"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Show/Hide Search Bar (Ctrl+F)"
-msgstr "Mostrar/Ocultar a Barra de Pesquisa"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Find:"
-msgstr "Procurar:"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Enter text to find..."
-msgstr "Digite o texto a procurar..."
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Previous match (Shift+Enter)"
-msgstr "Encontro prévio (Shift+Enter)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Next match (Ctrl+Enter)"
-msgstr "Próximo encontro (Ctrl+Enter)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Clear"
-msgstr "Limpar"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Playback Controls:"
-msgstr "Controles de Reprodução:"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Loop Segment"
-msgstr "Segmento de Loop"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Enable/disable looping when clicking on transcript segments"
-msgstr "Habilitar/desabilitar loop ao clicar em segmentos de transcrição"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Follow Audio"
-msgstr "Siga o Áudio"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid ""
-"Enable/disable following the current audio position in the transcript. When "
-"enabled, automatically scrolls to current text."
-msgstr ""
-"Ativar/desativar a opção de seguir a posição atual do áudio na transcrição. "
-"Quando ativado, rola automaticamente para o texto atual."
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Scroll to Current"
-msgstr "Rolar para o Atual"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Scroll to the currently spoken text"
-msgstr "Role até o texto falado no momento"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "1 of 100+ matches"
-msgstr "1 de 100+ encontros"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "1 of "
-msgstr "1 de "
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " matches"
-msgstr " encontros"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "No matches found"
-msgstr "Nada encontrado"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " of 100+ matches"
-msgstr " de 100+ encontros"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " of "
-msgstr " de "
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "API Key Required"
-msgstr "Chave API Necessária"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Please enter OpenAI API Key in preferences"
-msgstr "Insira a chave API OpenAI nas preferências"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend end time"
-msgstr "Estender o tempo final"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend endings by up to (seconds)"
-msgstr "Estender finais em até (segundos)"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend endings"
-msgstr "Estender finais"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Resize Options"
-msgstr "Opções de Redimensionamento"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Desired subtitle length"
-msgstr "Duração desejada da legenda"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Available only if word level timings were disabled during transcription"
-msgstr ""
-"Disponível apenas se os tempos em nível de palavra foram desabilitados "
-"durante a transcrição"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge Options"
-msgstr "Opções de Mesclagem"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge by gap"
-msgstr "Mesclar por intervalo"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Split by punctuation"
-msgstr "Dividir por pontuação"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Split by max length"
-msgstr "Dividir por tamanho máximo"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge"
-msgstr "Mesclar"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Available only if word level timings were enabled during transcription"
-msgstr ""
-"Disponível apenas se os tempos em nível de palavra foram habilitados durante "
-"a transcrição"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid ""
-"Speaker identification is not available: failed to load required libraries."
-msgstr ""
-"Identificação de falantes não está disponível: falha ao carregar as "
-"bibliotecas necessárias."
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "1/8 Collecting transcripts"
-msgstr "1/8 Coletando transcrições"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "2/8 Loading audio"
-msgstr "2/8 Carregando áudio"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "3/8 Loading alignment model"
-msgstr "3/8 Carregando modelo de alinhamento"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "3/8 Loading alignment model (retrying with cache...)"
-msgstr "3/8 Carregando modelo de alinhamento (tentando novamente com cache...)"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid ""
-"Failed to load alignment model. Please check your internet connection and "
-"try again."
-msgstr ""
-"Falha ao carregar o modelo de alinhamento. Verifique sua conexão com a "
-"internet e tente novamente."
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "4/8 Processing audio"
-msgstr "4/8 Processando áudio"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "5/8 Preparing transcripts"
-msgstr "5/8 Preparando transcrições"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "6/8 Identifying speakers"
-msgstr "6/8 Identificando falantes"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "7/8 Mapping speakers to transcripts"
-msgstr "7/8 Mapeando falantes para transcrições"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "8/8 Identification done"
-msgstr "8/8 Identificação concluída"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "0/0 Error identifying speakers"
-msgstr "0/0 Erro ao identificar falantes"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Step 1: Identify speakers"
-msgstr "Passo 1: Identificar falantes"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Identify"
-msgstr "Identificar"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Ready to identify speakers"
-msgstr "Pronto para identificar falantes"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Audio file not found"
-msgstr "Arquivo de áudio não encontrado"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Step 2: Name speakers"
-msgstr "Passo 2: Nomear falantes"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Play sample"
-msgstr "Reproduzir amostra"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Merge speaker sentences"
-msgstr "Mesclar frases do falante"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Save"
-msgstr "Salvar"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Cancelling..."
-msgstr "Cancelando..."
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Cancelled"
-msgstr "Cancelado"
-
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Save File"
-msgstr "Salvar Arquivo"
-
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Text files"
-msgstr "Arquivos de texto"
-
-#: buzz/widgets/model_download_progress_dialog.py
-msgid "Downloading model"
-msgstr "Baixando modelo"
-
-#: buzz/widgets/model_download_progress_dialog.py
-msgid "remaining"
-msgstr "restante"
-
-#: buzz/widgets/menu_bar.py
-msgid "Import File..."
-msgstr "Importar Arquivo..."
-
-#: buzz/widgets/menu_bar.py
-msgid "Import URL..."
-msgstr "Importar URL..."
-
-#: buzz/widgets/menu_bar.py
-msgid "Import Folder..."
-msgstr "Importar Pasta..."
-
-#: buzz/widgets/menu_bar.py
-msgid "About"
-msgstr "Sobre"
-
-#: buzz/widgets/menu_bar.py
-msgid "Preferences..."
-msgstr "Preferências..."
-
-#: buzz/widgets/menu_bar.py
-msgid "Help"
-msgstr "Ajuda"
-
-#: buzz/widgets/menu_bar.py
-msgid "File"
-msgstr "Arquivo"
-
-#: buzz/widgets/main_window.py
-msgid ""
-"Are you sure you want to delete the selected transcription(s)? This action "
-"cannot be undone."
-msgstr ""
-"Tem certeza que deseja excluir a(s) transcrição(ões) selecionada(s)? Esta "
-"ação não pode ser desfeita."
-
-#: buzz/widgets/main_window.py
-msgid "Select audio file"
-msgstr "Selecionar arquivo de áudio"
-
-#: buzz/widgets/main_window.py
-msgid "Select folder"
-msgstr "Selecionar Pasta de Entrada"
-
-#: buzz/widgets/main_window.py
-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
-#: buzz/transcriber/recording_transcriber.py
-msgid "Whisper server failed to start. Check logs for details."
-msgstr "Falha ao iniciar o servidor Whisper. Verifique os logs."
-
-#: buzz/transcriber/local_whisper_cpp_server_transcriber.py
-#: buzz/transcriber/recording_transcriber.py
-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 ""
-"O servidor Whisper falhou ao iniciar devido à memória insuficiente. Tente "
-"novamente com um modelo menor. Para forçar o modo CPU, use a variável de "
-"ambiente BUZZ_FORCE_CPU=TRUE."
-
-#: buzz/transcriber/transcriber.py
-msgid "Translate to English"
-msgstr "Traduzir para Inglês"
-
-#: buzz/transcriber/transcriber.py
-msgid "Transcribe"
-msgstr "Transcrever"
-
-#: buzz/transcriber/transcriber.py
-msgid "Chinese"
-msgstr "Chinês"
-
-#: buzz/transcriber/transcriber.py
-msgid "Russian"
-msgstr "Russo"
-
-#: buzz/transcriber/transcriber.py
-msgid "Korean"
-msgstr "Coreano"
-
-#: buzz/transcriber/transcriber.py
-msgid "French"
-msgstr "Francês"
-
-#: buzz/transcriber/transcriber.py
-msgid "Portuguese"
-msgstr "Português"
-
-#: buzz/transcriber/transcriber.py
-msgid "Turkish"
-msgstr "Turco"
-
-#: buzz/transcriber/transcriber.py
-msgid "Arabic"
-msgstr "Árabe"
-
-#: buzz/transcriber/transcriber.py
-msgid "Swedish"
-msgstr "Sueco"
-
-#: buzz/transcriber/transcriber.py
-msgid "Indonesian"
-msgstr "Indonésio"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hindi"
-msgstr "Híndi"
-
-#: buzz/transcriber/transcriber.py
-msgid "Finnish"
-msgstr "Finlandês"
-
-#: buzz/transcriber/transcriber.py
-msgid "Vietnamese"
-msgstr "Vietnamita"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hebrew"
-msgstr "Hebraico"
-
-#: buzz/transcriber/transcriber.py
-msgid "Greek"
-msgstr "Grego"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malay"
-msgstr "Malaio"
-
-#: buzz/transcriber/transcriber.py
-msgid "Czech"
-msgstr "Tcheco"
-
-#: buzz/transcriber/transcriber.py
-msgid "Romanian"
-msgstr "Romeno"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hungarian"
-msgstr "Húngaro"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tamil"
-msgstr "Tâmil"
-
-#: buzz/transcriber/transcriber.py
-msgid "Norwegian"
-msgstr "Norueguês"
-
-#: buzz/transcriber/transcriber.py
-msgid "Thai"
-msgstr "Tailandês"
-
-#: buzz/transcriber/transcriber.py
-msgid "Urdu"
-msgstr "Urdu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Croatian"
-msgstr "Croata"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bulgarian"
-msgstr "Búlgaro"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lithuanian"
-msgstr "Lituano"
-
-#: buzz/transcriber/transcriber.py
-msgid "Latin"
-msgstr "Latim"
-
-#: buzz/transcriber/transcriber.py
-msgid "Maori"
-msgstr "Maori"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malayalam"
-msgstr "Malaiala"
-
-#: buzz/transcriber/transcriber.py
-msgid "Welsh"
-msgstr "Galês"
-
-#: buzz/transcriber/transcriber.py
-msgid "Slovak"
-msgstr "Eslovaco"
-
-#: buzz/transcriber/transcriber.py
-msgid "Telugu"
-msgstr "Telugu"
-
-#: buzz/transcriber/transcriber.py
-msgid "Persian"
-msgstr "Persa"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bengali"
-msgstr "Bengali"
-
-#: buzz/transcriber/transcriber.py
-msgid "Serbian"
-msgstr "Sérvio"
-
-#: buzz/transcriber/transcriber.py
-msgid "Azerbaijani"
-msgstr "Azerbaijano"
-
-#: buzz/transcriber/transcriber.py
-msgid "Slovenian"
-msgstr "Esloveno"
-
-#: buzz/transcriber/transcriber.py
-msgid "Kannada"
-msgstr "Canarês"
-
-#: buzz/transcriber/transcriber.py
-msgid "Estonian"
-msgstr "Estoniano"
-
-#: buzz/transcriber/transcriber.py
-msgid "Macedonian"
-msgstr "Macedônio"
-
-#: buzz/transcriber/transcriber.py
-msgid "Breton"
-msgstr "Bretão"
-
-#: buzz/transcriber/transcriber.py
-msgid "Basque"
-msgstr "Basco"
-
-#: buzz/transcriber/transcriber.py
-msgid "Icelandic"
-msgstr "Islandês"
-
-#: buzz/transcriber/transcriber.py
-msgid "Armenian"
-msgstr "Armênio"
-
-#: buzz/transcriber/transcriber.py
-msgid "Nepali"
-msgstr "Nepalês"
-
-#: buzz/transcriber/transcriber.py
-msgid "Mongolian"
-msgstr "Mongol"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bosnian"
-msgstr "Bósnio"
-
-#: buzz/transcriber/transcriber.py
-msgid "Kazakh"
-msgstr "Cazaque"
-
-#: buzz/transcriber/transcriber.py
-msgid "Albanian"
-msgstr "Albanês"
-
-#: buzz/transcriber/transcriber.py
-msgid "Swahili"
-msgstr "Suaíli"
-
-#: buzz/transcriber/transcriber.py
-msgid "Galician"
-msgstr "Galego"
-
-#: buzz/transcriber/transcriber.py
-msgid "Marathi"
-msgstr "Marathi"
-
-#: buzz/transcriber/transcriber.py
-msgid "Punjabi"
-msgstr "Panjabi"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sinhala"
-msgstr "Cingalês"
-
-#: buzz/transcriber/transcriber.py
-msgid "Khmer"
-msgstr "Khmer"
-
-#: buzz/transcriber/transcriber.py
-msgid "Shona"
-msgstr "Shona"
-
-#: buzz/transcriber/transcriber.py
-msgid "Yoruba"
-msgstr "Iorubá"
-
-#: buzz/transcriber/transcriber.py
-msgid "Somali"
-msgstr "Somali"
-
-#: buzz/transcriber/transcriber.py
-msgid "Afrikaans"
-msgstr "Africâner"
-
-#: buzz/transcriber/transcriber.py
-msgid "Occitan"
-msgstr "Occitano"
-
-#: buzz/transcriber/transcriber.py
-msgid "Georgian"
-msgstr "Georgiano"
-
-#: buzz/transcriber/transcriber.py
-msgid "Belarusian"
-msgstr "Bielorrusso"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tajik"
-msgstr "Tajique"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sindhi"
-msgstr "Sindi"
-
-#: buzz/transcriber/transcriber.py
-msgid "Gujarati"
-msgstr "Gujarati"
-
-#: buzz/transcriber/transcriber.py
-msgid "Amharic"
-msgstr "Amárico"
-
-#: buzz/transcriber/transcriber.py
-msgid "Yiddish"
-msgstr "Ídiche"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lao"
-msgstr "Laosiano"
-
-#: buzz/transcriber/transcriber.py
-msgid "Uzbek"
-msgstr "Uzbeque"
-
-#: buzz/transcriber/transcriber.py
-msgid "Faroese"
-msgstr "Feroês"
-
-#: buzz/transcriber/transcriber.py
-msgid "Haitian Creole"
-msgstr "Crioulo Haitiano"
-
-#: buzz/transcriber/transcriber.py
-msgid "Pashto"
-msgstr "Afegão"
-
-#: buzz/transcriber/transcriber.py
-msgid "Turkmen"
-msgstr "Turcomeno"
-
-#: buzz/transcriber/transcriber.py
-msgid "Nynorsk"
-msgstr "Novo Norueguês"
-
-#: buzz/transcriber/transcriber.py
-msgid "Maltese"
-msgstr "Maltês"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sanskrit"
-msgstr "Sânscrito"
-
-#: buzz/transcriber/transcriber.py
-msgid "Luxembourgish"
-msgstr "Luxemburguês"
-
-#: buzz/transcriber/transcriber.py
-msgid "Myanmar"
-msgstr "Birmanês"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tibetan"
-msgstr "Tibetano"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tagalog"
-msgstr "Tagalo"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malagasy"
-msgstr "Malgaxe"
-
-#: buzz/transcriber/transcriber.py
-msgid "Assamese"
-msgstr "Assamês"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tatar"
-msgstr "Tártaro"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hawaiian"
-msgstr "Havaiano"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lingala"
-msgstr "Lingala"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hausa"
-msgstr "Hauçá"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bashkir"
-msgstr "Bashkir"
-
-#: buzz/transcriber/transcriber.py
-msgid "Javanese"
-msgstr "Javanês"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sundanese"
-msgstr "Sundanês"
-
-#: buzz/transcriber/transcriber.py
-msgid "Cantonese"
-msgstr "Cantonês"
-
-#: buzz/transcriber/recording_transcriber.py buzz/model_loader.py
-msgid "A connection error occurred"
-msgstr "Ocorreu um erro de conexão"
-
-#: buzz/transcriber/recording_transcriber.py
-msgid "Starting Whisper.cpp..."
-msgstr "Iniciando Whisper.cpp..."
-
-#: buzz/transcriber/recording_transcriber.py
-msgid "Starting transcription..."
-msgstr "Iniciando transcrição..."
-
-#: buzz/settings/shortcut.py
-msgid "Open Record Window"
-msgstr "Abrir Janela de Gravação"
-
-#: buzz/settings/shortcut.py
-msgid "Import File"
-msgstr "Importar Arquivo"
-
-#: buzz/settings/shortcut.py
-msgid "Open Preferences Window"
-msgstr "Abrir Janela de Preferências"
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Text"
-msgstr "Ver Texto da Transcrição"
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Translation"
-msgstr "Ver Tradução da Transcrição"
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Timestamps"
-msgstr "Ver Marcações de Tempo da Transcrição"
-
-#: buzz/settings/shortcut.py
-msgid "Search Transcript"
-msgstr "Pesquisar Transcrição"
-
-#: buzz/settings/shortcut.py
-msgid "Go to Next Transcript Search Result"
-msgstr "Ir para o Próximo Resultado de Pesquisa na Transcrição"
-
-#: buzz/settings/shortcut.py
-msgid "Go to Previous Transcript Search Result"
-msgstr "Ir para o Resultado de Pesquisa Anterior na Transcrição"
-
-#: buzz/settings/shortcut.py
-msgid "Scroll to Current Text"
-msgstr "Role até o Texto Atual"
-
-#: buzz/settings/shortcut.py
-msgid "Play/Pause Audio"
-msgstr "Tocar/Pausar o Áudio"
-
-#: buzz/settings/shortcut.py
-msgid "Replay Current Segment"
-msgstr "Repetir o Segmento Atual"
-
-#: buzz/settings/shortcut.py
-msgid "Toggle Playback Controls"
-msgstr "Alternar Controles de Reprodução"
-
-#: buzz/settings/shortcut.py
-msgid "Decrease Segment Start Time"
-msgstr "Diminuir o Inicio do Segmento"
-
-#: buzz/settings/shortcut.py
-msgid "Increase Segment Start Time"
-msgstr "Aumentar o Início do Segmento"
-
-#: buzz/settings/shortcut.py
-msgid "Decrease Segment End Time"
-msgstr "Diminuir o Final do Segmento"
-
-#: buzz/settings/shortcut.py
-msgid "Increase Segment End Time"
-msgstr "Estender o Final do Segmento"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append below"
-msgstr "Acrescentar abaixo"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append above"
-msgstr "Acrescentar acima"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append and correct"
-msgstr "Acrescentar e corrigir"
-
-#: buzz/file_transcriber_queue_worker.py
-msgid ""
-"Speech extraction failed! Check your internet connection — a model may need "
-"to be downloaded."
-msgstr ""
-"Falha na extração de fala! Verifique sua conexão com a internet — pode ser "
-"necessário baixar um modelo."
-
-#~ msgid "Comma-separated, e.g. \"0.0, 0.2, 0.4, 0.6, 0.8, 1.0\""
-#~ msgstr "Separado por vírgulas, ex: \"0.0, 0.2, 0.4, 0.6, 0.8, 1.0\""
-
-#~ msgid "Temperature:"
-#~ msgstr "Temperatura:"
-
-#~ msgid "Please translate each text sent to you from English to Spanish."
-#~ msgstr ""
-#~ "Por favor, traduza cada texto enviado a você do Inglês para o Espanhol."
-
-#~ msgid "Translation error, see logs!"
-#~ msgstr "Erro de tradução, verifique os logs!"
-
-#~ msgid "Snap permission notice"
-#~ msgstr "Aviso de permissão do Snap"
-
-#~ msgid ""
-#~ "Detected missing permissions, please check that snap permissions have "
-#~ "been granted"
-#~ msgstr ""
-#~ "Permissões ausentes detectadas, verifique se as permissões do Snap foram "
-#~ "concedidas"
-
-#~ msgid ""
-#~ "To enable necessary permissions run the following commands in the terminal"
-#~ msgstr ""
-#~ "Para habilitar as permissões necessárias, execute os seguintes comandos "
-#~ "no terminal"
-
-#~ msgid "Close"
-#~ msgstr "Fechar"
-
-#~ msgid "Enter instructions for AI on how to translate..."
-#~ msgstr "Instrua a IA sobre como traduzir..."
-
-#~ msgid "ID"
-#~ msgstr "Id."
-
-#~ msgid "Undo"
-#~ msgstr "Desfazer"
diff --git a/buzz/locale/uk_UA/LC_MESSAGES/buzz.po b/buzz/locale/uk_UA/LC_MESSAGES/buzz.po
deleted file mode 100644
index 0c671bd5..00000000
--- a/buzz/locale/uk_UA/LC_MESSAGES/buzz.po
+++ /dev/null
@@ -1,1669 +0,0 @@
-msgid ""
-msgstr ""
-"Project-Id-Version: \n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2026-03-07 20:20+0200\n"
-"PO-Revision-Date: \n"
-"Last-Translator: Yevhen Popok \n"
-"Language-Team: \n"
-"Language: uk_UA\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=4; plural=n==1 ? 3 : n%10==1 && n%100!=11 ? 0 : "
-"n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
-"X-Generator: Poedit 3.4.4\n"
-
-#: buzz/widgets/import_url_dialog.py buzz/settings/shortcut.py
-msgid "Import URL"
-msgstr "Імпортувати адресу"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "https://example.com/audio.mp3"
-msgstr "https://example.com/audio.mp3"
-
-#: buzz/widgets/import_url_dialog.py
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-#: buzz/widgets/main_window.py
-msgid "Ok"
-msgstr "Гаразд"
-
-#: buzz/widgets/import_url_dialog.py
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-#: buzz/widgets/model_download_progress_dialog.py buzz/widgets/main_window.py
-msgid "Cancel"
-msgstr "Скасувати"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "URL:"
-msgstr "Адреса:"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "Invalid URL"
-msgstr "Недійсна адреса"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "The URL you entered is invalid."
-msgstr "Адреса, яку ви ввели, є недійсною"
-
-#: buzz/widgets/presentation_window.py
-msgid "Live Transcript Presentation"
-msgstr "Презентація транскрипції в реальному часі"
-
-#: buzz/widgets/preferences_dialog/shortcuts_editor_preferences_widget.py
-msgid "Reset to Defaults"
-msgstr "Типові значення"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "English"
-msgstr "Англійська"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Catalan"
-msgstr "Каталонська"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Danish"
-msgstr "Датська"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Dutch"
-msgstr "Нідерландська"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "German"
-msgstr "Німецька"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Spanish"
-msgstr "Іспанська"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Italian"
-msgstr "Італійська"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Japanese"
-msgstr "Японська"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Latvian"
-msgstr "Латвійська"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Polish"
-msgstr "Польська"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Portuguese (Brazil)"
-msgstr "Португальська (Бразилія)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Ukrainian"
-msgstr "Українська"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Chinese (Simplified)"
-msgstr "Китайська (спрощена)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Chinese (Traditional)"
-msgstr "Китайська (традиційна)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Restart required!"
-msgstr "Потрібен перезапуск!"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Ui Language"
-msgstr "Мова інтерфейсу"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Font Size"
-msgstr "Розмір шрифту"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Test"
-msgstr "Тест"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API key"
-msgstr "API-ключ OpenAI"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI base url"
-msgstr "Базова адреса OpenAI"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API model"
-msgstr "Модель API OpenAI"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Default export file name"
-msgstr "Типова назва файлу експорту"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Enable live recording transcription export"
-msgstr "Увімкнути експорт транскрипції з живого запису"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Browse"
-msgstr "Огляд"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Export folder"
-msgstr "Тека для експорту"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Live recording mode"
-msgstr "Режим живого запису"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Note: Live recording export settings will be moved to the Advanced Settings "
-"in the Live Recording screen in a future version."
-msgstr ""
-"Примітка: Параметри експорту живого запису будуть перенесені до Додаткових "
-"налаштувань на екрані живого запису в майбутній версії."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Use 8-bit quantization to reduce memory usage"
-msgstr "Використовувати 8-бітне квантування для зменшення використання пам'яті"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Applies to Huggingface and Faster Whisper models. Reduces GPU memory usage "
-"but may slightly decrease transcription quality."
-msgstr ""
-"Застосовується до моделей Huggingface та Faster Whisper. Зменшує "
-"використання пам'яті GPU, але може дещо знизити якість транскрипції."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Reduce GPU RAM"
-msgstr "Зменшити RAM GPU"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Use only CPU and disable GPU acceleration"
-msgstr "Використовувати лише CPU та вимкнути прискорення GPU"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Set this if larger models do not fit your GPU memory and Buzz crashes"
-msgstr ""
-"Увімкніть це, якщо великі моделі не поміщаються в пам'ять GPU і Buzz падає"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Disable GPU"
-msgstr "Вимкнути GPU"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API Key Test"
-msgstr "Тест API-ключа OpenAI"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Your API key is valid. Buzz will use this key to perform Whisper API "
-"transcriptions and AI translations."
-msgstr ""
-"Ваш API-ключ дійсний. Buzz використає цей ключ для транскрипції з Whisper "
-"API та перекладу ШІ."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Invalid API key"
-msgstr "Недійсний API-ключ"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"API supports only base64 characters (A-Za-z0-9+/=_-). Other characters in "
-"API key may cause errors."
-msgstr ""
-"API підтримує лише символи base64 (A-Za-z0-9+/=_-). Інші символи в API-ключі "
-"можуть спричиняти помилки."
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Select Export Folder"
-msgstr "Виберіть теку для експорту"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"OpenAI API returned invalid response. Please check the API url or your key. "
-"Transcription and translation may still work if the API does not support key "
-"validation."
-msgstr ""
-"OpenAI API повернув недійсну відповідь. Будь ласка, перевірте адресу вашого "
-"API-ключа. Транскрипція та переклад можуть продовжити працювати, якщо API не "
-"підтримує перевірку ключа."
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Enable folder watch"
-msgstr "Увімкнути стеження за текою"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Delete processed files"
-msgstr "Видалити оброблені файли"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Input folder"
-msgstr "Тека введення"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Output folder"
-msgstr "Тека виведення"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Select Input Folder"
-msgstr "Виберіть теку введення"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Select Output Folder"
-msgstr "Виберіть теку виведення"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Preferences"
-msgstr "Налаштування"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "General"
-msgstr "Загальне"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Models"
-msgstr "Моделі"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Shortcuts"
-msgstr "Клавіатурні скорочення"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Folder Watch"
-msgstr "Нагляд за текою"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Group"
-msgstr "Група"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Huggingface ID of a Faster whisper model"
-msgstr "Huggingface ID для моделі Faster Whisper"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download"
-msgstr "Завантажити"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Show file location"
-msgstr "Показати розташування файлу"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Delete"
-msgstr "Видалити"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Downloaded"
-msgstr "Завантажене"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Available for Download"
-msgstr "Доступно для завантаження"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download link to Whisper.cpp ggml model file"
-msgstr "Посилання на завантаження файлу ggml моделі Whisper.cpp"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Delete Model"
-msgstr "Видалити модель"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Are you sure you want to delete the selected model?"
-msgstr "Ви впевнені, що хочете видалити вибрану модель?"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download failed"
-msgstr "Невдале завантаження"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcription_tasks_table_widget.py
-#: buzz/widgets/update_dialog.py buzz/widgets/main_window.py
-#: buzz/model_loader.py
-msgid "Error"
-msgstr "Помилка"
-
-#: buzz/widgets/record_button.py buzz/widgets/main_window_toolbar.py
-msgid "Record"
-msgstr "Записати"
-
-#: buzz/widgets/record_button.py
-msgid "Stop"
-msgstr "Зупинити"
-
-#: buzz/widgets/transcriber/languages_combo_box.py
-#: buzz/transcriber/transcriber.py
-msgid "Detect Language"
-msgstr "Визначити мову"
-
-#: buzz/widgets/transcriber/mms_language_line_edit.py
-msgid "e.g., eng, fra, deu"
-msgstr "напр., eng, fra, deu"
-
-#: buzz/widgets/transcriber/mms_language_line_edit.py
-msgid ""
-"Enter an ISO 639-3 language code (3 letters).\n"
-"Examples: eng (English), fra (French), deu (German),\n"
-"spa (Spanish), lav (Latvian)"
-msgstr ""
-"Введіть код мови ISO 639-3 (3 літери).\n"
-"Приклади: eng (Англійська), fra (Французька), deu (Німецька),\n"
-"spa (Іспанська), lav (Латвійська)"
-
-#: buzz/widgets/transcriber/file_transcriber_widget.py
-msgid "Run"
-msgstr "Запуск"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Model:"
-msgstr "Модель:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-#: buzz/transcriber/recording_transcriber.py
-msgid "First time use of a model may take up to several minutest to load."
-msgstr "Перше використання моделі може тривати кілька хвилин для завантаження."
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Api Key:"
-msgstr "API-ключ:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Task:"
-msgstr "Завдання:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Language:"
-msgstr "Мова:"
-
-#: buzz/widgets/transcriber/initial_prompt_text_edit.py
-msgid "Enter prompt..."
-msgstr "Введіть підказку..."
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Advanced Settings"
-msgstr "Додаткові налаштування"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Speech recognition settings"
-msgstr "Параметри розпізнавання мовлення"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Initial Prompt:"
-msgstr "Початкова підказка:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Translation settings"
-msgstr "Налаштування перекладу"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Enable AI translation"
-msgstr "Увімкнути переклад ШІ"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "AI model:"
-msgstr "Модель ШІ"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid ""
-"Please translate each text sent to you from English to Spanish. Translation "
-"will be used in an automated system, please do not add any comments or "
-"notes, just the translation."
-msgstr ""
-"Будь ласка, перекладайте кожен надісланий вам текст з англійської на "
-"іспанську. Переклад використовуватиметься в автоматизованій системі, тому, "
-"будь ласка, не додавайте жодних коментарів чи приміток, лише переклад."
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Instructions for AI:"
-msgstr "Інструкції для ШІ:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Recording settings"
-msgstr "Налаштування запису"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Silence threshold:"
-msgstr "Поріг тиші:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Live recording mode:"
-msgstr "Режим живого запису:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Line separator:"
-msgstr "Роздільник рядків:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Transcription step:"
-msgstr "Крок транскрибування:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Hide unconfirmed"
-msgstr "Приховати непідтверджене"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Enable live recording export"
-msgstr "Увімкнути експорт живого запису"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export folder:"
-msgstr "Тека для експорту:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export file name:"
-msgstr "Назва файлу експорту:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Text file (.txt)"
-msgstr "Текстовий файл (.txt)"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "CSV (.csv)"
-msgstr "CSV (.csv)"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export file type:"
-msgstr "Тип файлу експорту:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid ""
-"Limit export entries\n"
-"(0 = export all):"
-msgstr ""
-"Обмежити кількість записів експорту\n"
-"(0 = експортувати всі):"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Word-level timings"
-msgstr "Хронометраж на рівні слів"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Extract speech"
-msgstr "Витягти мовлення"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Export:"
-msgstr "Експорт:"
-
-#: buzz/widgets/transcriber/hugging_face_search_line_edit.py
-msgid "Huggingface ID of a model"
-msgstr "ID чи модель Huggingface"
-
-#: buzz/widgets/transcriber/advanced_settings_button.py
-msgid "Advanced..."
-msgstr "Додатково..."
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "New File Transcription"
-msgstr "Нова транскрипція файлу"
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "New URL Transcription"
-msgstr "Нова транскрипція за адресою"
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "Open Transcript"
-msgstr "Відкрити транскрипцію"
-
-#: buzz/widgets/main_window_toolbar.py buzz/settings/shortcut.py
-msgid "Cancel Transcription"
-msgstr "Скасувати транскрипцію"
-
-#: buzz/widgets/main_window_toolbar.py buzz/widgets/main_window.py
-#: buzz/settings/shortcut.py
-msgid "Clear History"
-msgstr "Очистити історію"
-
-#: buzz/widgets/main_window_toolbar.py buzz/widgets/update_dialog.py
-msgid "Update Available"
-msgstr "Доступне оновлення"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "In Progress"
-msgstr "В процесі"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Completed"
-msgstr "Завершено"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Failed"
-msgstr "Невдача"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Canceled"
-msgstr "Скасовано"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Queued"
-msgstr "У черзі"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "File Name / URL"
-msgstr "Назва файлу / посилання"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Model"
-msgstr "Модель"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Task"
-msgstr "Завдання"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Status"
-msgstr "Стан"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Date Completed"
-msgstr "Дата завершення"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Date Added"
-msgstr "Дата додавання"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Notes"
-msgstr "Нотатки"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Reset Column Order"
-msgstr "Скинути порядок стовпців"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Restart Transcription"
-msgstr "Перезапустити транскрипцію"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Rename"
-msgstr "Перейменувати"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Add/Edit Notes"
-msgstr "Додати/редагувати нотатки"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Rename Transcription"
-msgstr "Перейменувати транскрипцію"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Enter new name:"
-msgstr "Введіть нову назву:"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Enter some relevant notes for this transcription:"
-msgstr "Введіть відповідні нотатки для цієї транскрипції:"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Cannot Restart"
-msgstr "Неможливо перезапустити"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Only failed or canceled transcriptions can be restarted."
-msgstr "Перезапустити можна лише невдалі або скасовані транскрипції."
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Failed to restart transcription: {}"
-msgstr "Не вдалося перезапустити транскрипцію: {}"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid ""
-"Could not restart transcription: model not available and could not be "
-"downloaded."
-msgstr ""
-"Не вдалося перезапустити транскрипцію: модель недоступна та не може бути "
-"завантажена."
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Could not restart transcription: transcriber worker not found."
-msgstr ""
-"Не вдалося перезапустити транскрипцію: обробник транскрипції не знайдено."
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Live Recording"
-msgstr "Живий запис"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Click Record to begin..."
-msgstr "Натисніть на Запис, щоб розпочати..."
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Waiting for AI translation..."
-msgstr "Очікування перекладу від ШІ..."
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Microphone:"
-msgstr "Мікрофон:"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Show in new window"
-msgstr "Показати у новому вікні"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Text Size:"
-msgstr "Розмір тексту:"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Theme"
-msgstr "Тема"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Light"
-msgstr "Світла"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Dark"
-msgstr "Темна"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Custom"
-msgstr "Власна"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Text Color"
-msgstr "Колір тексту"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Background Color"
-msgstr "Колір фону"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Fullscreen"
-msgstr "Повноекранний режим"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy"
-msgstr "Копіювати"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy transcription to clipboard"
-msgstr "Копіювати транскрипцію до буфера обміну"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Nothing to copy!"
-msgstr "Нічого копіювати!"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy failed"
-msgstr "Не вдалося скопіювати"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copied!"
-msgstr "Скопійовано!"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Select Text Color"
-msgstr "Виберіть колір тексту"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Select Background Color"
-msgstr "Виберіть колір фону"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "An error occurred while starting a new recording:"
-msgstr "При старті нового запису виникла помилка:"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid ""
-"Please check your audio devices or check the application logs for more "
-"information."
-msgstr ""
-"Будь ласка, перевірте свої аудіопристрої або пошукайте додаткову інформацію "
-"в звітах програми."
-
-#: buzz/widgets/update_dialog.py
-msgid "A new version of Buzz is available!"
-msgstr "Доступна нова версія Buzz!"
-
-#: buzz/widgets/update_dialog.py
-msgid "Current version:"
-msgstr "Поточна версія:"
-
-#: buzz/widgets/update_dialog.py
-msgid "New version:"
-msgstr "Нова версія:"
-
-#: buzz/widgets/update_dialog.py
-msgid "Release Notes:"
-msgstr "Примітки до випуску:"
-
-#: buzz/widgets/update_dialog.py
-msgid "Download and Install"
-msgstr "Завантажити та встановити"
-
-#: buzz/widgets/update_dialog.py
-msgid "No download URL available for your platform."
-msgstr "Посилання для завантаження для вашої платформи відсутнє."
-
-#: buzz/widgets/update_dialog.py
-msgid "Downloading file {} of {}..."
-msgstr "Завантаження файлу {} з {}..."
-
-#: buzz/widgets/update_dialog.py
-msgid "Downloading file {} of {} ({:.1f} MB / {:.1f} MB)..."
-msgstr "Завантаження файлу {} з {} ({:.1f} МБ / {:.1f} МБ)..."
-
-#: buzz/widgets/update_dialog.py
-msgid "Download Failed"
-msgstr "Помилка завантаження"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to download the update: {}"
-msgstr "Не вдалося завантажити оновлення: {}"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to save the installer: {}"
-msgstr "Не вдалося зберегти інсталятор: {}"
-
-#: buzz/widgets/update_dialog.py
-msgid "Download complete!"
-msgstr "Завантаження завершено!"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to run the installer: {}"
-msgstr "Не вдалося запустити інсталятор: {}"
-
-#: buzz/widgets/about_dialog.py
-msgid "Check for updates"
-msgstr "Перевірити оновлення"
-
-#: buzz/widgets/about_dialog.py
-msgid "Show logs"
-msgstr "Показати журнали"
-
-#: buzz/widgets/about_dialog.py
-msgid "You're up to date!"
-msgstr "У вас актуальна версія!"
-
-#: buzz/widgets/audio_meter_widget.py
-msgid "Average volume"
-msgstr "Середній рівень гучності"
-
-#: buzz/widgets/audio_meter_widget.py
-msgid "Queue"
-msgstr "Черга"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-msgid "Start"
-msgstr "Початок"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-msgid "End"
-msgstr "Кінець"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Text"
-msgstr "Текст"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Translation"
-msgstr "Переклад"
-
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-msgid "View"
-msgstr "Вигляд"
-
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-msgid "Timestamps"
-msgstr "Позначки часу"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Export"
-msgstr "Експорт"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Translate"
-msgstr "Перекласти"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Resize"
-msgstr "Змінити розмір"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Identify Speakers"
-msgstr "Визначити мовців"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Find"
-msgstr "Знайти"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Show/Hide Search Bar (Ctrl+F)"
-msgstr "Показати/сховати панель пошуку (Ctrl+F)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Find:"
-msgstr "Знайти:"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Enter text to find..."
-msgstr "Введіть текст для пошуку..."
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Previous match (Shift+Enter)"
-msgstr "Попередній збіг (Shift+Enter)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Next match (Ctrl+Enter)"
-msgstr "Наступний збіг (Ctrl+Enter)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Clear"
-msgstr "Очистити"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Playback Controls:"
-msgstr "Керування відтворенням:"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Loop Segment"
-msgstr "Повторювати фрагмент"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Enable/disable looping when clicking on transcript segments"
-msgstr "Увімкнути/вимкнути повторення при натисканні на фрагменти транскрипції"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Follow Audio"
-msgstr "Стежити за аудіо"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid ""
-"Enable/disable following the current audio position in the transcript. When "
-"enabled, automatically scrolls to current text."
-msgstr ""
-"Увімкнути/вимкнути відстеження поточної позиції аудіо в транскрипції. Коли "
-"увімкнено, автоматично прокручує до поточного тексту."
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Scroll to Current"
-msgstr "Прокрутити до поточного"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Scroll to the currently spoken text"
-msgstr "Прокрутити до тексту, що зараз вимовляється"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "1 of 100+ matches"
-msgstr "1 з 100+ збігів"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "1 of "
-msgstr "1 з "
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " matches"
-msgstr " збігів"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "No matches found"
-msgstr "Збігів не знайдено"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " of 100+ matches"
-msgstr " з 100+ збігів"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " of "
-msgstr " з "
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "API Key Required"
-msgstr "Потрібен API-ключ"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Please enter OpenAI API Key in preferences"
-msgstr "Будь ласка, введіть API-ключ OpenAI в налаштуваннях"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend end time"
-msgstr "Продовжити час завершення"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend endings by up to (seconds)"
-msgstr "Продовжити закінчення до (секунд)"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend endings"
-msgstr "Продовжити закінчення"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Resize Options"
-msgstr "Параметри зміни розміру"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Desired subtitle length"
-msgstr "Бажана довжина субтитрів"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Available only if word level timings were disabled during transcription"
-msgstr ""
-"Доступно лише якщо хронометраж на рівні слів був вимкнений під час "
-"транскрипції"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge Options"
-msgstr "Параметри об'єднання"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge by gap"
-msgstr "Об'єднати за паузою"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Split by punctuation"
-msgstr "Розділити за пунктуацією"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Split by max length"
-msgstr "Розділити за максимальною довжиною"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge"
-msgstr "Об'єднати"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Available only if word level timings were enabled during transcription"
-msgstr ""
-"Доступно лише якщо хронометраж на рівні слів був увімкнений під час "
-"транскрипції"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid ""
-"Speaker identification is not available: failed to load required libraries."
-msgstr ""
-"Визначення мовців недоступне: не вдалося завантажити необхідні бібліотеки."
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "1/8 Collecting transcripts"
-msgstr "1/8 Збирання транскрипцій"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "2/8 Loading audio"
-msgstr "2/8 Завантаження аудіо"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "3/8 Loading alignment model"
-msgstr "3/8 Завантаження моделі вирівнювання"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "3/8 Loading alignment model (retrying with cache...)"
-msgstr "3/8 Завантаження моделі вирівнювання (повторна спроба з кешем...)"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid ""
-"Failed to load alignment model. Please check your internet connection and "
-"try again."
-msgstr ""
-"Не вдалося завантажити модель вирівнювання. Будь ласка, перевірте "
-"підключення до інтернету та спробуйте ще раз."
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "4/8 Processing audio"
-msgstr "4/8 Обробка аудіо"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "5/8 Preparing transcripts"
-msgstr "5/8 Підготовка транскрипцій"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "6/8 Identifying speakers"
-msgstr "6/8 Визначення мовців"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "7/8 Mapping speakers to transcripts"
-msgstr "7/8 Зіставлення мовців із транскрипціями"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "8/8 Identification done"
-msgstr "8/8 Визначення завершено"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "0/0 Error identifying speakers"
-msgstr "0/0 Помилка при визначенні мовців"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Step 1: Identify speakers"
-msgstr "Крок 1: Визначити мовців"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Identify"
-msgstr "Визначити"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Ready to identify speakers"
-msgstr "Готово до визначення мовців"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Audio file not found"
-msgstr "Аудіофайл не знайдено"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Step 2: Name speakers"
-msgstr "Крок 2: Назвати мовців"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Play sample"
-msgstr "Відтворити зразок"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Merge speaker sentences"
-msgstr "Об'єднати речення мовців"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Save"
-msgstr "Зберегти"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Cancelling..."
-msgstr "Скасування..."
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Cancelled"
-msgstr "Скасовано"
-
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Save File"
-msgstr "Зберегти файл"
-
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Text files"
-msgstr "Текстові файли"
-
-#: buzz/widgets/model_download_progress_dialog.py
-msgid "Downloading model"
-msgstr "Завантаження моделі"
-
-#: buzz/widgets/model_download_progress_dialog.py
-msgid "remaining"
-msgstr "залишилось"
-
-#: buzz/widgets/menu_bar.py
-msgid "Import File..."
-msgstr "Імпортувати файл..."
-
-#: buzz/widgets/menu_bar.py
-msgid "Import URL..."
-msgstr "Імпортувати адресу..."
-
-#: buzz/widgets/menu_bar.py
-msgid "Import Folder..."
-msgstr "Імпортувати теку..."
-
-#: buzz/widgets/menu_bar.py
-msgid "About"
-msgstr "Про застосунок"
-
-#: buzz/widgets/menu_bar.py
-msgid "Preferences..."
-msgstr "Налаштування..."
-
-#: buzz/widgets/menu_bar.py
-msgid "Help"
-msgstr "Допомога"
-
-#: buzz/widgets/menu_bar.py
-msgid "File"
-msgstr "Файл"
-
-#: buzz/widgets/main_window.py
-msgid ""
-"Are you sure you want to delete the selected transcription(s)? This action "
-"cannot be undone."
-msgstr ""
-"Ви впевнені, що хочете видалити вибрані транскрипції? Це незворотна дія."
-
-#: buzz/widgets/main_window.py
-msgid "Select audio file"
-msgstr "Вибрати аудіофайл"
-
-#: buzz/widgets/main_window.py
-msgid "Select folder"
-msgstr "Виберіть теку введення"
-
-#: buzz/widgets/main_window.py
-msgid "Unable to save OpenAI API key to keyring"
-msgstr "Не вдається додати до звʼязки ключів API-ключ OpenAI"
-
-#: buzz/transcriber/local_whisper_cpp_server_transcriber.py
-#: buzz/transcriber/recording_transcriber.py
-msgid "Whisper server failed to start. Check logs for details."
-msgstr ""
-"Не вдалося запустити сервер Whisper. Перевірте журнали для отримання деталей."
-
-#: buzz/transcriber/local_whisper_cpp_server_transcriber.py
-#: buzz/transcriber/recording_transcriber.py
-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 через недостатній обсяг пам'яті. Будь "
-"ласка, спробуйте ще раз із меншою моделлю. Для примусового режиму CPU "
-"використовуйте змінну середовища BUZZ_FORCE_CPU=TRUE."
-
-#: buzz/transcriber/transcriber.py
-msgid "Translate to English"
-msgstr "Перекласти на англійську"
-
-#: buzz/transcriber/transcriber.py
-msgid "Transcribe"
-msgstr "Розпізнати"
-
-#: buzz/transcriber/transcriber.py
-msgid "Chinese"
-msgstr "Китайська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Russian"
-msgstr "Російська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Korean"
-msgstr "Корейська"
-
-#: buzz/transcriber/transcriber.py
-msgid "French"
-msgstr "Французька"
-
-#: buzz/transcriber/transcriber.py
-msgid "Portuguese"
-msgstr "Португальська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Turkish"
-msgstr "Турецька"
-
-#: buzz/transcriber/transcriber.py
-msgid "Arabic"
-msgstr "Арабська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Swedish"
-msgstr "Шведська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Indonesian"
-msgstr "Індонезійська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hindi"
-msgstr "Гінді"
-
-#: buzz/transcriber/transcriber.py
-msgid "Finnish"
-msgstr "Фінська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Vietnamese"
-msgstr "В'єтнамська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hebrew"
-msgstr "Іврит"
-
-#: buzz/transcriber/transcriber.py
-msgid "Greek"
-msgstr "Грецька"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malay"
-msgstr "Малайська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Czech"
-msgstr "Чеська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Romanian"
-msgstr "Румунська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hungarian"
-msgstr "Угорська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tamil"
-msgstr "Тамільська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Norwegian"
-msgstr "Норвезька"
-
-#: buzz/transcriber/transcriber.py
-msgid "Thai"
-msgstr "Тайська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Urdu"
-msgstr "Урду"
-
-#: buzz/transcriber/transcriber.py
-msgid "Croatian"
-msgstr "Хорватська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bulgarian"
-msgstr "Болгарська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lithuanian"
-msgstr "Литовська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Latin"
-msgstr "Латинська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Maori"
-msgstr "Маорі"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malayalam"
-msgstr "Малаялам"
-
-#: buzz/transcriber/transcriber.py
-msgid "Welsh"
-msgstr "Валлійська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Slovak"
-msgstr "Словацька"
-
-#: buzz/transcriber/transcriber.py
-msgid "Telugu"
-msgstr "Телугу"
-
-#: buzz/transcriber/transcriber.py
-msgid "Persian"
-msgstr "Перська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bengali"
-msgstr "Бенгальська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Serbian"
-msgstr "Сербська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Azerbaijani"
-msgstr "Азербайджанська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Slovenian"
-msgstr "Словенська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Kannada"
-msgstr "Каннада"
-
-#: buzz/transcriber/transcriber.py
-msgid "Estonian"
-msgstr "Естонська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Macedonian"
-msgstr "Македонська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Breton"
-msgstr "Бретонська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Basque"
-msgstr "Баскська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Icelandic"
-msgstr "Ісландська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Armenian"
-msgstr "Вірменська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Nepali"
-msgstr "Непальська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Mongolian"
-msgstr "Монгольська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bosnian"
-msgstr "Боснійська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Kazakh"
-msgstr "Казахська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Albanian"
-msgstr "Албанська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Swahili"
-msgstr "Суахілі"
-
-#: buzz/transcriber/transcriber.py
-msgid "Galician"
-msgstr "Галісійська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Marathi"
-msgstr "Маратхі"
-
-#: buzz/transcriber/transcriber.py
-msgid "Punjabi"
-msgstr "Панджабі"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sinhala"
-msgstr "Сингальська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Khmer"
-msgstr "Кхмерська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Shona"
-msgstr "Шона"
-
-#: buzz/transcriber/transcriber.py
-msgid "Yoruba"
-msgstr "Йоруба"
-
-#: buzz/transcriber/transcriber.py
-msgid "Somali"
-msgstr "Сомалійська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Afrikaans"
-msgstr "Африкаанс"
-
-#: buzz/transcriber/transcriber.py
-msgid "Occitan"
-msgstr "Окситанська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Georgian"
-msgstr "Грузинська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Belarusian"
-msgstr "Білоруська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tajik"
-msgstr "Таджицька"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sindhi"
-msgstr "Сіндхі"
-
-#: buzz/transcriber/transcriber.py
-msgid "Gujarati"
-msgstr "Гуджараті"
-
-#: buzz/transcriber/transcriber.py
-msgid "Amharic"
-msgstr "Амхарська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Yiddish"
-msgstr "Їдиш"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lao"
-msgstr "Лаоська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Uzbek"
-msgstr "Узбецька"
-
-#: buzz/transcriber/transcriber.py
-msgid "Faroese"
-msgstr "Фарерська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Haitian Creole"
-msgstr "Гаїтянська креольська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Pashto"
-msgstr "Пушту"
-
-#: buzz/transcriber/transcriber.py
-msgid "Turkmen"
-msgstr "Туркменська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Nynorsk"
-msgstr "Нюношк"
-
-#: buzz/transcriber/transcriber.py
-msgid "Maltese"
-msgstr "Мальтійська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sanskrit"
-msgstr "Санскрит"
-
-#: buzz/transcriber/transcriber.py
-msgid "Luxembourgish"
-msgstr "Люксембурзька"
-
-#: buzz/transcriber/transcriber.py
-msgid "Myanmar"
-msgstr "М'янма"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tibetan"
-msgstr "Тибетська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tagalog"
-msgstr "Тагальська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malagasy"
-msgstr "Малагасійська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Assamese"
-msgstr "Ассамська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tatar"
-msgstr "Татарська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hawaiian"
-msgstr "Гавайська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lingala"
-msgstr "Лінгала"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hausa"
-msgstr "Хауса"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bashkir"
-msgstr "Башкирська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Javanese"
-msgstr "Яванська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sundanese"
-msgstr "Сунданська"
-
-#: buzz/transcriber/transcriber.py
-msgid "Cantonese"
-msgstr "Кантонська"
-
-#: buzz/transcriber/recording_transcriber.py buzz/model_loader.py
-msgid "A connection error occurred"
-msgstr "Виникла помилка зʼєднання"
-
-#: buzz/transcriber/recording_transcriber.py
-msgid "Starting Whisper.cpp..."
-msgstr "Запуск Whisper.cpp..."
-
-#: buzz/transcriber/recording_transcriber.py
-msgid "Starting transcription..."
-msgstr "Запуск транскрипції..."
-
-#: buzz/settings/shortcut.py
-msgid "Open Record Window"
-msgstr "Відкрити вікно запису"
-
-#: buzz/settings/shortcut.py
-msgid "Import File"
-msgstr "Імпортувати файл"
-
-#: buzz/settings/shortcut.py
-msgid "Open Preferences Window"
-msgstr "Відкрити вікно налаштувань"
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Text"
-msgstr "Переглянути текст транскрипції"
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Translation"
-msgstr "Переглянути переклад транскрипції"
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Timestamps"
-msgstr "Переглянути позначки часу в транскрипції"
-
-#: buzz/settings/shortcut.py
-msgid "Search Transcript"
-msgstr "Пошук у транскрипції"
-
-#: buzz/settings/shortcut.py
-msgid "Go to Next Transcript Search Result"
-msgstr "Перейти до наступного результату пошуку в транскрипції"
-
-#: buzz/settings/shortcut.py
-msgid "Go to Previous Transcript Search Result"
-msgstr "Перейти до попереднього результату пошуку в транскрипції"
-
-#: buzz/settings/shortcut.py
-msgid "Scroll to Current Text"
-msgstr "Прокрутити до поточного тексту"
-
-#: buzz/settings/shortcut.py
-msgid "Play/Pause Audio"
-msgstr "Відтворити/призупинити аудіо"
-
-#: buzz/settings/shortcut.py
-msgid "Replay Current Segment"
-msgstr "Повторити поточний фрагмент"
-
-#: buzz/settings/shortcut.py
-msgid "Toggle Playback Controls"
-msgstr "Перемкнути елементи керування відтворенням"
-
-#: buzz/settings/shortcut.py
-msgid "Decrease Segment Start Time"
-msgstr "Зменшити час початку фрагмента"
-
-#: buzz/settings/shortcut.py
-msgid "Increase Segment Start Time"
-msgstr "Збільшити час початку фрагмента"
-
-#: buzz/settings/shortcut.py
-msgid "Decrease Segment End Time"
-msgstr "Зменшити час завершення фрагмента"
-
-#: buzz/settings/shortcut.py
-msgid "Increase Segment End Time"
-msgstr "Збільшити час завершення фрагмента"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append below"
-msgstr "Додати знизу"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append above"
-msgstr "Додати зверху"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append and correct"
-msgstr "Додати та виправити"
-
-#: buzz/file_transcriber_queue_worker.py
-msgid ""
-"Speech extraction failed! Check your internet connection — a model may need "
-"to be downloaded."
-msgstr ""
-"Не вдалося витягти мовлення! Перевірте підключення до інтернету — можливо, "
-"потрібно завантажити модель."
-
-#~ msgid "Comma-separated, e.g. \"0.0, 0.2, 0.4, 0.6, 0.8, 1.0\""
-#~ msgstr "Значення розділені комами, напр., \"0.0, 0.2, 0.4, 0.6, 0.8, 1.0\""
-
-#~ msgid "Temperature:"
-#~ msgstr "Температура:"
-
-#~ msgid "Please translate each text sent to you from English to Spanish."
-#~ msgstr ""
-#~ "Будь ласка, перекладайте кожен текст, надісланий вам, з англійської на "
-#~ "іспанську."
-
-#~ msgid "Translation error, see logs!"
-#~ msgstr "Помилка перекладу, перегляньте журнали!"
-
-#~ msgid "Snap permission notice"
-#~ msgstr "Попередження щодо дозволів Snap"
-
-#~ msgid ""
-#~ "Detected missing permissions, please check that snap permissions have "
-#~ "been granted"
-#~ msgstr ""
-#~ "Виявлено нестачу повноважень. Будь ласка, перевірте, чи були надані "
-#~ "дозволи для Snap"
-
-#~ msgid ""
-#~ "To enable necessary permissions run the following commands in the terminal"
-#~ msgstr ""
-#~ "Для активації необхідних дозволів, запустіть наступну команду в терміналі"
-
-#~ msgid "Close"
-#~ msgstr "Закрити"
-
-#~ msgid "Enter instructions for AI on how to translate..."
-#~ msgstr "Введіть інструкції для перекладу ШІ..."
diff --git a/buzz/locale/zh_CN/LC_MESSAGES/buzz.po b/buzz/locale/zh_CN/LC_MESSAGES/buzz.po
deleted file mode 100644
index 35b76c69..00000000
--- a/buzz/locale/zh_CN/LC_MESSAGES/buzz.po
+++ /dev/null
@@ -1,1649 +0,0 @@
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
-# This file is distributed under the same license as the PACKAGE package.
-# FIRST AUTHOR , 2024.
-# 'transcript' as '识别'
-msgid ""
-msgstr ""
-"Project-Id-Version: \n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2026-03-07 20:20+0200\n"
-"PO-Revision-Date: 2023-05-01 15:45+0800\n"
-"Last-Translator: \n"
-"Language-Team: lamb \n"
-"Language: zh_CN\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Generator: Poedit 3.2.2\n"
-
-#: buzz/widgets/import_url_dialog.py buzz/settings/shortcut.py
-msgid "Import URL"
-msgstr "导入URL"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "https://example.com/audio.mp3"
-msgstr "https://example.com/audio.mp3"
-
-#: buzz/widgets/import_url_dialog.py
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-#: buzz/widgets/main_window.py
-msgid "Ok"
-msgstr "Ok"
-
-#: buzz/widgets/import_url_dialog.py
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-#: buzz/widgets/model_download_progress_dialog.py buzz/widgets/main_window.py
-msgid "Cancel"
-msgstr "取消"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "URL:"
-msgstr "网址:"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "Invalid URL"
-msgstr "无效的网址"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "The URL you entered is invalid."
-msgstr "输入的网址无效"
-
-#: buzz/widgets/presentation_window.py
-msgid "Live Transcript Presentation"
-msgstr "实时识别展示"
-
-#: buzz/widgets/preferences_dialog/shortcuts_editor_preferences_widget.py
-msgid "Reset to Defaults"
-msgstr "恢复默认"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "English"
-msgstr "英语"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Catalan"
-msgstr "加泰罗尼亚语"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Danish"
-msgstr "丹麦语"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Dutch"
-msgstr "荷兰语"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "German"
-msgstr "德语"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Spanish"
-msgstr "西班牙语"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Italian"
-msgstr "意大利语"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Japanese"
-msgstr "日语"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Latvian"
-msgstr "拉脱维亚语"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Polish"
-msgstr "波兰语"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Portuguese (Brazil)"
-msgstr "葡萄牙语(巴西)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Ukrainian"
-msgstr "乌克兰语"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Chinese (Simplified)"
-msgstr "中文(简体)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Chinese (Traditional)"
-msgstr "中文(繁体)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Restart required!"
-msgstr "需要重启!"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Ui Language"
-msgstr "界面语言"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Font Size"
-msgstr "字体大小"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Test"
-msgstr "测试"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API key"
-msgstr "OpenAI API key"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI base url"
-msgstr "OpenAI 基于 url"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API model"
-msgstr "OpenAI API 模型"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Default export file name"
-msgstr "默认输出文件名"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Enable live recording transcription export"
-msgstr "启用实时录制转录导出"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Browse"
-msgstr "浏览"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Export folder"
-msgstr "导出文件夹"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Live recording mode"
-msgstr "实时录制模式"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Note: Live recording export settings will be moved to the Advanced Settings "
-"in the Live Recording screen in a future version."
-msgstr "注意:实时录制导出设置将在未来版本中移至实时录制界面的高级设置中。"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Use 8-bit quantization to reduce memory usage"
-msgstr "使用8位量化以减少内存占用"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Applies to Huggingface and Faster Whisper models. Reduces GPU memory usage "
-"but may slightly decrease transcription quality."
-msgstr ""
-"适用于 Huggingface 和 Faster Whisper 模型。可减少 GPU 内存占用,但可能略微降"
-"低转录质量。"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Reduce GPU RAM"
-msgstr "减少 GPU 内存"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Use only CPU and disable GPU acceleration"
-msgstr "仅使用 CPU 并禁用 GPU 加速"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Set this if larger models do not fit your GPU memory and Buzz crashes"
-msgstr "如果较大的模型无法适配您的 GPU 内存且 Buzz 崩溃,请启用此选项"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Disable GPU"
-msgstr "禁用 GPU"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API Key Test"
-msgstr "测试OpenAI API Key"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Your API key is valid. Buzz will use this key to perform Whisper API "
-"transcriptions and AI translations."
-msgstr "您的API密钥有效。Buzz将使用此密钥执行 Whisper API 识别和 AI 翻译。"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Invalid API key"
-msgstr "无效的API key"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"API supports only base64 characters (A-Za-z0-9+/=_-). Other characters in "
-"API key may cause errors."
-msgstr ""
-"API只支持 base64字符(A-Za-z0-9+/=_-)。其他字符在API密钥中可能导致错误。"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Select Export Folder"
-msgstr "选择输出文件夹"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"OpenAI API returned invalid response. Please check the API url or your key. "
-"Transcription and translation may still work if the API does not support key "
-"validation."
-msgstr ""
-"OpenAI API返回无效响应。请检查API网址或您的密钥。如果API不支持密钥验证,转录"
-"和翻译可能仍然有效翻"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Enable folder watch"
-msgstr "开启文件夹监控"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Delete processed files"
-msgstr "删除已处理的文件"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Input folder"
-msgstr "输入文件夹"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Output folder"
-msgstr "输出文件夹"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Select Input Folder"
-msgstr "选择输入文件夹"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Select Output Folder"
-msgstr "选择输出文件夹"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Preferences"
-msgstr "偏好设置"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "General"
-msgstr "通用"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Models"
-msgstr "模型"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Shortcuts"
-msgstr "快捷键"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Folder Watch"
-msgstr "文件夹查看"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Group"
-msgstr "组"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Huggingface ID of a Faster whisper model"
-msgstr "较快的Whisper模型的Huggingface ID"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download"
-msgstr "下载"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Show file location"
-msgstr "查看文件位置"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Delete"
-msgstr "删除"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Downloaded"
-msgstr "已下载"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Available for Download"
-msgstr "可用的下载"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download link to Whisper.cpp ggml model file"
-msgstr "Whisper.cpp ggml 模型文件的下载链接"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Delete Model"
-msgstr "删除模型"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Are you sure you want to delete the selected model?"
-msgstr "您确定要删除所选模型吗?"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download failed"
-msgstr "下载失败"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcription_tasks_table_widget.py
-#: buzz/widgets/update_dialog.py buzz/widgets/main_window.py
-#: buzz/model_loader.py
-msgid "Error"
-msgstr "错误"
-
-#: buzz/widgets/record_button.py buzz/widgets/main_window_toolbar.py
-msgid "Record"
-msgstr "录制"
-
-#: buzz/widgets/record_button.py
-msgid "Stop"
-msgstr "停止"
-
-#: buzz/widgets/transcriber/languages_combo_box.py
-#: buzz/transcriber/transcriber.py
-msgid "Detect Language"
-msgstr "检测语言"
-
-#: buzz/widgets/transcriber/mms_language_line_edit.py
-msgid "e.g., eng, fra, deu"
-msgstr "例如:eng, fra, deu"
-
-#: buzz/widgets/transcriber/mms_language_line_edit.py
-msgid ""
-"Enter an ISO 639-3 language code (3 letters).\n"
-"Examples: eng (English), fra (French), deu (German),\n"
-"spa (Spanish), lav (Latvian)"
-msgstr ""
-"输入 ISO 639-3 语言代码(3个字母)。\n"
-"示例:eng(英语), fra(法语), deu(德语),\n"
-"spa(西班牙语), lav(拉脱维亚语)"
-
-#: buzz/widgets/transcriber/file_transcriber_widget.py
-msgid "Run"
-msgstr "开始执行"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Model:"
-msgstr "模型:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-#: buzz/transcriber/recording_transcriber.py
-msgid "First time use of a model may take up to several minutest to load."
-msgstr "首次使用模型可能需要几分钟的时间才能加载"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Api Key:"
-msgstr "Api Key:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Task:"
-msgstr "任务:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Language:"
-msgstr "语言:"
-
-#: buzz/widgets/transcriber/initial_prompt_text_edit.py
-msgid "Enter prompt..."
-msgstr "请输入文本..."
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Advanced Settings"
-msgstr "高级设置"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Speech recognition settings"
-msgstr "语音识别设置"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Initial Prompt:"
-msgstr "初始提示:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Translation settings"
-msgstr "翻译设置"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Enable AI translation"
-msgstr "启用AI翻译"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "AI model:"
-msgstr "AI 模型:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid ""
-"Please translate each text sent to you from English to Spanish. Translation "
-"will be used in an automated system, please do not add any comments or "
-"notes, just the translation."
-msgstr ""
-"请将发送给您的每段文本从英语翻译成西班牙语。翻译将用于自动化系统,请不要添加"
-"任何评论或备注,只需提供翻译即可。"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Instructions for AI:"
-msgstr "AI说明:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Recording settings"
-msgstr "录制设置"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Silence threshold:"
-msgstr "静音阈值:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Live recording mode:"
-msgstr "实时录制模式:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Line separator:"
-msgstr "行分隔符:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Transcription step:"
-msgstr "转录步长:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Hide unconfirmed"
-msgstr "隐藏未确认内容"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Enable live recording export"
-msgstr "启用实时录制导出"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export folder:"
-msgstr "导出文件夹:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export file name:"
-msgstr "导出文件名:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Text file (.txt)"
-msgstr "文本文件 (.txt)"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "CSV (.csv)"
-msgstr "CSV (.csv)"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export file type:"
-msgstr "导出文件类型:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid ""
-"Limit export entries\n"
-"(0 = export all):"
-msgstr ""
-"限制导出条目数\n"
-"(0 = 全部导出):"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Word-level timings"
-msgstr "逐词识别"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Extract speech"
-msgstr "提取语音"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Export:"
-msgstr "导出:"
-
-#: buzz/widgets/transcriber/hugging_face_search_line_edit.py
-msgid "Huggingface ID of a model"
-msgstr "模型的Huggingface ID "
-
-#: buzz/widgets/transcriber/advanced_settings_button.py
-msgid "Advanced..."
-msgstr "高级..."
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "New File Transcription"
-msgstr "新增文件识别"
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "New URL Transcription"
-msgstr "新增URL识别"
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "Open Transcript"
-msgstr "打开识别结果"
-
-#: buzz/widgets/main_window_toolbar.py buzz/settings/shortcut.py
-msgid "Cancel Transcription"
-msgstr "取消识别"
-
-#: buzz/widgets/main_window_toolbar.py buzz/widgets/main_window.py
-#: buzz/settings/shortcut.py
-msgid "Clear History"
-msgstr "清除历史纪录"
-
-#: buzz/widgets/main_window_toolbar.py buzz/widgets/update_dialog.py
-msgid "Update Available"
-msgstr "有可用更新"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "In Progress"
-msgstr "运行中"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Completed"
-msgstr "完成"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Failed"
-msgstr "失败"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Canceled"
-msgstr "取消"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Queued"
-msgstr "队列中"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "File Name / URL"
-msgstr "文件名称/URL"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Model"
-msgstr "模型"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Task"
-msgstr "任务"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Status"
-msgstr "状态"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Date Completed"
-msgstr "完成时间"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Date Added"
-msgstr "添加日期"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Notes"
-msgstr "备注"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Reset Column Order"
-msgstr "重置列顺序"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Restart Transcription"
-msgstr "重新开始识别"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Rename"
-msgstr "重命名"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Add/Edit Notes"
-msgstr "添加/编辑备注"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Rename Transcription"
-msgstr "重命名识别"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Enter new name:"
-msgstr "输入新名称:"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Enter some relevant notes for this transcription:"
-msgstr "为此识别输入相关备注:"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Cannot Restart"
-msgstr "无法重新开始"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Only failed or canceled transcriptions can be restarted."
-msgstr "只有失败或已取消的识别才能重新开始。"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Failed to restart transcription: {}"
-msgstr "重新开始识别失败:{}"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid ""
-"Could not restart transcription: model not available and could not be "
-"downloaded."
-msgstr "无法重新开始识别:模型不可用且无法下载。"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Could not restart transcription: transcriber worker not found."
-msgstr "无法重新开始识别:未找到识别工作进程。"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Live Recording"
-msgstr "实时录制"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Click Record to begin..."
-msgstr "点击开始录制"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Waiting for AI translation..."
-msgstr "等待AI翻译..."
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Microphone:"
-msgstr "麦克风:"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Show in new window"
-msgstr "在新窗口中显示"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Text Size:"
-msgstr "文字大小:"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Theme"
-msgstr "主题"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Light"
-msgstr "浅色"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Dark"
-msgstr "深色"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Custom"
-msgstr "自定义"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Text Color"
-msgstr "文字颜色"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Background Color"
-msgstr "背景颜色"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Fullscreen"
-msgstr "全屏"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy"
-msgstr "复制"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy transcription to clipboard"
-msgstr "复制识别内容到剪贴板"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Nothing to copy!"
-msgstr "没有可复制的内容!"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy failed"
-msgstr "复制失败"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copied!"
-msgstr "已复制!"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Select Text Color"
-msgstr "选择文字颜色"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Select Background Color"
-msgstr "选择背景颜色"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "An error occurred while starting a new recording:"
-msgstr "开始新录制时出错"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid ""
-"Please check your audio devices or check the application logs for more "
-"information."
-msgstr "请检查您的音频设备或检查应用程序日志以获取更多信息。"
-
-#: buzz/widgets/update_dialog.py
-msgid "A new version of Buzz is available!"
-msgstr "Buzz 有新版本可用!"
-
-#: buzz/widgets/update_dialog.py
-msgid "Current version:"
-msgstr "当前版本:"
-
-#: buzz/widgets/update_dialog.py
-msgid "New version:"
-msgstr "新版本:"
-
-#: buzz/widgets/update_dialog.py
-msgid "Release Notes:"
-msgstr "发行说明:"
-
-#: buzz/widgets/update_dialog.py
-msgid "Download and Install"
-msgstr "下载并安装"
-
-#: buzz/widgets/update_dialog.py
-msgid "No download URL available for your platform."
-msgstr "您的平台没有可用的下载链接。"
-
-#: buzz/widgets/update_dialog.py
-msgid "Downloading file {} of {}..."
-msgstr "正在下载第 {} 个文件,共 {} 个..."
-
-#: buzz/widgets/update_dialog.py
-msgid "Downloading file {} of {} ({:.1f} MB / {:.1f} MB)..."
-msgstr "正在下载第 {} 个文件,共 {} 个({:.1f} MB / {:.1f} MB)..."
-
-#: buzz/widgets/update_dialog.py
-msgid "Download Failed"
-msgstr "下载失败"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to download the update: {}"
-msgstr "下载更新失败:{}"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to save the installer: {}"
-msgstr "无法保存安装程序:{}"
-
-#: buzz/widgets/update_dialog.py
-msgid "Download complete!"
-msgstr "下载完成!"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to run the installer: {}"
-msgstr "无法运行安装程序:{}"
-
-#: buzz/widgets/about_dialog.py
-msgid "Check for updates"
-msgstr "检查更新"
-
-#: buzz/widgets/about_dialog.py
-msgid "Show logs"
-msgstr "显示日志"
-
-#: buzz/widgets/about_dialog.py
-msgid "You're up to date!"
-msgstr "已经是最新版本"
-
-#: buzz/widgets/audio_meter_widget.py
-msgid "Average volume"
-msgstr "平均音量"
-
-#: buzz/widgets/audio_meter_widget.py
-msgid "Queue"
-msgstr "队列"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-msgid "Start"
-msgstr "开始"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-msgid "End"
-msgstr "结束"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Text"
-msgstr "文本"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Translation"
-msgstr "翻译"
-
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-msgid "View"
-msgstr "查看"
-
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-msgid "Timestamps"
-msgstr "时间戳"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Export"
-msgstr "导出"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Translate"
-msgstr "翻译"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Resize"
-msgstr "调整大小"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Identify Speakers"
-msgstr "识别说话人"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Find"
-msgstr "查找"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Show/Hide Search Bar (Ctrl+F)"
-msgstr "显示/隐藏搜索栏(Ctrl+F)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Find:"
-msgstr "查找:"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Enter text to find..."
-msgstr "输入要查找的文本..."
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Previous match (Shift+Enter)"
-msgstr "上一个匹配项(Shift+Enter)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Next match (Ctrl+Enter)"
-msgstr "下一个匹配项(Ctrl+Enter)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Clear"
-msgstr "清除"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Playback Controls:"
-msgstr "播放控制:"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Loop Segment"
-msgstr "循环片段"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Enable/disable looping when clicking on transcript segments"
-msgstr "点击识别片段时启用/禁用循环播放"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Follow Audio"
-msgstr "跟随音频"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid ""
-"Enable/disable following the current audio position in the transcript. When "
-"enabled, automatically scrolls to current text."
-msgstr "在识别文本中启用/禁用跟随当前音频位置。启用后,自动滚动到当前文本。"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Scroll to Current"
-msgstr "滚动到当前位置"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Scroll to the currently spoken text"
-msgstr "滚动到当前正在播放的文本"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "1 of 100+ matches"
-msgstr "第1项,共100+个匹配项"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "1 of "
-msgstr "第1项,共"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " matches"
-msgstr "个匹配项"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "No matches found"
-msgstr "未找到匹配项"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " of 100+ matches"
-msgstr "项,共100+个匹配项"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " of "
-msgstr "项,共"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "API Key Required"
-msgstr "需要API Key"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Please enter OpenAI API Key in preferences"
-msgstr "请在偏好设置中输入OpenAI API Key"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend end time"
-msgstr "延长结束时间"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend endings by up to (seconds)"
-msgstr "最多延长结束时间(秒)"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend endings"
-msgstr "延长结尾"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Resize Options"
-msgstr "调整大小选项"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Desired subtitle length"
-msgstr "所需字幕长度"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Available only if word level timings were disabled during transcription"
-msgstr "仅在转录时禁用逐词时间戳时可用"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge Options"
-msgstr "合并选项"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge by gap"
-msgstr "按间隔合并"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Split by punctuation"
-msgstr "按标点符号拆分"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Split by max length"
-msgstr "按最大长度拆分"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge"
-msgstr "合并"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Available only if word level timings were enabled during transcription"
-msgstr "仅在转录时启用逐词时间戳时可用"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid ""
-"Speaker identification is not available: failed to load required libraries."
-msgstr "说话人识别不可用:加载所需库失败。"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "1/8 Collecting transcripts"
-msgstr "1/8 收集识别文本"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "2/8 Loading audio"
-msgstr "2/8 加载音频"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "3/8 Loading alignment model"
-msgstr "3/8 加载对齐模型"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "3/8 Loading alignment model (retrying with cache...)"
-msgstr "3/8 加载对齐模型(正在使用缓存重试...)"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid ""
-"Failed to load alignment model. Please check your internet connection and "
-"try again."
-msgstr "加载对齐模型失败。请检查您的网络连接后重试。"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "4/8 Processing audio"
-msgstr "4/8 处理音频"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "5/8 Preparing transcripts"
-msgstr "5/8 准备识别文本"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "6/8 Identifying speakers"
-msgstr "6/8 识别说话人"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "7/8 Mapping speakers to transcripts"
-msgstr "7/8 将说话人映射到识别文本"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "8/8 Identification done"
-msgstr "8/8 识别完成"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "0/0 Error identifying speakers"
-msgstr "0/0 识别说话人时出错"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Step 1: Identify speakers"
-msgstr "第1步:识别说话人"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Identify"
-msgstr "识别"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Ready to identify speakers"
-msgstr "准备好识别说话人"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Audio file not found"
-msgstr "未找到音频文件"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Step 2: Name speakers"
-msgstr "第2步:命名说话人"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Play sample"
-msgstr "播放样本"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Merge speaker sentences"
-msgstr "合并说话人句子"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Save"
-msgstr "保存"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Cancelling..."
-msgstr "正在取消..."
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Cancelled"
-msgstr "已取消"
-
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Save File"
-msgstr "保存文件"
-
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Text files"
-msgstr "文本文件"
-
-#: buzz/widgets/model_download_progress_dialog.py
-msgid "Downloading model"
-msgstr "模型下载中"
-
-#: buzz/widgets/model_download_progress_dialog.py
-msgid "remaining"
-msgstr "剩余"
-
-#: buzz/widgets/menu_bar.py
-msgid "Import File..."
-msgstr "导入文件..."
-
-#: buzz/widgets/menu_bar.py
-msgid "Import URL..."
-msgstr "导入URL..."
-
-#: buzz/widgets/menu_bar.py
-msgid "Import Folder..."
-msgstr "导入文件夹..."
-
-#: buzz/widgets/menu_bar.py
-msgid "About"
-msgstr "关于"
-
-#: buzz/widgets/menu_bar.py
-msgid "Preferences..."
-msgstr "偏好设置..."
-
-#: buzz/widgets/menu_bar.py
-msgid "Help"
-msgstr "帮助"
-
-#: buzz/widgets/menu_bar.py
-msgid "File"
-msgstr "文件"
-
-#: buzz/widgets/main_window.py
-msgid ""
-"Are you sure you want to delete the selected transcription(s)? This action "
-"cannot be undone."
-msgstr "您确定要删除所选录制吗?此操作无法撤消。"
-
-#: buzz/widgets/main_window.py
-msgid "Select audio file"
-msgstr "选择音频文件"
-
-#: buzz/widgets/main_window.py
-msgid "Select folder"
-msgstr "选择输入文件夹"
-
-#: buzz/widgets/main_window.py
-msgid "Unable to save OpenAI API key to keyring"
-msgstr "无法将OpenAI API密钥保存到密钥串"
-
-#: buzz/transcriber/local_whisper_cpp_server_transcriber.py
-#: buzz/transcriber/recording_transcriber.py
-msgid "Whisper server failed to start. Check logs for details."
-msgstr "Whisper 服务器启动失败。请查看日志以获取详情。"
-
-#: buzz/transcriber/local_whisper_cpp_server_transcriber.py
-#: buzz/transcriber/recording_transcriber.py
-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 服务器因内存不足而启动失败。请尝试使用较小的模型。如需强制使用 CPU 模"
-"式,请设置环境变量 BUZZ_FORCE_CPU=TRUE。"
-
-#: buzz/transcriber/transcriber.py
-msgid "Translate to English"
-msgstr "翻译成英语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Transcribe"
-msgstr "识别"
-
-#: buzz/transcriber/transcriber.py
-msgid "Chinese"
-msgstr "中文"
-
-#: buzz/transcriber/transcriber.py
-msgid "Russian"
-msgstr "俄语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Korean"
-msgstr "韩语"
-
-#: buzz/transcriber/transcriber.py
-msgid "French"
-msgstr "法语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Portuguese"
-msgstr "葡萄牙语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Turkish"
-msgstr "土耳其语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Arabic"
-msgstr "阿拉伯语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Swedish"
-msgstr "瑞典语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Indonesian"
-msgstr "印度尼西亚语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hindi"
-msgstr "印地语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Finnish"
-msgstr "芬兰语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Vietnamese"
-msgstr "越南语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hebrew"
-msgstr "希伯来语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Greek"
-msgstr "希腊语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malay"
-msgstr "马来语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Czech"
-msgstr "捷克语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Romanian"
-msgstr "罗马尼亚语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hungarian"
-msgstr "匈牙利语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tamil"
-msgstr "泰米尔语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Norwegian"
-msgstr "挪威语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Thai"
-msgstr "泰语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Urdu"
-msgstr "乌尔都语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Croatian"
-msgstr "克罗地亚语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bulgarian"
-msgstr "保加利亚语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lithuanian"
-msgstr "立陶宛语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Latin"
-msgstr "拉丁语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Maori"
-msgstr "毛利语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malayalam"
-msgstr "马拉雅拉姆语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Welsh"
-msgstr "威尔士语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Slovak"
-msgstr "斯洛伐克语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Telugu"
-msgstr "泰卢固语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Persian"
-msgstr "波斯语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bengali"
-msgstr "孟加拉语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Serbian"
-msgstr "塞尔维亚语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Azerbaijani"
-msgstr "阿塞拜疆语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Slovenian"
-msgstr "斯洛文尼亚语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Kannada"
-msgstr "卡纳达语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Estonian"
-msgstr "爱沙尼亚语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Macedonian"
-msgstr "马其顿语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Breton"
-msgstr "布列塔尼语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Basque"
-msgstr "巴斯克语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Icelandic"
-msgstr "冰岛语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Armenian"
-msgstr "亚美尼亚语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Nepali"
-msgstr "尼泊尔语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Mongolian"
-msgstr "蒙古语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bosnian"
-msgstr "波斯尼亚语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Kazakh"
-msgstr "哈萨克语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Albanian"
-msgstr "阿尔巴尼亚语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Swahili"
-msgstr "斯瓦希里语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Galician"
-msgstr "加利西亚语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Marathi"
-msgstr "马拉地语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Punjabi"
-msgstr "旁遮普语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sinhala"
-msgstr "僧伽罗语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Khmer"
-msgstr "高棉语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Shona"
-msgstr "绍纳语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Yoruba"
-msgstr "约鲁巴语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Somali"
-msgstr "索马里语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Afrikaans"
-msgstr "南非荷兰语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Occitan"
-msgstr "奥克语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Georgian"
-msgstr "格鲁吉亚语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Belarusian"
-msgstr "白俄罗斯语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tajik"
-msgstr "塔吉克语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sindhi"
-msgstr "信德语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Gujarati"
-msgstr "古吉拉特语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Amharic"
-msgstr "阿姆哈拉语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Yiddish"
-msgstr "意第绪语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lao"
-msgstr "老挝语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Uzbek"
-msgstr "乌兹别克语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Faroese"
-msgstr "法罗语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Haitian Creole"
-msgstr "海地克里奥尔语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Pashto"
-msgstr "普什图语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Turkmen"
-msgstr "土库曼语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Nynorsk"
-msgstr "新挪威语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Maltese"
-msgstr "马耳他语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sanskrit"
-msgstr "梵语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Luxembourgish"
-msgstr "卢森堡语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Myanmar"
-msgstr "缅甸语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tibetan"
-msgstr "藏语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tagalog"
-msgstr "他加禄语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malagasy"
-msgstr "马达加斯加语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Assamese"
-msgstr "阿萨姆语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tatar"
-msgstr "鞑靼语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hawaiian"
-msgstr "夏威夷语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lingala"
-msgstr "林加拉语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hausa"
-msgstr "豪萨语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bashkir"
-msgstr "巴什基尔语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Javanese"
-msgstr "爪哇语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sundanese"
-msgstr "巽他语"
-
-#: buzz/transcriber/transcriber.py
-msgid "Cantonese"
-msgstr "粤语"
-
-#: buzz/transcriber/recording_transcriber.py buzz/model_loader.py
-msgid "A connection error occurred"
-msgstr "连接发生错误"
-
-#: buzz/transcriber/recording_transcriber.py
-msgid "Starting Whisper.cpp..."
-msgstr "正在启动 Whisper.cpp..."
-
-#: buzz/transcriber/recording_transcriber.py
-msgid "Starting transcription..."
-msgstr "正在开始识别..."
-
-#: buzz/settings/shortcut.py
-msgid "Open Record Window"
-msgstr "打开录制窗口"
-
-#: buzz/settings/shortcut.py
-msgid "Import File"
-msgstr "导入文件"
-
-#: buzz/settings/shortcut.py
-msgid "Open Preferences Window"
-msgstr "打开偏好设置窗口"
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Text"
-msgstr "查看识别的文本"
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Translation"
-msgstr "查看识别的翻译"
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Timestamps"
-msgstr "查看识别时间戳"
-
-#: buzz/settings/shortcut.py
-msgid "Search Transcript"
-msgstr "搜索识别内容"
-
-#: buzz/settings/shortcut.py
-msgid "Go to Next Transcript Search Result"
-msgstr "转到下一个识别搜索结果"
-
-#: buzz/settings/shortcut.py
-msgid "Go to Previous Transcript Search Result"
-msgstr "转到上一个识别搜索结果"
-
-#: buzz/settings/shortcut.py
-msgid "Scroll to Current Text"
-msgstr "滚动到当前文本"
-
-#: buzz/settings/shortcut.py
-msgid "Play/Pause Audio"
-msgstr "播放/暂停音频"
-
-#: buzz/settings/shortcut.py
-msgid "Replay Current Segment"
-msgstr "重播当前片段"
-
-#: buzz/settings/shortcut.py
-msgid "Toggle Playback Controls"
-msgstr "切换播放控制"
-
-#: buzz/settings/shortcut.py
-msgid "Decrease Segment Start Time"
-msgstr "减少片段开始时间"
-
-#: buzz/settings/shortcut.py
-msgid "Increase Segment Start Time"
-msgstr "增加片段开始时间"
-
-#: buzz/settings/shortcut.py
-msgid "Decrease Segment End Time"
-msgstr "减少片段结束时间"
-
-#: buzz/settings/shortcut.py
-msgid "Increase Segment End Time"
-msgstr "增加片段结束时间"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append below"
-msgstr "增加下方"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append above"
-msgstr "增加上方"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append and correct"
-msgstr "增加并纠正"
-
-#: buzz/file_transcriber_queue_worker.py
-msgid ""
-"Speech extraction failed! Check your internet connection — a model may need "
-"to be downloaded."
-msgstr "语音提取失败!请检查您的网络连接——可能需要下载模型。"
-
-#~ msgid "Comma-separated, e.g. \"0.0, 0.2, 0.4, 0.6, 0.8, 1.0\""
-#~ msgstr "逗号分隔,例如\"0.0, 0.2, 0.4, 0.6, 0.8, 1.0\""
-
-#~ msgid "Temperature:"
-#~ msgstr "温度:"
-
-#~ msgid "Please translate each text sent to you from English to Spanish."
-#~ msgstr "请将发送给您的每段文本从英语翻译成西班牙语。"
-
-#~ msgid "Translation error, see logs!"
-#~ msgstr "翻译出错,请查看日志!"
-
-#~ msgid "Snap permission notice"
-#~ msgstr "快照权限通知"
-
-#~ msgid ""
-#~ "Detected missing permissions, please check that snap permissions have "
-#~ "been granted"
-#~ msgstr "检测到缺少权限,请检查是否已获得快照权限"
-
-#~ msgid ""
-#~ "To enable necessary permissions run the following commands in the terminal"
-#~ msgstr "要启用必要的权限,请在终端中运行以下命令"
-
-#~ msgid "Close"
-#~ msgstr "关闭"
-
-#~ msgid "Enter instructions for AI on how to translate..."
-#~ msgstr "输入AI如何翻译的说明..."
-
-#~ msgid "Enter target characters per subtitle:"
-#~ msgstr "为每个字幕输入目标字符:"
-
-#~ msgid "ID"
-#~ msgstr "ID"
-
-#~ msgid "Downloading model (0%, unknown time remaining)"
-#~ msgstr "正在下载模型 (0%, 剩余时间未知)"
diff --git a/buzz/locale/zh_TW/LC_MESSAGES/buzz.po b/buzz/locale/zh_TW/LC_MESSAGES/buzz.po
deleted file mode 100644
index 93d4afcf..00000000
--- a/buzz/locale/zh_TW/LC_MESSAGES/buzz.po
+++ /dev/null
@@ -1,1629 +0,0 @@
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
-# This file is distributed under the same license as the PACKAGE package.
-# FIRST AUTHOR , YEAR.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: \n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2026-03-07 20:20+0200\n"
-"PO-Revision-Date: 2023-05-01 15:45+0800\n"
-"Last-Translator: \n"
-"Language-Team: Lamb\n"
-"Language: zh_TW\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Generator: Poedit 3.2.2\n"
-
-#: buzz/widgets/import_url_dialog.py buzz/settings/shortcut.py
-msgid "Import URL"
-msgstr "匯入網址"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "https://example.com/audio.mp3"
-msgstr "https://example.com/audio.mp3"
-
-#: buzz/widgets/import_url_dialog.py
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-#: buzz/widgets/main_window.py
-msgid "Ok"
-msgstr "確定"
-
-#: buzz/widgets/import_url_dialog.py
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-#: buzz/widgets/model_download_progress_dialog.py buzz/widgets/main_window.py
-msgid "Cancel"
-msgstr "取消"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "URL:"
-msgstr "網址:"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "Invalid URL"
-msgstr "無效的網址"
-
-#: buzz/widgets/import_url_dialog.py
-msgid "The URL you entered is invalid."
-msgstr "您輸入的網址無效。"
-
-#: buzz/widgets/presentation_window.py
-msgid "Live Transcript Presentation"
-msgstr "即時轉錄簡報"
-
-#: buzz/widgets/preferences_dialog/shortcuts_editor_preferences_widget.py
-msgid "Reset to Defaults"
-msgstr "重設為預設值"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "English"
-msgstr "英語"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Catalan"
-msgstr "加泰隆尼亞語"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Danish"
-msgstr "丹麥語"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Dutch"
-msgstr "荷蘭語"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "German"
-msgstr "德語"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Spanish"
-msgstr "西班牙語"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Italian"
-msgstr "義大利語"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Japanese"
-msgstr "日語"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Latvian"
-msgstr "拉脫維亞語"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Polish"
-msgstr "波蘭語"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Portuguese (Brazil)"
-msgstr "葡萄牙語(巴西)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/transcriber/transcriber.py
-msgid "Ukrainian"
-msgstr "烏克蘭語"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Chinese (Simplified)"
-msgstr "中文(簡體)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Chinese (Traditional)"
-msgstr "中文(繁體)"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Restart required!"
-msgstr "需要重新啟動!"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Ui Language"
-msgstr "介面語言"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Font Size"
-msgstr "字體大小"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Test"
-msgstr "測試"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API key"
-msgstr "OpenAI API 金鑰"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI base url"
-msgstr "OpenAI 基礎網址"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API model"
-msgstr "OpenAI API 模型"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Default export file name"
-msgstr "預設匯出檔案名稱"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Enable live recording transcription export"
-msgstr "啟用即時錄製轉錄匯出"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Browse"
-msgstr "瀏覽"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Export folder"
-msgstr "匯出資料夾"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Live recording mode"
-msgstr "即時錄製模式"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Note: Live recording export settings will be moved to the Advanced Settings "
-"in the Live Recording screen in a future version."
-msgstr "注意:即時錄製匯出設定將在未來版本中移至即時錄製畫面的進階設定中。"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Use 8-bit quantization to reduce memory usage"
-msgstr "使用 8 位元量化以降低記憶體使用量"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Applies to Huggingface and Faster Whisper models. Reduces GPU memory usage "
-"but may slightly decrease transcription quality."
-msgstr ""
-"適用於 Huggingface 和 Faster Whisper 模型。可降低 GPU 記憶體使用量,但可能略"
-"微降低轉錄品質。"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Reduce GPU RAM"
-msgstr "降低 GPU 記憶體"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Use only CPU and disable GPU acceleration"
-msgstr "僅使用 CPU 並停用 GPU 加速"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Set this if larger models do not fit your GPU memory and Buzz crashes"
-msgstr "若較大的模型無法放入 GPU 記憶體且 Buzz 當機,請啟用此選項"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Disable GPU"
-msgstr "停用 GPU"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "OpenAI API Key Test"
-msgstr "OpenAI API 金鑰測試"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"Your API key is valid. Buzz will use this key to perform Whisper API "
-"transcriptions and AI translations."
-msgstr "您的 API 金鑰有效。Buzz 將使用此金鑰執行 Whisper API 轉錄和 AI 翻譯。"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid "Invalid API key"
-msgstr "無效的 API 金鑰"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"API supports only base64 characters (A-Za-z0-9+/=_-). Other characters in "
-"API key may cause errors."
-msgstr ""
-"API 僅支援 base64 字元(A-Za-z0-9+/=_-)。API 金鑰中的其他字元可能會導致錯"
-"誤。"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Select Export Folder"
-msgstr "選擇匯出資料夾"
-
-#: buzz/widgets/preferences_dialog/general_preferences_widget.py
-msgid ""
-"OpenAI API returned invalid response. Please check the API url or your key. "
-"Transcription and translation may still work if the API does not support key "
-"validation."
-msgstr ""
-"OpenAI API 傳回無效回應。請檢查 API 網址或您的金鑰。若 API 不支援金鑰驗證,轉"
-"錄和翻譯仍可能正常運作。"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Enable folder watch"
-msgstr "啟用資料夾監視"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Delete processed files"
-msgstr "刪除已處理的檔案"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Input folder"
-msgstr "輸入資料夾"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Output folder"
-msgstr "輸出資料夾"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Select Input Folder"
-msgstr "選擇輸入資料夾"
-
-#: buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
-msgid "Select Output Folder"
-msgstr "選擇輸出資料夾"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Preferences"
-msgstr "偏好設定"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "General"
-msgstr "一般"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Models"
-msgstr "模型"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Shortcuts"
-msgstr "快捷鍵"
-
-#: buzz/widgets/preferences_dialog/preferences_dialog.py
-msgid "Folder Watch"
-msgstr "資料夾監視"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Group"
-msgstr "群組"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Huggingface ID of a Faster whisper model"
-msgstr "Faster Whisper 模型的 Huggingface ID"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download"
-msgstr "下載"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Show file location"
-msgstr "顯示檔案位置"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Delete"
-msgstr "刪除"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Downloaded"
-msgstr "已下載"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Available for Download"
-msgstr "可供下載"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download link to Whisper.cpp ggml model file"
-msgstr "Whisper.cpp ggml 模型檔案的下載連結"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Delete Model"
-msgstr "刪除模型"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Are you sure you want to delete the selected model?"
-msgstr "您確定要刪除所選模型嗎?"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-msgid "Download failed"
-msgstr "下載失敗"
-
-#: buzz/widgets/preferences_dialog/models_preferences_widget.py
-#: buzz/widgets/transcription_tasks_table_widget.py
-#: buzz/widgets/update_dialog.py buzz/widgets/main_window.py
-#: buzz/model_loader.py
-msgid "Error"
-msgstr "錯誤"
-
-#: buzz/widgets/record_button.py buzz/widgets/main_window_toolbar.py
-msgid "Record"
-msgstr "錄製"
-
-#: buzz/widgets/record_button.py
-msgid "Stop"
-msgstr "停止"
-
-#: buzz/widgets/transcriber/languages_combo_box.py
-#: buzz/transcriber/transcriber.py
-msgid "Detect Language"
-msgstr "檢測語言"
-
-#: buzz/widgets/transcriber/mms_language_line_edit.py
-msgid "e.g., eng, fra, deu"
-msgstr "例如:eng、fra、deu"
-
-#: buzz/widgets/transcriber/mms_language_line_edit.py
-msgid ""
-"Enter an ISO 639-3 language code (3 letters).\n"
-"Examples: eng (English), fra (French), deu (German),\n"
-"spa (Spanish), lav (Latvian)"
-msgstr ""
-"請輸入 ISO 639-3 語言代碼(3 個字母)。\n"
-"範例:eng(英語)、fra(法語)、deu(德語)、\n"
-"spa(西班牙語)、lav(拉脫維亞語)"
-
-#: buzz/widgets/transcriber/file_transcriber_widget.py
-msgid "Run"
-msgstr "開始執行"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Model:"
-msgstr "模型:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-#: buzz/transcriber/recording_transcriber.py
-msgid "First time use of a model may take up to several minutest to load."
-msgstr "首次使用模型最多可能需要數分鐘載入。"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Api Key:"
-msgstr "API 金鑰:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Task:"
-msgstr "任務:"
-
-#: buzz/widgets/transcriber/transcription_options_group_box.py
-msgid "Language:"
-msgstr "語言:"
-
-#: buzz/widgets/transcriber/initial_prompt_text_edit.py
-msgid "Enter prompt..."
-msgstr "輸入提示..."
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Advanced Settings"
-msgstr "進階設定"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Speech recognition settings"
-msgstr "語音識別設定"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Initial Prompt:"
-msgstr "初始提示:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Translation settings"
-msgstr "翻譯設定"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Enable AI translation"
-msgstr "啟用 AI 翻譯"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "AI model:"
-msgstr "AI 模型:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid ""
-"Please translate each text sent to you from English to Spanish. Translation "
-"will be used in an automated system, please do not add any comments or "
-"notes, just the translation."
-msgstr ""
-"請將傳送給您的每段文字從英文翻譯成西班牙文。翻譯將用於自動化系統,請勿添加任"
-"何評論或備註,僅提供翻譯即可。"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Instructions for AI:"
-msgstr "AI 指令:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Recording settings"
-msgstr "錄製設定"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Silence threshold:"
-msgstr "靜音閾值:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Live recording mode:"
-msgstr "即時錄製模式:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Line separator:"
-msgstr "行分隔符號:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Transcription step:"
-msgstr "轉錄步長:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Hide unconfirmed"
-msgstr "隱藏未確認內容"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Enable live recording export"
-msgstr "啟用即時錄製匯出"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export folder:"
-msgstr "匯出資料夾:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export file name:"
-msgstr "匯出檔案名稱:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Text file (.txt)"
-msgstr "文字檔案(.txt)"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "CSV (.csv)"
-msgstr "CSV(.csv)"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid "Export file type:"
-msgstr "匯出檔案類型:"
-
-#: buzz/widgets/transcriber/advanced_settings_dialog.py
-msgid ""
-"Limit export entries\n"
-"(0 = export all):"
-msgstr ""
-"限制匯出筆數\n"
-"(0 = 全部匯出):"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Word-level timings"
-msgstr "單字級別的時間表達"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Extract speech"
-msgstr "提取語音"
-
-#: buzz/widgets/transcriber/file_transcription_form_widget.py
-msgid "Export:"
-msgstr "匯出:"
-
-#: buzz/widgets/transcriber/hugging_face_search_line_edit.py
-msgid "Huggingface ID of a model"
-msgstr "模型的 Huggingface ID"
-
-#: buzz/widgets/transcriber/advanced_settings_button.py
-msgid "Advanced..."
-msgstr "進階..."
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "New File Transcription"
-msgstr "新增檔案轉錄"
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "New URL Transcription"
-msgstr "新增網址轉錄"
-
-#: buzz/widgets/main_window_toolbar.py
-msgid "Open Transcript"
-msgstr "打開轉換結果"
-
-#: buzz/widgets/main_window_toolbar.py buzz/settings/shortcut.py
-msgid "Cancel Transcription"
-msgstr "取消錄製"
-
-#: buzz/widgets/main_window_toolbar.py buzz/widgets/main_window.py
-#: buzz/settings/shortcut.py
-msgid "Clear History"
-msgstr "清除歷史紀錄"
-
-#: buzz/widgets/main_window_toolbar.py buzz/widgets/update_dialog.py
-msgid "Update Available"
-msgstr "有可用更新"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "In Progress"
-msgstr "進行中"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Completed"
-msgstr "完成"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Failed"
-msgstr "失敗"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Canceled"
-msgstr "取消"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Queued"
-msgstr "排隊中"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "File Name / URL"
-msgstr "檔案名稱 / 網址"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Model"
-msgstr "模型"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Task"
-msgstr "任務"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Status"
-msgstr "狀態"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Date Completed"
-msgstr "完成日期"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Date Added"
-msgstr "新增日期"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Notes"
-msgstr "備註"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Reset Column Order"
-msgstr "重設欄位順序"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Restart Transcription"
-msgstr "重新開始轉錄"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Rename"
-msgstr "重新命名"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Add/Edit Notes"
-msgstr "新增/編輯備註"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Rename Transcription"
-msgstr "重新命名轉錄"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Enter new name:"
-msgstr "輸入新名稱:"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Enter some relevant notes for this transcription:"
-msgstr "為此轉錄輸入相關備註:"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Cannot Restart"
-msgstr "無法重新開始"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Only failed or canceled transcriptions can be restarted."
-msgstr "只有失敗或已取消的轉錄才能重新開始。"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Failed to restart transcription: {}"
-msgstr "重新開始轉錄失敗:{}"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid ""
-"Could not restart transcription: model not available and could not be "
-"downloaded."
-msgstr "無法重新開始轉錄:模型不可用且無法下載。"
-
-#: buzz/widgets/transcription_tasks_table_widget.py
-msgid "Could not restart transcription: transcriber worker not found."
-msgstr "無法重新開始轉錄:找不到轉錄工作器。"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Live Recording"
-msgstr "現場錄製"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Click Record to begin..."
-msgstr "點擊開始錄製"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Waiting for AI translation..."
-msgstr "等待 AI 翻譯..."
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Microphone:"
-msgstr "麥克風:"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Show in new window"
-msgstr "在新視窗中顯示"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Text Size:"
-msgstr "文字大小:"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Theme"
-msgstr "主題"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Light"
-msgstr "淺色"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Dark"
-msgstr "深色"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Custom"
-msgstr "自訂"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Text Color"
-msgstr "文字顏色"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Background Color"
-msgstr "背景顏色"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Fullscreen"
-msgstr "全螢幕"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy"
-msgstr "複製"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy transcription to clipboard"
-msgstr "複製轉錄內容到剪貼簿"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Nothing to copy!"
-msgstr "沒有可複製的內容!"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copy failed"
-msgstr "複製失敗"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Copied!"
-msgstr "已複製!"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Select Text Color"
-msgstr "選擇文字顏色"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "Select Background Color"
-msgstr "選擇背景顏色"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid "An error occurred while starting a new recording:"
-msgstr "開始新錄製出錯"
-
-#: buzz/widgets/recording_transcriber_widget.py
-msgid ""
-"Please check your audio devices or check the application logs for more "
-"information."
-msgstr "請檢查您的音頻設備或檢查應用程序日誌以獲取更多信息。"
-
-#: buzz/widgets/update_dialog.py
-msgid "A new version of Buzz is available!"
-msgstr "Buzz 有新版本可用!"
-
-#: buzz/widgets/update_dialog.py
-msgid "Current version:"
-msgstr "目前版本:"
-
-#: buzz/widgets/update_dialog.py
-msgid "New version:"
-msgstr "新版本:"
-
-#: buzz/widgets/update_dialog.py
-msgid "Release Notes:"
-msgstr "版本說明:"
-
-#: buzz/widgets/update_dialog.py
-msgid "Download and Install"
-msgstr "下載並安裝"
-
-#: buzz/widgets/update_dialog.py
-msgid "No download URL available for your platform."
-msgstr "您的平台沒有可用的下載連結。"
-
-#: buzz/widgets/update_dialog.py
-msgid "Downloading file {} of {}..."
-msgstr "正在下載第 {} 個檔案,共 {} 個..."
-
-#: buzz/widgets/update_dialog.py
-msgid "Downloading file {} of {} ({:.1f} MB / {:.1f} MB)..."
-msgstr "正在下載第 {} 個檔案,共 {} 個({:.1f} MB / {:.1f} MB)..."
-
-#: buzz/widgets/update_dialog.py
-msgid "Download Failed"
-msgstr "下載失敗"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to download the update: {}"
-msgstr "下載更新失敗:{}"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to save the installer: {}"
-msgstr "無法儲存安裝程式:{}"
-
-#: buzz/widgets/update_dialog.py
-msgid "Download complete!"
-msgstr "下載完成!"
-
-#: buzz/widgets/update_dialog.py
-msgid "Failed to run the installer: {}"
-msgstr "無法執行安裝程式:{}"
-
-#: buzz/widgets/about_dialog.py
-msgid "Check for updates"
-msgstr "檢查更新"
-
-#: buzz/widgets/about_dialog.py
-msgid "Show logs"
-msgstr "顯示日誌"
-
-#: buzz/widgets/about_dialog.py
-msgid "You're up to date!"
-msgstr "你是最新的!"
-
-#: buzz/widgets/audio_meter_widget.py
-msgid "Average volume"
-msgstr "平均音量"
-
-#: buzz/widgets/audio_meter_widget.py
-msgid "Queue"
-msgstr "佇列"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-msgid "Start"
-msgstr "開始"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-msgid "End"
-msgstr "結束"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Text"
-msgstr "文字"
-
-#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Translation"
-msgstr "翻譯"
-
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-msgid "View"
-msgstr "檢視"
-
-#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
-msgid "Timestamps"
-msgstr "時間戳記"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Export"
-msgstr "匯出"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Translate"
-msgstr "翻譯"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Resize"
-msgstr "調整大小"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Identify Speakers"
-msgstr "識別說話者"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Find"
-msgstr "尋找"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Show/Hide Search Bar (Ctrl+F)"
-msgstr "顯示/隱藏搜尋列(Ctrl+F)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Find:"
-msgstr "尋找:"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Enter text to find..."
-msgstr "輸入要尋找的文字..."
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Previous match (Shift+Enter)"
-msgstr "上一個符合項目(Shift+Enter)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Next match (Ctrl+Enter)"
-msgstr "下一個符合項目(Ctrl+Enter)"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Clear"
-msgstr "清除"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Playback Controls:"
-msgstr "播放控制:"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Loop Segment"
-msgstr "循環片段"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Enable/disable looping when clicking on transcript segments"
-msgstr "點擊轉錄片段時啟用/停用循環播放"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Follow Audio"
-msgstr "跟隨音訊"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid ""
-"Enable/disable following the current audio position in the transcript. When "
-"enabled, automatically scrolls to current text."
-msgstr "啟用/停用在轉錄稿中跟隨目前音訊位置。啟用後,自動捲動至目前文字。"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Scroll to Current"
-msgstr "捲動至目前位置"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Scroll to the currently spoken text"
-msgstr "捲動至目前正在說話的文字"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "1 of 100+ matches"
-msgstr "100 個以上符合項目中的第 1 個"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "1 of "
-msgstr "第 1 個,共 "
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " matches"
-msgstr " 個符合項目"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "No matches found"
-msgstr "找不到符合項目"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " of 100+ matches"
-msgstr ",共 100 個以上符合項目"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid " of "
-msgstr ",共 "
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "API Key Required"
-msgstr "需要 API 金鑰"
-
-#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py
-msgid "Please enter OpenAI API Key in preferences"
-msgstr "請在偏好設定中輸入 OpenAI API 金鑰"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend end time"
-msgstr "延長結束時間"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend endings by up to (seconds)"
-msgstr "最多延長結束時間(秒)"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Extend endings"
-msgstr "延長結尾"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Resize Options"
-msgstr "調整大小選項"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Desired subtitle length"
-msgstr "理想字幕長度"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Available only if word level timings were disabled during transcription"
-msgstr "僅在轉錄時停用單字級別時間戳記時可用"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge Options"
-msgstr "合併選項"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge by gap"
-msgstr "依間隔合併"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Split by punctuation"
-msgstr "依標點符號分割"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Split by max length"
-msgstr "依最大長度分割"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Merge"
-msgstr "合併"
-
-#: buzz/widgets/transcription_viewer/transcription_resizer_widget.py
-msgid "Available only if word level timings were enabled during transcription"
-msgstr "僅在轉錄時啟用單字級別時間戳記時可用"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid ""
-"Speaker identification is not available: failed to load required libraries."
-msgstr "說話者識別不可用:無法載入所需程式庫。"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "1/8 Collecting transcripts"
-msgstr "1/8 收集轉錄稿"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "2/8 Loading audio"
-msgstr "2/8 載入音訊"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "3/8 Loading alignment model"
-msgstr "3/8 載入對齊模型"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "3/8 Loading alignment model (retrying with cache...)"
-msgstr "3/8 載入對齊模型(正在使用快取重試...)"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid ""
-"Failed to load alignment model. Please check your internet connection and "
-"try again."
-msgstr "無法載入對齊模型。請檢查您的網路連線並重試。"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "4/8 Processing audio"
-msgstr "4/8 處理音訊"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "5/8 Preparing transcripts"
-msgstr "5/8 準備轉錄稿"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "6/8 Identifying speakers"
-msgstr "6/8 識別說話者"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "7/8 Mapping speakers to transcripts"
-msgstr "7/8 將說話者對應到轉錄稿"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "8/8 Identification done"
-msgstr "8/8 識別完成"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "0/0 Error identifying speakers"
-msgstr "0/0 識別說話者時發生錯誤"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Step 1: Identify speakers"
-msgstr "步驟 1:識別說話者"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Identify"
-msgstr "識別"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Ready to identify speakers"
-msgstr "準備識別說話者"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Audio file not found"
-msgstr "找不到音訊檔案"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Step 2: Name speakers"
-msgstr "步驟 2:為說話者命名"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Play sample"
-msgstr "播放範例"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Merge speaker sentences"
-msgstr "合併說話者句子"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Save"
-msgstr "儲存"
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Cancelling..."
-msgstr "取消中..."
-
-#: buzz/widgets/transcription_viewer/speaker_identification_widget.py
-msgid "Cancelled"
-msgstr "已取消"
-
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Save File"
-msgstr "儲存檔案"
-
-#: buzz/widgets/transcription_viewer/export_transcription_menu.py
-msgid "Text files"
-msgstr "文字檔案"
-
-#: buzz/widgets/model_download_progress_dialog.py
-msgid "Downloading model"
-msgstr "正在下載模型"
-
-#: buzz/widgets/model_download_progress_dialog.py
-msgid "remaining"
-msgstr "剩餘"
-
-#: buzz/widgets/menu_bar.py
-msgid "Import File..."
-msgstr "匯入檔案..."
-
-#: buzz/widgets/menu_bar.py
-msgid "Import URL..."
-msgstr "匯入網址..."
-
-#: buzz/widgets/menu_bar.py
-msgid "Import Folder..."
-msgstr "導入資料夾..."
-
-#: buzz/widgets/menu_bar.py
-msgid "About"
-msgstr "關於"
-
-#: buzz/widgets/menu_bar.py
-msgid "Preferences..."
-msgstr "偏好設定..."
-
-#: buzz/widgets/menu_bar.py
-msgid "Help"
-msgstr "幫助"
-
-#: buzz/widgets/menu_bar.py
-msgid "File"
-msgstr "檔案"
-
-#: buzz/widgets/main_window.py
-msgid ""
-"Are you sure you want to delete the selected transcription(s)? This action "
-"cannot be undone."
-msgstr "您確定要刪除所選錄製嗎?此操作無法撤消。"
-
-#: buzz/widgets/main_window.py
-msgid "Select audio file"
-msgstr "選擇聲音檔案"
-
-#: buzz/widgets/main_window.py
-msgid "Select folder"
-msgstr "選擇資料夾"
-
-#: buzz/widgets/main_window.py
-msgid "Unable to save OpenAI API key to keyring"
-msgstr "無法將 OpenAI API 金鑰儲存至鑰匙圈"
-
-#: buzz/transcriber/local_whisper_cpp_server_transcriber.py
-#: buzz/transcriber/recording_transcriber.py
-msgid "Whisper server failed to start. Check logs for details."
-msgstr "Whisper 伺服器啟動失敗。請查看日誌以了解詳情。"
-
-#: buzz/transcriber/local_whisper_cpp_server_transcriber.py
-#: buzz/transcriber/recording_transcriber.py
-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 伺服器因記憶體不足而啟動失敗。請改用較小的模型重試。若要強制使用 CPU "
-"模式,請設定環境變數 BUZZ_FORCE_CPU=TRUE。"
-
-#: buzz/transcriber/transcriber.py
-msgid "Translate to English"
-msgstr "翻譯為英語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Transcribe"
-msgstr "轉錄"
-
-#: buzz/transcriber/transcriber.py
-msgid "Chinese"
-msgstr "中文"
-
-#: buzz/transcriber/transcriber.py
-msgid "Russian"
-msgstr "俄語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Korean"
-msgstr "韓語"
-
-#: buzz/transcriber/transcriber.py
-msgid "French"
-msgstr "法語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Portuguese"
-msgstr "葡萄牙語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Turkish"
-msgstr "土耳其語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Arabic"
-msgstr "阿拉伯語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Swedish"
-msgstr "瑞典語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Indonesian"
-msgstr "印尼語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hindi"
-msgstr "印地語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Finnish"
-msgstr "芬蘭語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Vietnamese"
-msgstr "越南語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hebrew"
-msgstr "希伯來語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Greek"
-msgstr "希臘語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malay"
-msgstr "馬來語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Czech"
-msgstr "捷克語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Romanian"
-msgstr "羅馬尼亞語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hungarian"
-msgstr "匈牙利語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tamil"
-msgstr "泰米爾語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Norwegian"
-msgstr "挪威語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Thai"
-msgstr "泰語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Urdu"
-msgstr "烏爾都語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Croatian"
-msgstr "克羅埃西亞語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bulgarian"
-msgstr "保加利亞語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lithuanian"
-msgstr "立陶宛語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Latin"
-msgstr "拉丁語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Maori"
-msgstr "毛利語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malayalam"
-msgstr "馬拉雅拉姆語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Welsh"
-msgstr "威爾斯語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Slovak"
-msgstr "斯洛伐克語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Telugu"
-msgstr "泰盧固語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Persian"
-msgstr "波斯語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bengali"
-msgstr "孟加拉語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Serbian"
-msgstr "塞爾維亞語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Azerbaijani"
-msgstr "亞塞拜然語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Slovenian"
-msgstr "斯洛維尼亞語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Kannada"
-msgstr "卡納達語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Estonian"
-msgstr "愛沙尼亞語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Macedonian"
-msgstr "馬其頓語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Breton"
-msgstr "布列塔尼語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Basque"
-msgstr "巴斯克語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Icelandic"
-msgstr "冰島語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Armenian"
-msgstr "亞美尼亞語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Nepali"
-msgstr "尼泊爾語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Mongolian"
-msgstr "蒙古語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bosnian"
-msgstr "波士尼亞語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Kazakh"
-msgstr "哈薩克語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Albanian"
-msgstr "阿爾巴尼亞語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Swahili"
-msgstr "斯瓦希里語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Galician"
-msgstr "加利西亞語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Marathi"
-msgstr "馬拉地語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Punjabi"
-msgstr "旁遮普語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sinhala"
-msgstr "僧伽羅語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Khmer"
-msgstr "高棉語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Shona"
-msgstr "紹納語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Yoruba"
-msgstr "約魯巴語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Somali"
-msgstr "索馬利語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Afrikaans"
-msgstr "南非荷蘭語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Occitan"
-msgstr "奧克語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Georgian"
-msgstr "喬治亞語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Belarusian"
-msgstr "白俄羅斯語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tajik"
-msgstr "塔吉克語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sindhi"
-msgstr "信德語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Gujarati"
-msgstr "古吉拉特語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Amharic"
-msgstr "阿姆哈拉語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Yiddish"
-msgstr "意第緒語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lao"
-msgstr "寮語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Uzbek"
-msgstr "烏茲別克語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Faroese"
-msgstr "法羅語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Haitian Creole"
-msgstr "海地克里奧爾語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Pashto"
-msgstr "普什圖語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Turkmen"
-msgstr "土庫曼語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Nynorsk"
-msgstr "新挪威語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Maltese"
-msgstr "馬爾他語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sanskrit"
-msgstr "梵語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Luxembourgish"
-msgstr "盧森堡語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Myanmar"
-msgstr "緬甸語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tibetan"
-msgstr "藏語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tagalog"
-msgstr "他加祿語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Malagasy"
-msgstr "馬拉加斯語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Assamese"
-msgstr "阿薩姆語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Tatar"
-msgstr "韃靼語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hawaiian"
-msgstr "夏威夷語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Lingala"
-msgstr "林加拉語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Hausa"
-msgstr "豪薩語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Bashkir"
-msgstr "巴什基爾語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Javanese"
-msgstr "爪哇語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Sundanese"
-msgstr "巽他語"
-
-#: buzz/transcriber/transcriber.py
-msgid "Cantonese"
-msgstr "粵語"
-
-#: buzz/transcriber/recording_transcriber.py buzz/model_loader.py
-msgid "A connection error occurred"
-msgstr "發生連線錯誤"
-
-#: buzz/transcriber/recording_transcriber.py
-msgid "Starting Whisper.cpp..."
-msgstr "正在啟動 Whisper.cpp..."
-
-#: buzz/transcriber/recording_transcriber.py
-msgid "Starting transcription..."
-msgstr "正在開始轉錄..."
-
-#: buzz/settings/shortcut.py
-msgid "Open Record Window"
-msgstr "開啟錄製視窗"
-
-#: buzz/settings/shortcut.py
-msgid "Import File"
-msgstr "匯入檔案"
-
-#: buzz/settings/shortcut.py
-msgid "Open Preferences Window"
-msgstr "開啟偏好設定視窗"
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Text"
-msgstr "檢視轉錄文字"
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Translation"
-msgstr "檢視轉錄翻譯"
-
-#: buzz/settings/shortcut.py
-msgid "View Transcript Timestamps"
-msgstr "檢視轉錄時間戳記"
-
-#: buzz/settings/shortcut.py
-msgid "Search Transcript"
-msgstr "搜尋轉錄稿"
-
-#: buzz/settings/shortcut.py
-msgid "Go to Next Transcript Search Result"
-msgstr "移至下一個轉錄搜尋結果"
-
-#: buzz/settings/shortcut.py
-msgid "Go to Previous Transcript Search Result"
-msgstr "移至上一個轉錄搜尋結果"
-
-#: buzz/settings/shortcut.py
-msgid "Scroll to Current Text"
-msgstr "捲動至目前文字"
-
-#: buzz/settings/shortcut.py
-msgid "Play/Pause Audio"
-msgstr "播放/暫停音訊"
-
-#: buzz/settings/shortcut.py
-msgid "Replay Current Segment"
-msgstr "重播目前片段"
-
-#: buzz/settings/shortcut.py
-msgid "Toggle Playback Controls"
-msgstr "切換播放控制項"
-
-#: buzz/settings/shortcut.py
-msgid "Decrease Segment Start Time"
-msgstr "縮短片段開始時間"
-
-#: buzz/settings/shortcut.py
-msgid "Increase Segment Start Time"
-msgstr "延長片段開始時間"
-
-#: buzz/settings/shortcut.py
-msgid "Decrease Segment End Time"
-msgstr "縮短片段結束時間"
-
-#: buzz/settings/shortcut.py
-msgid "Increase Segment End Time"
-msgstr "延長片段結束時間"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append below"
-msgstr "附加至下方"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append above"
-msgstr "附加至上方"
-
-#: buzz/settings/recording_transcriber_mode.py
-msgid "Append and correct"
-msgstr "附加並校正"
-
-#: buzz/file_transcriber_queue_worker.py
-msgid ""
-"Speech extraction failed! Check your internet connection — a model may need "
-"to be downloaded."
-msgstr "語音提取失敗!請檢查您的網路連線——可能需要下載模型。"
-
-#~ msgid "Comma-separated, e.g. \"0.0, 0.2, 0.4, 0.6, 0.8, 1.0\""
-#~ msgstr "逗號分隔,例如\"0.0, 0.2, 0.4, 0.6, 0.8, 1.0\""
-
-#~ msgid "Temperature:"
-#~ msgstr "溫度:"
-
-#~ msgid "Please translate each text sent to you from English to Spanish."
-#~ msgstr "請將傳送給您的每段文字從英語翻譯為西班牙語。"
-
-#~ msgid "Translation error, see logs!"
-#~ msgstr "翻譯錯誤,請查看日誌!"
-
-#~ msgid "ID"
-#~ msgstr "ID"
-
-#~ msgid "Downloading model (0%, unknown time remaining)"
-#~ msgstr "正在下載模型 (0%, unknown 剩餘時間)"
diff --git a/buzz/model_loader.py b/buzz/model_loader.py
deleted file mode 100644
index 224bd6d3..00000000
--- a/buzz/model_loader.py
+++ /dev/null
@@ -1,1009 +0,0 @@
-import enum
-import hashlib
-import logging
-import os
-import time
-import threading
-import shutil
-import subprocess
-import sys
-import ssl
-import warnings
-import platform
-
-# Fix SSL certificate verification for bundled applications (macOS, Windows).
-# This must be done before importing libraries that make HTTPS requests.
-try:
- import certifi
- _certifi_ca_bundle = certifi.where()
- os.environ.setdefault("REQUESTS_CA_BUNDLE", _certifi_ca_bundle)
- os.environ.setdefault("SSL_CERT_FILE", _certifi_ca_bundle)
- os.environ.setdefault("SSL_CERT_DIR", os.path.dirname(_certifi_ca_bundle))
- # Also update the default SSL context for urllib
- ssl._create_default_https_context = lambda: ssl.create_default_context(cafile=_certifi_ca_bundle)
-except ImportError:
- _certifi_ca_bundle = None
-
-import requests
-import whisper
-import huggingface_hub
-import zipfile
-from dataclasses import dataclass
-from typing import Optional, List
-
-from PyQt6.QtCore import QObject, pyqtSignal, QRunnable
-from platformdirs import user_cache_dir
-from huggingface_hub.errors import LocalEntryNotFoundError
-
-from buzz.locale import _
-
-# Configure huggingface_hub to use certifi certificates directly.
-# This is more reliable than environment variables for frozen apps.
-if _certifi_ca_bundle is not None:
- try:
- from huggingface_hub import configure_http_backend
-
- def _hf_session_factory() -> requests.Session:
- session = requests.Session()
- session.verify = _certifi_ca_bundle
- return session
-
- configure_http_backend(backend_factory=_hf_session_factory)
- except ImportError:
- # configure_http_backend not available in older huggingface_hub versions
- pass
- except Exception as e:
- logging.debug(f"Failed to configure huggingface_hub HTTP backend: {e}")
-
-# On Windows, creating symlinks requires special privileges (Developer Mode or
-# SeCreateSymbolicLinkPrivilege). Monkey-patch huggingface_hub to use file
-# copying instead of symlinks to avoid [WinError 1314] errors.
-if sys.platform == "win32":
- try:
- from huggingface_hub import file_download
- from pathlib import Path
-
- _original_create_symlink = file_download._create_symlink
-
- def _windows_create_symlink(src: Path, dst: Path, new_blob: bool = False) -> None:
- """Windows-compatible replacement that copies instead of symlinking."""
- src = Path(src)
- dst = Path(dst)
-
- # If dst already exists and is correct, skip
- if dst.exists():
- if dst.is_symlink():
- # Existing symlink - leave it
- return
- if dst.is_file():
- # Check if it's the same file
- if dst.stat().st_size == src.stat().st_size:
- return
-
- dst.parent.mkdir(parents=True, exist_ok=True)
-
- # Try symlink first (works if Developer Mode is enabled)
- try:
- dst.unlink(missing_ok=True)
- os.symlink(src, dst)
- return
- except OSError:
- pass
-
- # Fallback: copy the file instead
- dst.unlink(missing_ok=True)
- shutil.copy2(src, dst)
-
- file_download._create_symlink = _windows_create_symlink
- logging.debug("Patched huggingface_hub to use file copying on Windows")
- except Exception as e:
- logging.warning(f"Failed to patch huggingface_hub for Windows: {e}")
-
-
-model_root_dir = user_cache_dir("Buzz")
-model_root_dir = os.path.join(model_root_dir, "models")
-model_root_dir = os.getenv("BUZZ_MODEL_ROOT", model_root_dir)
-os.makedirs(model_root_dir, exist_ok=True)
-
-logging.debug("Model root directory: %s", model_root_dir)
-
-class WhisperModelSize(str, enum.Enum):
- TINY = "tiny"
- TINYEN = "tiny.en"
- BASE = "base"
- BASEEN = "base.en"
- SMALL = "small"
- SMALLEN = "small.en"
- MEDIUM = "medium"
- MEDIUMEN = "medium.en"
- LARGE = "large"
- LARGEV2 = "large-v2"
- LARGEV3 = "large-v3"
- LARGEV3TURBO = "large-v3-turbo"
- CUSTOM = "custom"
- LUMII = "lumii"
-
- def to_faster_whisper_model_size(self) -> str:
- if self == WhisperModelSize.LARGE:
- return "large-v1"
- return self.value
-
- def to_whisper_cpp_model_size(self) -> str:
- if self == WhisperModelSize.LARGE:
- return "large-v1"
- return self.value
-
- def __str__(self):
- return self.value.capitalize()
-
-# Approximate expected file sizes for Whisper models (based on actual .pt file sizes)
-WHISPER_MODEL_SIZES = {
- WhisperModelSize.TINY: 72 * 1024 * 1024, # ~73 MB actual
- WhisperModelSize.TINYEN: 72 * 1024 * 1024, # ~73 MB actual
- WhisperModelSize.BASE: 138 * 1024 * 1024, # ~139 MB actual
- WhisperModelSize.BASEEN: 138 * 1024 * 1024, # ~139 MB actual
- WhisperModelSize.SMALL: 460 * 1024 * 1024, # ~462 MB actual
- WhisperModelSize.SMALLEN: 460 * 1024 * 1024, # ~462 MB actual
- WhisperModelSize.MEDIUM: 1500 * 1024 * 1024, # ~1.5 GB actual
- WhisperModelSize.MEDIUMEN: 1500 * 1024 * 1024, # ~1.5 GB actual
- WhisperModelSize.LARGE: 2870 * 1024 * 1024, # ~2.9 GB actual
- WhisperModelSize.LARGEV2: 2870 * 1024 * 1024, # ~2.9 GB actual
- WhisperModelSize.LARGEV3: 2870 * 1024 * 1024, # ~2.9 GB actual
- WhisperModelSize.LARGEV3TURBO: 1550 * 1024 * 1024, # ~1.6 GB actual (turbo is smaller)
-}
-
-def get_expected_whisper_model_size(size: WhisperModelSize) -> Optional[int]:
- """Get expected file size for a Whisper model without network request."""
- return WHISPER_MODEL_SIZES.get(size, None)
-
-class ModelType(enum.Enum):
- WHISPER = "Whisper"
- WHISPER_CPP = "Whisper.cpp"
- HUGGING_FACE = "Hugging Face"
- FASTER_WHISPER = "Faster Whisper"
- OPEN_AI_WHISPER_API = "OpenAI Whisper API"
-
- @property
- def supports_initial_prompt(self):
- return self in (
- ModelType.WHISPER,
- ModelType.WHISPER_CPP,
- ModelType.OPEN_AI_WHISPER_API,
- ModelType.FASTER_WHISPER,
- )
-
- def is_available(self):
- if (
- # Hide Faster Whisper option on macOS x86_64
- # See: https://github.com/SYSTRAN/faster-whisper/issues/541
- (self == ModelType.FASTER_WHISPER
- and platform.system() == "Darwin" and platform.machine() == "x86_64")
- ):
- return False
- return True
-
- def is_manually_downloadable(self):
- return self in (
- ModelType.WHISPER,
- ModelType.WHISPER_CPP,
- ModelType.FASTER_WHISPER,
- )
-
-
-HUGGING_FACE_MODEL_ALLOW_PATTERNS = [
- "model.safetensors", # largest by size first
- "pytorch_model.bin",
- "model-00001-of-00002.safetensors",
- "model-00002-of-00002.safetensors",
- "model.safetensors.index.json",
- "added_tokens.json",
- "config.json",
- "generation_config.json",
- "merges.txt",
- "normalizer.json",
- "preprocessor_config.json",
- "special_tokens_map.json",
- "tokenizer.json",
- "tokenizer_config.json",
- "vocab.json",
-]
-
-# MMS models use different patterns - adapters are downloaded on-demand by transformers
-MMS_MODEL_ALLOW_PATTERNS = [
- "model.safetensors",
- "pytorch_model.bin",
- "config.json",
- "preprocessor_config.json",
- "tokenizer_config.json",
- "vocab.json",
- "special_tokens_map.json",
- "added_tokens.json",
-]
-
-# ISO 639-1 to ISO 639-3 language code mapping for MMS models
-ISO_639_1_TO_3 = {
- "en": "eng", "fr": "fra", "de": "deu", "es": "spa", "it": "ita",
- "pt": "por", "ru": "rus", "ja": "jpn", "ko": "kor", "zh": "cmn",
- "ar": "ara", "hi": "hin", "nl": "nld", "pl": "pol", "sv": "swe",
- "tr": "tur", "uk": "ukr", "vi": "vie", "cs": "ces", "da": "dan",
- "fi": "fin", "el": "ell", "he": "heb", "hu": "hun", "id": "ind",
- "ms": "zsm", "no": "nob", "ro": "ron", "sk": "slk", "th": "tha",
- "bg": "bul", "ca": "cat", "hr": "hrv", "lt": "lit", "lv": "lav",
- "sl": "slv", "et": "est", "sr": "srp", "tl": "tgl", "bn": "ben",
- "ta": "tam", "te": "tel", "mr": "mar", "gu": "guj", "kn": "kan",
- "ml": "mal", "pa": "pan", "ur": "urd", "fa": "pes", "sw": "swh",
- "af": "afr", "az": "azj", "be": "bel", "bs": "bos", "cy": "cym",
- "eo": "epo", "eu": "eus", "ga": "gle", "gl": "glg", "hy": "hye",
- "is": "isl", "ka": "kat", "kk": "kaz", "km": "khm", "lo": "lao",
- "mk": "mkd", "mn": "khk", "my": "mya", "ne": "npi", "si": "sin",
- "sq": "sqi", "uz": "uzn", "zu": "zul", "am": "amh", "jw": "jav",
- "la": "lat", "so": "som", "su": "sun", "tt": "tat", "yo": "yor",
-}
-
-
-def map_language_to_mms(language_code: str) -> str:
- """Convert ISO 639-1 code to ISO 639-3 code for MMS models.
-
- If the code is already 3 letters, returns it as-is.
- If the code is not found in the mapping, returns as-is.
- """
- if not language_code:
- return "eng" # Default to English for MMS
- if len(language_code) == 3:
- return language_code # Already ISO 639-3
- return ISO_639_1_TO_3.get(language_code, language_code)
-
-
-def is_mms_model(model_id: str) -> bool:
- """Detect if a HuggingFace model is an MMS (Massively Multilingual Speech) model.
-
- Detection criteria:
- 1. Model ID contains "mms-" (e.g., facebook/mms-1b-all)
- 2. Model config has model_type == "wav2vec2" with adapter architecture
- """
- if not model_id:
- return False
-
- # Fast check: model ID pattern
- if "mms-" in model_id.lower():
- return True
-
- # For cached/downloaded models, check config.json
- try:
- import json
- config_path = huggingface_hub.hf_hub_download(
- model_id, "config.json", local_files_only=True, cache_dir=model_root_dir
- )
- with open(config_path) as f:
- config = json.load(f)
- # MMS models have model_type "wav2vec2" and use adapter architecture
- return (config.get("model_type") == "wav2vec2"
- and config.get("adapter_attn_dim") is not None)
- except Exception:
- return False
-
-
-@dataclass()
-class TranscriptionModel:
- def __init__(
- self,
- model_type: ModelType = ModelType.WHISPER,
- whisper_model_size: Optional[WhisperModelSize] = WhisperModelSize.TINY,
- hugging_face_model_id: Optional[str] = ""
- ):
- self.model_type = model_type
- self.whisper_model_size = whisper_model_size
- self.hugging_face_model_id = hugging_face_model_id
-
- def __str__(self):
- match self.model_type:
- case ModelType.WHISPER:
- return f"Whisper ({self.whisper_model_size})"
- case ModelType.WHISPER_CPP:
- return f"Whisper.cpp ({self.whisper_model_size})"
- case ModelType.HUGGING_FACE:
- return f"Hugging Face ({self.hugging_face_model_id})"
- case ModelType.FASTER_WHISPER:
- return f"Faster Whisper ({self.whisper_model_size})"
- case ModelType.OPEN_AI_WHISPER_API:
- return "OpenAI Whisper API"
- case _:
- raise Exception("Unknown model type")
-
- def is_deletable(self):
- return (
- self.model_type == ModelType.WHISPER
- or self.model_type == ModelType.WHISPER_CPP
- or self.model_type == ModelType.FASTER_WHISPER
- ) and self.get_local_model_path() is not None
-
- def open_file_location(self):
- model_path = self.get_local_model_path()
-
- if (self.model_type == ModelType.HUGGING_FACE
- or self.model_type == ModelType.FASTER_WHISPER):
- model_path = os.path.dirname(model_path)
-
- if model_path is None:
- return
- self.open_path(path=os.path.dirname(model_path))
-
- @staticmethod
- def default():
- model_type = next(
- model_type for model_type in ModelType if model_type.is_available()
- )
- return TranscriptionModel(model_type=model_type)
-
- @staticmethod
- def open_path(path: str):
- if sys.platform == "win32":
- os.startfile(path)
- else:
- opener = "open" if sys.platform == "darwin" else "xdg-open"
- subprocess.call([opener, path])
-
- def delete_local_file(self):
- model_path = self.get_local_model_path()
-
- if self.model_type in (ModelType.HUGGING_FACE,
- ModelType.FASTER_WHISPER):
- # Go up two directories to get the huggingface cache root for this model
- # Structure: models--repo--name/snapshots/xxx/files
- model_path = os.path.dirname(os.path.dirname(model_path))
-
- logging.debug("Deleting model directory: %s", model_path)
-
- shutil.rmtree(model_path, ignore_errors=True)
- return
-
- if self.model_type == ModelType.WHISPER_CPP:
- if self.whisper_model_size == WhisperModelSize.CUSTOM:
- # Custom models are stored as a single .bin file directly in model_root_dir
- logging.debug("Deleting model file: %s", model_path)
- os.remove(model_path)
- else:
- # Non-custom models are downloaded via huggingface_hub.
- # Multiple models share the same repo directory, so we only delete
- # the specific model files, not the entire directory.
- logging.debug("Deleting model file: %s", model_path)
- os.remove(model_path)
-
- # Also delete CoreML files if they exist (.mlmodelc.zip and extracted directory)
- model_dir = os.path.dirname(model_path)
- model_name = self.whisper_model_size.to_whisper_cpp_model_size()
- coreml_zip = os.path.join(model_dir, f"ggml-{model_name}-encoder.mlmodelc.zip")
- coreml_dir = os.path.join(model_dir, f"ggml-{model_name}-encoder.mlmodelc")
-
- if os.path.exists(coreml_zip):
- logging.debug("Deleting CoreML zip: %s", coreml_zip)
- os.remove(coreml_zip)
- if os.path.exists(coreml_dir):
- logging.debug("Deleting CoreML directory: %s", coreml_dir)
- shutil.rmtree(coreml_dir, ignore_errors=True)
- return
-
- logging.debug("Deleting model file: %s", model_path)
- os.remove(model_path)
-
- def get_local_model_path(self) -> Optional[str]:
- if self.model_type == ModelType.WHISPER_CPP:
- file_path = get_whisper_cpp_file_path(size=self.whisper_model_size)
- if not file_path or not os.path.exists(file_path) or not os.path.isfile(file_path):
- return None
- return file_path
-
- if self.model_type == ModelType.WHISPER:
- file_path = get_whisper_file_path(size=self.whisper_model_size)
- if not os.path.exists(file_path) or not os.path.isfile(file_path):
- return None
-
- file_size = os.path.getsize(file_path)
-
- expected_size = get_expected_whisper_model_size(self.whisper_model_size)
-
- if expected_size is not None:
- if file_size < expected_size * 0.95: # Allow 5% tolerance for file system differences
- return None
- return file_path
- else:
- # For unknown model size
- if file_size < 50 * 1024 * 1024:
- return None
-
- return file_path
-
- if self.model_type == ModelType.FASTER_WHISPER:
- try:
- return download_faster_whisper_model(
- model=self, local_files_only=True
- )
- except (ValueError, FileNotFoundError):
- return None
-
- if self.model_type == ModelType.OPEN_AI_WHISPER_API:
- return ""
-
- if self.model_type == ModelType.HUGGING_FACE:
- try:
- return huggingface_hub.snapshot_download(
- self.hugging_face_model_id,
- allow_patterns=HUGGING_FACE_MODEL_ALLOW_PATTERNS,
- local_files_only=True,
- cache_dir=model_root_dir,
- etag_timeout=60
- )
- except (ValueError, FileNotFoundError):
- return None
-
- raise Exception("Unknown model type")
-
-
-WHISPER_CPP_REPO_ID = "ggerganov/whisper.cpp"
-WHISPER_CPP_LUMII_REPO_ID = "RaivisDejus/whisper.cpp-lv"
-
-
-def get_whisper_cpp_file_path(size: WhisperModelSize) -> str:
- if size == WhisperModelSize.CUSTOM:
- return os.path.join(model_root_dir, f"ggml-model-whisper-custom.bin")
-
- repo_id = WHISPER_CPP_REPO_ID
-
- if size == WhisperModelSize.LUMII:
- repo_id = WHISPER_CPP_LUMII_REPO_ID
-
- model_filename = f"ggml-{size.to_whisper_cpp_model_size()}.bin"
-
- try:
- model_path = huggingface_hub.snapshot_download(
- repo_id=repo_id,
- allow_patterns=[model_filename],
- local_files_only=True,
- cache_dir=model_root_dir,
- etag_timeout=60
- )
-
- return os.path.join(model_path, model_filename)
- except LocalEntryNotFoundError:
- return ''
-
-
-def get_whisper_file_path(size: WhisperModelSize) -> str:
- root_dir = os.path.join(model_root_dir, "whisper")
-
- if size == WhisperModelSize.CUSTOM:
- return os.path.join(root_dir, "custom")
-
- url = whisper._MODELS[size.value]
- return os.path.join(root_dir, os.path.basename(url))
-
-
-class HuggingfaceDownloadMonitor:
- def __init__(self, model_root: str, progress: pyqtSignal(tuple), total_file_size: int):
- self.model_root = model_root
- self.progress = progress
- # To keep dialog open even if it reports 100%
- self.total_file_size = round(total_file_size * 1.1)
- self.incomplete_download_root = None
- self.stop_event = threading.Event()
- self.monitor_thread = None
- self.set_download_roots()
-
- def set_download_roots(self):
- normalized_model_root = os.path.normpath(self.model_root)
- two_dirs_up = os.path.normpath(
- os.path.join(normalized_model_root, "..", ".."))
- self.incomplete_download_root = os.path.normpath(
- os.path.join(two_dirs_up, "blobs"))
-
- def clean_tmp_files(self):
- for filename in os.listdir(model_root_dir):
- if filename.startswith("tmp"):
- os.remove(os.path.join(model_root_dir, filename))
-
- def monitor_file_size(self):
- while not self.stop_event.is_set():
- try:
- if model_root_dir is not None and os.path.isdir(model_root_dir):
- for filename in os.listdir(model_root_dir):
- if filename.startswith("tmp"):
- try:
- file_size = os.path.getsize(
- os.path.join(model_root_dir, filename))
- self.progress.emit((file_size, self.total_file_size))
- except OSError:
- pass # File may have been deleted
-
- if self.incomplete_download_root and os.path.isdir(self.incomplete_download_root):
- for filename in os.listdir(self.incomplete_download_root):
- if filename.endswith(".incomplete"):
- try:
- file_size = os.path.getsize(os.path.join(
- self.incomplete_download_root, filename))
- self.progress.emit((file_size, self.total_file_size))
- except OSError:
- pass # File may have been deleted
- except OSError:
- pass # Directory listing failed, ignore
-
- time.sleep(2)
-
- def start_monitoring(self):
- self.clean_tmp_files()
- self.monitor_thread = threading.Thread(target=self.monitor_file_size)
- self.monitor_thread.start()
-
- def stop_monitoring(self):
- self.progress.emit((self.total_file_size, self.total_file_size))
-
- if self.monitor_thread is not None:
- self.stop_event.set()
- self.monitor_thread.join()
-
-
-def get_file_size(url):
- response = requests.head(url, allow_redirects=True)
- response.raise_for_status()
- return int(response.headers['Content-Length'])
-
-
-def download_from_huggingface(
- repo_id: str,
- allow_patterns: List[str],
- progress: pyqtSignal(tuple),
- num_large_files: int = 1
-):
- progress.emit((0, 100))
-
- try:
- model_root = huggingface_hub.snapshot_download(
- repo_id,
- # all, but largest
- allow_patterns=allow_patterns[num_large_files:],
- cache_dir=model_root_dir,
- etag_timeout=60
- )
- except Exception as exc:
- logging.exception(exc)
- return ""
-
- progress.emit((1, 100))
-
- largest_file_size = 0
- for pattern in allow_patterns[:num_large_files]:
- try:
- file_url = huggingface_hub.hf_hub_url(repo_id, pattern)
- file_size = get_file_size(file_url)
-
- if file_size > largest_file_size:
- largest_file_size = file_size
-
- except requests.exceptions.RequestException as e:
- continue
-
- model_download_monitor = HuggingfaceDownloadMonitor(
- model_root, progress, largest_file_size)
- model_download_monitor.start_monitoring()
-
- try:
- huggingface_hub.snapshot_download(
- repo_id,
- allow_patterns=allow_patterns[:num_large_files], # largest
- cache_dir=model_root_dir,
- etag_timeout=60
- )
- except Exception as exc:
- logging.exception(exc)
- model_download_monitor.stop_monitoring()
-
- return ""
-
- model_download_monitor.stop_monitoring()
-
- return model_root
-
-
-def download_faster_whisper_model(
- model: TranscriptionModel, local_files_only=False, progress: pyqtSignal(tuple) = None
-):
- size = model.whisper_model_size.to_faster_whisper_model_size()
- custom_repo_id = model.hugging_face_model_id
-
- if size == WhisperModelSize.CUSTOM and custom_repo_id == "":
- raise ValueError("Custom model id is not provided")
-
- if size == WhisperModelSize.CUSTOM:
- repo_id = custom_repo_id
- # Replicating models from faster-whisper code https://github.com/SYSTRAN/faster-whisper/blob/master/faster_whisper/utils.py#L29
- elif size == WhisperModelSize.LARGEV3TURBO:
- repo_id = "mobiuslabsgmbh/faster-whisper-large-v3-turbo"
- else:
- repo_id = "Systran/faster-whisper-%s" % size
-
- allow_patterns = [
- "model.bin", # largest by size first
- "pytorch_model.bin", # possible alternative model filename
- "config.json",
- "preprocessor_config.json",
- "tokenizer.json",
- "vocabulary.*",
- ]
-
- if local_files_only:
- return huggingface_hub.snapshot_download(
- repo_id,
- allow_patterns=allow_patterns,
- local_files_only=True,
- cache_dir=model_root_dir,
- etag_timeout=60
- )
-
- return download_from_huggingface(
- repo_id,
- allow_patterns=allow_patterns,
- progress=progress,
- num_large_files=2
- )
-
-
-class ModelDownloader(QRunnable):
- class Signals(QObject):
- finished = pyqtSignal(str)
- progress = pyqtSignal(tuple) # (current, total)
- error = pyqtSignal(str)
-
- def __init__(self, model: TranscriptionModel, custom_model_url: Optional[str] = None):
- super().__init__()
-
- self.is_coreml_supported = platform.system(
- ) == "Darwin" and platform.machine() == "arm64"
- self.signals = self.Signals()
- self.model = model
- self.stopped = False
- self.custom_model_url = custom_model_url
-
- def run(self) -> None:
- logging.debug("Downloading model: %s, %s", self.model,
- self.model.hugging_face_model_id)
-
- if self.model.model_type == ModelType.WHISPER_CPP:
- if self.custom_model_url:
- url = self.custom_model_url
- file_path = get_whisper_cpp_file_path(
- size=self.model.whisper_model_size)
- return self.download_model_to_path(url=url, file_path=file_path)
-
- repo_id = WHISPER_CPP_REPO_ID
-
- if self.model.whisper_model_size == WhisperModelSize.LUMII:
- repo_id = WHISPER_CPP_LUMII_REPO_ID
-
- model_name = self.model.whisper_model_size.to_whisper_cpp_model_size()
-
- whisper_cpp_model_files = [
- f"ggml-{model_name}.bin",
- "README.md"
- ]
- num_large_files = 1
- if self.is_coreml_supported:
- whisper_cpp_model_files = [
- f"ggml-{model_name}.bin",
- f"ggml-{model_name}-encoder.mlmodelc.zip",
- "README.md"
- ]
- num_large_files = 2
-
- model_path = download_from_huggingface(
- repo_id=repo_id,
- allow_patterns=whisper_cpp_model_files,
- progress=self.signals.progress,
- num_large_files=num_large_files
- )
-
- if self.is_coreml_supported:
- import tempfile
-
- target_dir = os.path.join(model_path, f"ggml-{model_name}-encoder.mlmodelc")
- zip_path = os.path.join(model_path, f"ggml-{model_name}-encoder.mlmodelc.zip")
-
- # Remove target directory if it exists
- if os.path.exists(target_dir):
- shutil.rmtree(target_dir)
-
- # Extract to a temporary directory first
- with tempfile.TemporaryDirectory() as temp_dir:
- with zipfile.ZipFile(zip_path, 'r') as zip_ref:
- zip_ref.extractall(temp_dir)
-
- # Remove __MACOSX metadata folders if present
- macosx_path = os.path.join(temp_dir, "__MACOSX")
- if os.path.exists(macosx_path):
- shutil.rmtree(macosx_path)
-
- # Check if there's a single top-level directory
- temp_contents = os.listdir(temp_dir)
- if len(temp_contents) == 1 and os.path.isdir(os.path.join(temp_dir, temp_contents[0])):
- # Single directory - move its contents to target
- nested_dir = os.path.join(temp_dir, temp_contents[0])
- shutil.move(nested_dir, target_dir)
- else:
- # Multiple items or files - copy everything to target
- os.makedirs(target_dir, exist_ok=True)
- for item in temp_contents:
- src = os.path.join(temp_dir, item)
- dst = os.path.join(target_dir, item)
- if os.path.isdir(src):
- shutil.copytree(src, dst)
- else:
- shutil.copy2(src, dst)
-
- self.signals.finished.emit(os.path.join(
- model_path, f"ggml-{model_name}.bin"))
- return
-
- if self.model.model_type == ModelType.WHISPER:
- url = whisper._MODELS[self.model.whisper_model_size.value]
- file_path = get_whisper_file_path(
- size=self.model.whisper_model_size)
- expected_sha256 = url.split("/")[-2]
- return self.download_model_to_path(
- url=url, file_path=file_path, expected_sha256=expected_sha256
- )
-
- if self.model.model_type == ModelType.FASTER_WHISPER:
- model_path = download_faster_whisper_model(
- model=self.model,
- progress=self.signals.progress,
- )
-
- if model_path == "":
- self.signals.error.emit(_("Error"))
-
- self.signals.finished.emit(model_path)
- return
-
- if self.model.model_type == ModelType.HUGGING_FACE:
- model_path = download_from_huggingface(
- self.model.hugging_face_model_id,
- allow_patterns=HUGGING_FACE_MODEL_ALLOW_PATTERNS,
- progress=self.signals.progress,
- num_large_files=4
- )
-
- if model_path == "":
- self.signals.error.emit(_("Error"))
-
- self.signals.finished.emit(model_path)
- return
-
- if self.model.model_type == ModelType.OPEN_AI_WHISPER_API:
- self.signals.finished.emit("")
- return
-
- raise Exception("Invalid model type: " + self.model.model_type.value)
-
- def download_model_to_path(
- self, url: str, file_path: str, expected_sha256: Optional[str] = None
- ):
- try:
- downloaded = self.download_model(url, file_path, expected_sha256)
- if downloaded:
- self.signals.finished.emit(file_path)
- except requests.RequestException as e:
- self.signals.error.emit(_("A connection error occurred"))
- if not self.stopped and "timeout" not in str(e).lower():
- if os.path.exists(file_path):
- os.remove(file_path)
- logging.exception("")
- except Exception as exc:
- self.signals.error.emit(str(exc))
- if not self.stopped:
- if os.path.exists(file_path):
- os.remove(file_path)
- logging.exception(exc)
-
- def download_model(
- self, url: str, file_path: str, expected_sha256: Optional[str]
- ) -> bool:
- logging.debug(f"Downloading model from {url} to {file_path}")
-
- os.makedirs(os.path.dirname(file_path), exist_ok=True)
-
- if os.path.exists(file_path) and not os.path.isfile(file_path):
- raise RuntimeError(f"{file_path} exists and is not a regular file")
-
- resume_from = 0
- file_mode = "wb"
-
- if os.path.isfile(file_path):
- file_size = os.path.getsize(file_path)
-
- if expected_sha256 is not None:
- # Get the expected file size from URL
- try:
- head_response = requests.head(url, timeout=5, allow_redirects=True)
- expected_size = int(head_response.headers.get("Content-Length", 0))
-
- if expected_size > 0:
- if file_size < expected_size:
- resume_from = file_size
- file_mode = "ab"
- logging.debug(
- f"File incomplete ({file_size}/{expected_size} bytes), resuming from byte {resume_from}"
- )
- elif file_size == expected_size:
- # This means file size matches - verify SHA256 to confirm it is complete
- try:
- # Use chunked reading to avoid loading entire file into memory
- sha256_hash = hashlib.sha256()
- with open(file_path, "rb") as f:
- for chunk in iter(lambda: f.read(8192), b""):
- sha256_hash.update(chunk)
- model_sha256 = sha256_hash.hexdigest()
- if model_sha256 == expected_sha256:
- logging.debug("Model already downloaded and verified")
- return True
- else:
- warnings.warn(
- f"{file_path} exists, but the SHA256 checksum does not match; re-downloading the file"
- )
- # File exists but it is wrong, delete it
- os.remove(file_path)
- except Exception as e:
- logging.warning(f"Error checking existing file: {e}")
- os.remove(file_path)
- else:
- # File is larger than expected - corrupted, delete it
- warnings.warn(f"File size ({file_size}) exceeds expected size ({expected_size}), re-downloading")
- os.remove(file_path)
- else:
- # Can't get expected size - use threshold approach
- if file_size < 10 * 1024 * 1024:
- resume_from = file_size
- file_mode = "ab" # Append mode to resume
- logging.debug(f"Resuming download from byte {resume_from}")
- else:
- # Large file - verify SHA256 using chunked reading
- try:
- sha256_hash = hashlib.sha256()
- with open(file_path, "rb") as f:
- for chunk in iter(lambda: f.read(8192), b""):
- sha256_hash.update(chunk)
- model_sha256 = sha256_hash.hexdigest()
- if model_sha256 == expected_sha256:
- logging.debug("Model already downloaded and verified")
- return True
- else:
- warnings.warn("SHA256 mismatch, re-downloading")
- os.remove(file_path)
- except Exception as e:
- logging.warning(f"Error verifying file: {e}")
- os.remove(file_path)
-
- except Exception as e:
- # Can't get expected size - use threshold
- logging.debug(f"Could not get expected file size: {e}, using threshold")
- if file_size < 10 * 1024 * 1024:
- resume_from = file_size
- file_mode = "ab"
- logging.debug(f"Resuming from byte {resume_from}")
- else:
- # No SHA256 to verify - just check file size
- if file_size > 0:
- resume_from = file_size
- file_mode = "ab"
- logging.debug(f"Resuming download from byte {resume_from}")
-
- # Downloads the model using the requests module instead of urllib to
- # use the certs from certifi when the app is running in frozen mode
-
- # Check if server supports Range requests before starting download
- supports_range = False
- if resume_from > 0:
- try:
- head_resp = requests.head(url, timeout=10, allow_redirects=True)
- accept_ranges = head_resp.headers.get("Accept-Ranges", "").lower()
- supports_range = accept_ranges == "bytes"
- if not supports_range:
- logging.debug("Server doesn't support Range requests, starting from beginning")
- resume_from = 0
- file_mode = "wb"
- except requests.RequestException as e:
- logging.debug(f"HEAD request failed, starting fresh: {e}")
- resume_from = 0
- file_mode = "wb"
-
- headers = {}
- if resume_from > 0 and supports_range:
- headers["Range"] = f"bytes={resume_from}-"
-
- # Use a temporary file for fresh downloads to ensure atomic writes
- temp_file_path = None
- if resume_from == 0:
- temp_file_path = file_path + ".downloading"
- # Clean up any existing temp file
- if os.path.exists(temp_file_path):
- try:
- os.remove(temp_file_path)
- except OSError:
- pass
- download_path = temp_file_path
- else:
- download_path = file_path
-
- try:
- with requests.get(url, stream=True, timeout=30, headers=headers) as source:
- source.raise_for_status()
-
- if resume_from > 0:
- if source.status_code == 206:
- logging.debug(
- f"Server supports resume, continuing from byte {resume_from}")
- content_range = source.headers.get("Content-Range", "")
- if "/" in content_range:
- total_size = int(content_range.split("/")[-1])
- else:
- total_size = resume_from + int(source.headers.get("Content-Length", 0))
- current = resume_from
- else:
- # Server returned 200 instead of 206, need to start over
- logging.debug("Server returned 200 instead of 206, starting fresh")
- resume_from = 0
- file_mode = "wb"
- temp_file_path = file_path + ".downloading"
- download_path = temp_file_path
- total_size = float(source.headers.get("Content-Length", 0))
- current = 0.0
- else:
- total_size = float(source.headers.get("Content-Length", 0))
- current = 0.0
-
- self.signals.progress.emit((current, total_size))
-
- with open(download_path, file_mode) as output:
- for chunk in source.iter_content(chunk_size=8192):
- if self.stopped:
- return False
- output.write(chunk)
- current += len(chunk)
- self.signals.progress.emit((current, total_size))
-
- # If we used a temp file, rename it to the final path
- if temp_file_path and os.path.exists(temp_file_path):
- # Remove existing file if present
- if os.path.exists(file_path):
- os.remove(file_path)
- shutil.move(temp_file_path, file_path)
-
- except Exception:
- # Clean up temp file on error
- if temp_file_path and os.path.exists(temp_file_path):
- try:
- os.remove(temp_file_path)
- except OSError:
- pass
- raise
-
- if expected_sha256 is not None:
- # Use chunked reading to avoid loading entire file into memory
- sha256_hash = hashlib.sha256()
- with open(file_path, "rb") as f:
- for chunk in iter(lambda: f.read(8192), b""):
- sha256_hash.update(chunk)
- if sha256_hash.hexdigest() != expected_sha256:
- # Delete the corrupted file before raising the error
- try:
- os.remove(file_path)
- except OSError as e:
- logging.warning(f"Failed to delete corrupted model file: {e}")
- raise RuntimeError(
- "Model has been downloaded but the SHA256 checksum does not match. Please retry loading the "
- "model."
- )
-
- logging.debug("Downloaded model")
-
- return True
-
- def cancel(self):
- self.stopped = True
diff --git a/buzz/paths.py b/buzz/paths.py
deleted file mode 100644
index bd2c2d06..00000000
--- a/buzz/paths.py
+++ /dev/null
@@ -1,5 +0,0 @@
-import os
-
-
-def file_path_as_title(file_path: str):
- return os.path.basename(file_path)
diff --git a/buzz/recording.py b/buzz/recording.py
deleted file mode 100644
index 158f6e5c..00000000
--- a/buzz/recording.py
+++ /dev/null
@@ -1,56 +0,0 @@
-from typing import Optional
-
-import logging
-import numpy as np
-import sounddevice
-from PyQt6.QtCore import QObject, pyqtSignal
-
-
-class RecordingAmplitudeListener(QObject):
- stream: Optional[sounddevice.InputStream] = None
- amplitude_changed = pyqtSignal(float)
- average_amplitude_changed = pyqtSignal(float)
-
- ACCUMULATION_SECONDS = 1
-
- def __init__(
- self,
- input_device_index: Optional[int] = None,
- parent: Optional[QObject] = None,
- ):
- super().__init__(parent)
- self.input_device_index = input_device_index
- self.buffer = np.ndarray([], dtype=np.float32)
- self.accumulation_size = 0
- self._active = True
-
- def start_recording(self):
- try:
- self.stream = sounddevice.InputStream(
- device=self.input_device_index,
- dtype="float32",
- channels=1,
- callback=self.stream_callback,
- )
- self.stream.start()
- self.accumulation_size = int(self.stream.samplerate * self.ACCUMULATION_SECONDS)
- except Exception as e:
- self.stop_recording()
- logging.exception("Failed to start audio stream on device %s: %s", self.input_device_index, e)
-
- def stop_recording(self):
- self._active = False
- if self.stream is not None:
- self.stream.stop()
- self.stream.close()
-
- def stream_callback(self, in_data: np.ndarray, frame_count, time_info, status):
- if not self._active:
- return
- chunk = in_data.ravel()
- self.amplitude_changed.emit(float(np.sqrt(np.mean(chunk**2))))
-
- self.buffer = np.append(self.buffer, chunk)
- if self.buffer.size >= self.accumulation_size:
- self.average_amplitude_changed.emit(float(np.sqrt(np.mean(self.buffer**2))))
- self.buffer = np.ndarray([], dtype=np.float32)
diff --git a/buzz/schema.sql b/buzz/schema.sql
deleted file mode 100644
index a6a11bad..00000000
--- a/buzz/schema.sql
+++ /dev/null
@@ -1,34 +0,0 @@
-CREATE TABLE transcription (
- id TEXT PRIMARY KEY,
- error_message TEXT,
- export_formats TEXT,
- file TEXT,
- output_folder TEXT,
- progress DOUBLE PRECISION DEFAULT 0.0,
- language TEXT,
- model_type TEXT,
- source TEXT,
- status TEXT,
- task TEXT,
- time_ended TIMESTAMP,
- time_queued TIMESTAMP NOT NULL,
- time_started TIMESTAMP,
- url TEXT,
- whisper_model_size TEXT,
- hugging_face_model_id TEXT,
- word_level_timings BOOLEAN DEFAULT FALSE,
- extract_speech BOOLEAN DEFAULT FALSE,
- name TEXT,
- notes TEXT
-);
-
-CREATE TABLE transcription_segment (
- id INTEGER PRIMARY KEY,
- end_time INT DEFAULT 0,
- start_time INT DEFAULT 0,
- text TEXT NOT NULL,
- translation TEXT DEFAULT '',
- transcription_id TEXT,
- FOREIGN KEY (transcription_id) REFERENCES transcription(id) ON DELETE CASCADE
-);
-CREATE INDEX idx_transcription_id ON transcription_segment(transcription_id);
diff --git a/buzz/settings/__init__.py b/buzz/settings/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/buzz/settings/recording_transcriber_mode.py b/buzz/settings/recording_transcriber_mode.py
deleted file mode 100644
index 4c6e8286..00000000
--- a/buzz/settings/recording_transcriber_mode.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from enum import Enum
-from buzz.locale import _
-
-class RecordingTranscriberMode(Enum):
- APPEND_BELOW = _("Append below")
- APPEND_ABOVE = _("Append above")
- APPEND_AND_CORRECT = _("Append and correct")
\ No newline at end of file
diff --git a/buzz/settings/settings.py b/buzz/settings/settings.py
deleted file mode 100644
index 0f9242a4..00000000
--- a/buzz/settings/settings.py
+++ /dev/null
@@ -1,165 +0,0 @@
-import enum
-import typing
-import logging
-import uuid
-
-from PyQt6.QtCore import QSettings
-
-APP_NAME = "Buzz"
-
-
-class Settings:
- def __init__(self, application=""):
- self.settings = QSettings(APP_NAME, application)
- self.settings.sync()
-
- class Key(enum.Enum):
- RECORDING_TRANSCRIBER_TASK = "recording-transcriber/task"
- RECORDING_TRANSCRIBER_MODEL = "recording-transcriber/model"
- RECORDING_TRANSCRIBER_LANGUAGE = "recording-transcriber/language"
- RECORDING_TRANSCRIBER_INITIAL_PROMPT = "recording-transcriber/initial-prompt"
- RECORDING_TRANSCRIBER_ENABLE_LLM_TRANSLATION = "recording-transcriber/enable-llm-translation"
- RECORDING_TRANSCRIBER_LLM_MODEL = "recording-transcriber/llm-model"
- RECORDING_TRANSCRIBER_LLM_PROMPT = "recording-transcriber/llm-prompt"
- RECORDING_TRANSCRIBER_EXPORT_ENABLED = "recording-transcriber/export-enabled"
- RECORDING_TRANSCRIBER_EXPORT_FOLDER = "recording-transcriber/export-folder"
- RECORDING_TRANSCRIBER_MODE = "recording-transcriber/mode"
- RECORDING_TRANSCRIBER_SILENCE_THRESHOLD = "recording-transcriber/silence-threshold"
- RECORDING_TRANSCRIBER_LINE_SEPARATOR = "recording-transcriber/line-separator"
- RECORDING_TRANSCRIBER_TRANSCRIPTION_STEP = "recording-transcriber/transcription-step"
- RECORDING_TRANSCRIBER_EXPORT_FILE_TYPE = "recording-transcriber/export-file-type"
- RECORDING_TRANSCRIBER_EXPORT_MAX_ENTRIES = "recording-transcriber/export-max-entries"
- RECORDING_TRANSCRIBER_EXPORT_FILE_NAME = "recording-transcriber/export-file-name"
- RECORDING_TRANSCRIBER_HIDE_UNCONFIRMED = "recording-transcriber/hide-unconfirmed"
-
- PRESENTATION_WINDOW_TEXT_COLOR = "presentation-window/text-color"
- PRESENTATION_WINDOW_BACKGROUND_COLOR = "presentation-window/background-color"
- PRESENTATION_WINDOW_TEXT_SIZE = "presentation-window/text-size"
- PRESENTATION_WINDOW_THEME = "presentation-window/theme"
-
- FILE_TRANSCRIBER_TASK = "file-transcriber/task"
- FILE_TRANSCRIBER_MODEL = "file-transcriber/model"
- FILE_TRANSCRIBER_LANGUAGE = "file-transcriber/language"
- FILE_TRANSCRIBER_INITIAL_PROMPT = "file-transcriber/initial-prompt"
- FILE_TRANSCRIBER_ENABLE_LLM_TRANSLATION = "file-transcriber/enable-llm-translation"
- FILE_TRANSCRIBER_LLM_MODEL = "file-transcriber/llm-model"
- FILE_TRANSCRIBER_LLM_PROMPT = "file-transcriber/llm-prompt"
- FILE_TRANSCRIBER_WORD_LEVEL_TIMINGS = "file-transcriber/word-level-timings"
- FILE_TRANSCRIBER_EXPORT_FORMATS = "file-transcriber/export-formats"
-
- DEFAULT_EXPORT_FILE_NAME = "transcriber/default-export-file-name"
- CUSTOM_OPENAI_BASE_URL = "transcriber/custom-openai-base-url"
- OPENAI_API_MODEL = "transcriber/openai-api-model"
- CUSTOM_FASTER_WHISPER_ID = "transcriber/custom-faster-whisper-id"
- HUGGINGFACE_MODEL_ID = "transcriber/huggingface-model-id"
-
- SHORTCUTS = "shortcuts"
-
- FONT_SIZE = "font-size"
-
- UI_LOCALE = "ui-locale"
-
- USER_IDENTIFIER = "user-identifier"
-
- TRANSCRIPTION_TASKS_TABLE_COLUMN_VISIBILITY = (
- "transcription-tasks-table/column-visibility"
- )
- TRANSCRIPTION_TASKS_TABLE_COLUMN_ORDER = (
- "transcription-tasks-table/column-order"
- )
- TRANSCRIPTION_TASKS_TABLE_COLUMN_WIDTHS = (
- "transcription-tasks-table/column-widths"
- )
- TRANSCRIPTION_TASKS_TABLE_SORT_STATE = (
- "transcription-tasks-table/sort-state"
- )
-
- MAIN_WINDOW = "main-window"
- TRANSCRIPTION_VIEWER = "transcription-viewer"
-
- AUDIO_PLAYBACK_RATE = "audio/playback-rate"
-
- FORCE_CPU = "force-cpu"
- REDUCE_GPU_MEMORY = "reduce-gpu-memory"
-
- LAST_UPDATE_CHECK = "update/last-check"
- UPDATE_AVAILABLE_VERSION = "update/available-version"
-
- def get_user_identifier(self) -> str:
- user_id = self.value(self.Key.USER_IDENTIFIER, "")
- if not user_id:
- user_id = str(uuid.uuid4())
- self.set_value(self.Key.USER_IDENTIFIER, user_id)
- return user_id
-
- def set_value(self, key: Key, value: typing.Any) -> None:
- self.settings.setValue(key.value, value)
-
- def save_custom_model_id(self, model) -> None:
- from buzz.model_loader import ModelType
- match model.model_type:
- case ModelType.FASTER_WHISPER:
- self.set_value(
- Settings.Key.CUSTOM_FASTER_WHISPER_ID,
- model.hugging_face_model_id,
- )
- case ModelType.HUGGING_FACE:
- self.set_value(
- Settings.Key.HUGGINGFACE_MODEL_ID,
- model.hugging_face_model_id,
- )
-
- def load_custom_model_id(self, model) -> str:
- from buzz.model_loader import ModelType
- match model.model_type:
- case ModelType.FASTER_WHISPER:
- return self.value(
- Settings.Key.CUSTOM_FASTER_WHISPER_ID,
- "",
- )
- case ModelType.HUGGING_FACE:
- return self.value(
- Settings.Key.HUGGINGFACE_MODEL_ID,
- "",
- )
-
- return ""
-
- def value(
- self,
- key: Key,
- default_value: typing.Any,
- value_type: typing.Optional[type] = None,
- ) -> typing.Any:
- val = self.settings.value(
- key.value,
- default_value,
- value_type if value_type is not None else type(default_value),
- )
- if (value_type is bool or isinstance(default_value, bool)):
- if isinstance(val, bool):
- return val
- if isinstance(val, str):
- return val.lower() in ("true", "1", "yes", "on")
- if isinstance(val, int):
- return val != 0
- return bool(val)
- return val
-
- def clear(self):
- self.settings.clear()
-
- def begin_group(self, group: Key) -> None:
- self.settings.beginGroup(group.value)
-
- def end_group(self) -> None:
- self.settings.endGroup()
-
- def sync(self):
- self.settings.sync()
-
- def get_default_export_file_template(self) -> str:
- return self.value(
- Settings.Key.DEFAULT_EXPORT_FILE_NAME,
- "{{ input_file_name }} ({{ task }}d on {{ date_time }})",
- )
diff --git a/buzz/settings/shortcut.py b/buzz/settings/shortcut.py
deleted file mode 100644
index 0816f4f6..00000000
--- a/buzz/settings/shortcut.py
+++ /dev/null
@@ -1,43 +0,0 @@
-import enum
-import typing
-from buzz.locale import _
-
-
-class Shortcut(str, enum.Enum):
- sequence: str
- description: str
-
- def __new__(cls, sequence: str, description: str):
- obj = str.__new__(cls, sequence)
- obj._value_ = sequence
- obj.sequence = sequence
- obj.description = description
- return obj
-
- OPEN_RECORD_WINDOW = ("Ctrl+R", _("Open Record Window"))
- OPEN_IMPORT_WINDOW = ("Ctrl+O", _("Import File"))
- OPEN_IMPORT_URL_WINDOW = ("Ctrl+U", _("Import URL"))
- OPEN_PREFERENCES_WINDOW = ("Ctrl+,", _("Open Preferences Window"))
-
- VIEW_TRANSCRIPT_TEXT = ("Ctrl+E", _("View Transcript Text"))
- VIEW_TRANSCRIPT_TRANSLATION = ("Ctrl+L", _("View Transcript Translation"))
- VIEW_TRANSCRIPT_TIMESTAMPS = ("Ctrl+T", _("View Transcript Timestamps"))
- SEARCH_TRANSCRIPT = ("Ctrl+F", _("Search Transcript"))
- SEARCH_NEXT = ("Ctrl+Return", _("Go to Next Transcript Search Result"))
- SEARCH_PREVIOUS = ("Shift+Return", _("Go to Previous Transcript Search Result"))
- SCROLL_TO_CURRENT_TEXT = ("Ctrl+G", _("Scroll to Current Text"))
- PLAY_PAUSE_AUDIO = ("Ctrl+P", _("Play/Pause Audio"))
- REPLAY_CURRENT_SEGMENT = ("Ctrl+Shift+P", _("Replay Current Segment"))
- TOGGLE_PLAYBACK_CONTROLS = ("Ctrl+Alt+P", _("Toggle Playback Controls"))
-
- DECREASE_SEGMENT_START = ("Ctrl+Left", _("Decrease Segment Start Time"))
- INCREASE_SEGMENT_START = ("Ctrl+Right", _("Increase Segment Start Time"))
- DECREASE_SEGMENT_END = ("Ctrl+Shift+Left", _("Decrease Segment End Time"))
- INCREASE_SEGMENT_END = ("Ctrl+Shift+Right", _("Increase Segment End Time"))
-
- CLEAR_HISTORY = ("Ctrl+S", _("Clear History"))
- STOP_TRANSCRIPTION = ("Ctrl+X", _("Cancel Transcription"))
-
- @staticmethod
- def get_default_shortcuts() -> typing.Dict[str, str]:
- return {shortcut.name: shortcut.sequence for shortcut in Shortcut}
diff --git a/buzz/settings/shortcuts.py b/buzz/settings/shortcuts.py
deleted file mode 100644
index 5e4b99d4..00000000
--- a/buzz/settings/shortcuts.py
+++ /dev/null
@@ -1,24 +0,0 @@
-import typing
-
-from buzz.settings.settings import Settings
-from buzz.settings.shortcut import Shortcut
-
-
-class Shortcuts:
- def __init__(self, settings: Settings):
- self.settings = settings
-
- def get(self, shortcut: Shortcut) -> str:
- custom_shortcuts = self.get_custom_shortcuts()
- return custom_shortcuts.get(shortcut.name, shortcut.sequence)
-
- def set(self, shortcut: Shortcut, sequence: str) -> None:
- custom_shortcuts = self.get_custom_shortcuts()
- custom_shortcuts[shortcut.name] = sequence
- self.settings.set_value(Settings.Key.SHORTCUTS, custom_shortcuts)
-
- def clear(self) -> None:
- self.settings.set_value(Settings.Key.SHORTCUTS, {})
-
- def get_custom_shortcuts(self) -> typing.Dict[str, str]:
- return self.settings.value(Settings.Key.SHORTCUTS, {})
diff --git a/buzz/store/__init__.py b/buzz/store/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/buzz/store/keyring_store.py b/buzz/store/keyring_store.py
deleted file mode 100644
index 670be19e..00000000
--- a/buzz/store/keyring_store.py
+++ /dev/null
@@ -1,243 +0,0 @@
-import base64
-import enum
-import hashlib
-import json
-import logging
-import os
-import sys
-
-import keyring
-
-from buzz.settings.settings import APP_NAME
-
-
-class Key(enum.Enum):
- OPENAI_API_KEY = "OpenAI API key"
-
-
-def _is_linux() -> bool:
- return sys.platform.startswith("linux")
-
-
-def _get_secrets_file_path() -> str:
- """Get the path to the local encrypted secrets file."""
- from platformdirs import user_data_dir
-
- data_dir = user_data_dir(APP_NAME)
- os.makedirs(data_dir, exist_ok=True)
- return os.path.join(data_dir, ".secrets.json")
-
-
-def _get_portal_secret() -> bytes | None:
- """Get the application secret from XDG Desktop Portal.
-
- The portal provides a per-application secret that can be used
- for encrypting application-specific data. This works in sandboxed
- environments (Snap/Flatpak) via the desktop plug.
- """
- if not _is_linux():
- return None
-
- try:
- from jeepney import DBusAddress, new_method_call
- from jeepney.io.blocking import open_dbus_connection
- import socket
-
- # Open connection with file descriptor support enabled
- conn = open_dbus_connection(bus="SESSION", enable_fds=True)
-
- portal = DBusAddress(
- "/org/freedesktop/portal/desktop",
- bus_name="org.freedesktop.portal.Desktop",
- interface="org.freedesktop.portal.Secret",
- )
-
- # Create a socket pair for receiving the secret
- sock_read, sock_write = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
-
- try:
- # Build the method call with file descriptor
- # RetrieveSecret(fd: h, options: a{sv}) -> (handle: o)
- # Pass the socket object directly - jeepney handles fd passing
- msg = new_method_call(portal, "RetrieveSecret", "ha{sv}", (sock_write, {}))
-
- # Send message and get reply
- conn.send_and_get_reply(msg, timeout=10)
-
- # Close the write end - portal has it now
- sock_write.close()
- sock_write = None
-
- # Read the secret from the read end
- # The portal writes the secret and closes its end
- sock_read.settimeout(5.0)
- secret_data = b""
- while True:
- try:
- chunk = sock_read.recv(4096)
- if not chunk:
- break
- secret_data += chunk
- except socket.timeout:
- break
-
- if secret_data:
- return secret_data
-
- return None
-
- finally:
- sock_read.close()
- if sock_write is not None:
- sock_write.close()
-
- except Exception as exc:
- logging.debug("XDG Portal secret not available: %s", exc)
- return None
-
-
-def _derive_key(master_secret: bytes, key_name: str) -> bytes:
- """Derive a key-specific encryption key from the master secret."""
- # Use PBKDF2 to derive a key for this specific secret
- return hashlib.pbkdf2_hmac(
- "sha256",
- master_secret,
- f"{APP_NAME}:{key_name}".encode(),
- 100000,
- dklen=32,
- )
-
-
-def _encrypt_value(value: str, key: bytes) -> str:
- """Encrypt a value using XOR with the derived key (simple encryption)."""
- # For a more secure implementation, use cryptography library with AES
- # This is a simple XOR-based encryption suitable for the use case
- value_bytes = value.encode("utf-8")
- key_extended = (key * ((len(value_bytes) // len(key)) + 1))[: len(value_bytes)]
- encrypted = bytes(a ^ b for a, b in zip(value_bytes, key_extended))
- return base64.b64encode(encrypted).decode("ascii")
-
-
-def _decrypt_value(encrypted: str, key: bytes) -> str:
- """Decrypt a value using XOR with the derived key."""
- encrypted_bytes = base64.b64decode(encrypted.encode("ascii"))
- key_extended = (key * ((len(encrypted_bytes) // len(key)) + 1))[: len(encrypted_bytes)]
- decrypted = bytes(a ^ b for a, b in zip(encrypted_bytes, key_extended))
- return decrypted.decode("utf-8")
-
-
-def _load_local_secrets() -> dict:
- """Load the local secrets file."""
- secrets_file = _get_secrets_file_path()
- if os.path.exists(secrets_file):
- try:
- with open(secrets_file, "r") as f:
- return json.load(f)
- except (json.JSONDecodeError, IOError) as exc:
- logging.debug("Failed to load secrets file: %s", exc)
- return {}
-
-
-def _save_local_secrets(secrets: dict) -> None:
- """Save secrets to the local file."""
- secrets_file = _get_secrets_file_path()
- try:
- with open(secrets_file, "w") as f:
- json.dump(secrets, f)
- # Set restrictive permissions
- os.chmod(secrets_file, 0o600)
- except IOError as exc:
- logging.warning("Failed to save secrets file: %s", exc)
-
-
-def _get_portal_password(key: Key) -> str | None:
- """Get a password using the XDG Desktop Portal Secret."""
- portal_secret = _get_portal_secret()
- if portal_secret is None:
- return None
-
- secrets = _load_local_secrets()
- encrypted_value = secrets.get(key.value)
- if encrypted_value is None:
- return None
-
- try:
- derived_key = _derive_key(portal_secret, key.value)
- return _decrypt_value(encrypted_value, derived_key)
- except Exception as exc:
- logging.debug("Failed to decrypt portal secret: %s", exc)
- return None
-
-
-def _set_portal_password(key: Key, password: str) -> bool:
- """Set a password using the XDG Desktop Portal Secret."""
- portal_secret = _get_portal_secret()
- if portal_secret is None:
- return False
-
- try:
- derived_key = _derive_key(portal_secret, key.value)
- encrypted_value = _encrypt_value(password, derived_key)
-
- secrets = _load_local_secrets()
- secrets[key.value] = encrypted_value
- _save_local_secrets(secrets)
- return True
- except Exception as exc:
- logging.debug("Failed to set portal secret: %s", exc)
- return False
-
-
-def _delete_portal_password(key: Key) -> bool:
- """Delete a password from the portal-based local storage."""
- secrets = _load_local_secrets()
- if key.value in secrets:
- del secrets[key.value]
- _save_local_secrets(secrets)
- return True
- return False
-
-
-def get_password(key: Key) -> str | None:
- # On Linux, try XDG Desktop Portal first (works in sandboxed environments)
- if _is_linux():
- result = _get_portal_password(key)
-
-
- if result is not None:
- return result
-
- # Fall back to keyring (cross-platform, uses Secret Service on Linux)
- try:
- password = keyring.get_password(APP_NAME, username=key.value)
- if password is None:
- return ""
- return password
- except Exception as exc:
- logging.warning("Unable to read from keyring: %s", exc)
- return ""
-
-
-def set_password(username: Key, password: str) -> None:
- # On Linux, try XDG Desktop Portal first (works in sandboxed environments)
- if _is_linux():
- if _set_portal_password(username, password):
- return
-
- # Fall back to keyring (cross-platform, uses Secret Service on Linux)
- keyring.set_password(APP_NAME, username.value, password)
-
-
-def delete_password(key: Key) -> None:
- """Delete a password from the secret store."""
- # On Linux, also delete from portal storage
- if _is_linux():
- _delete_portal_password(key)
-
- # Delete from keyring
- try:
- keyring.delete_password(APP_NAME, key.value)
- except keyring.errors.PasswordDeleteError:
- pass # Password doesn't exist, ignore
- except Exception as exc:
- logging.warning("Unable to delete from keyring: %s", exc)
diff --git a/buzz/transcriber/__init__.py b/buzz/transcriber/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/buzz/transcriber/file_transcriber.py b/buzz/transcriber/file_transcriber.py
deleted file mode 100755
index 822e7107..00000000
--- a/buzz/transcriber/file_transcriber.py
+++ /dev/null
@@ -1,242 +0,0 @@
-import logging
-import os
-import sys
-import subprocess
-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 import whisper_audio
-from buzz.assets import APP_BASE_DIR
-from buzz.transcriber.transcriber import (
- FileTranscriptionTask,
- get_output_file_path,
- Segment,
- 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
- progress = pyqtSignal(tuple) # (current, total)
- download_progress = pyqtSignal(float)
- completed = pyqtSignal(list) # List[Segment]
- error = pyqtSignal(str)
-
- def __init__(self, task: FileTranscriptionTask, parent: Optional["QObject"] = None):
- super().__init__(parent)
- self.transcription_task = task
-
- @pyqtSlot()
- def run(self):
- if self.transcription_task.source == FileTranscriptionTask.Source.URL_IMPORT:
- cookiefile = os.getenv("BUZZ_DOWNLOAD_COOKIEFILE")
-
- # First extract info to get the video title
- extract_options = {
- "logger": logging.getLogger(),
- }
- if cookiefile:
- extract_options["cookiefile"] = cookiefile
-
- try:
- with YoutubeDL(extract_options) as ydl_info:
- info = ydl_info.extract_info(self.transcription_task.url, download=False)
- video_title = info.get("title", "audio")
- except Exception as exc:
- logging.debug(f"Error extracting video info: {exc}")
- video_title = "audio"
-
- # Sanitize title for use as filename
- video_title = YoutubeDL.sanitize_info({"title": video_title})["title"]
- # Remove characters that are problematic in filenames
- for char in ['/', '\\', ':', '*', '?', '"', '<', '>', '|']:
- video_title = video_title.replace(char, '_')
-
- # Create temp directory and use video title as filename
- temp_dir = tempfile.mkdtemp()
- temp_output_path = os.path.join(temp_dir, video_title)
- wav_file = temp_output_path + ".wav"
- wav_file = str(Path(wav_file).resolve())
-
- options = {
- "format": "bestaudio/best",
- "progress_hooks": [self.on_download_progress],
- "outtmpl": temp_output_path,
- "logger": logging.getLogger(),
- }
-
- if cookiefile:
- options["cookiefile"] = cookiefile
-
- ydl = YoutubeDL(options)
-
- try:
- logging.debug(f"Downloading audio file from URL: {self.transcription_task.url}")
- ydl.download([self.transcription_task.url])
- except Exception as exc:
- logging.debug(f"Error downloading audio: {exc.msg}")
- self.error.emit(exc.msg)
- return
-
- cmd = [
- "ffmpeg",
- "-nostdin",
- "-threads", "0",
- "-i", temp_output_path,
- "-ac", "1",
- "-ar", str(whisper_audio.SAMPLE_RATE),
- "-acodec", "pcm_s16le",
- "-loglevel", "panic",
- 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,
- env=app_env,
- creationflags=subprocess.CREATE_NO_WINDOW
- )
- else:
- result = subprocess.run(cmd, capture_output=True)
-
- if len(result.stderr):
- logging.warning(f"Error processing downloaded audio. Error: {result.stderr.decode()}")
- raise Exception(f"Error processing downloaded audio: {result.stderr.decode()}")
-
- self.transcription_task.file_path = wav_file
- logging.debug(f"Downloaded audio to file: {self.transcription_task.file_path}")
-
- try:
- segments = self.transcribe()
- except Exception as exc:
- logging.exception("")
- self.error.emit(str(exc))
- return
-
- for segment in segments:
- segment.text = segment.text.strip()
-
- self.completed.emit(segments)
-
- for (
- output_format
- ) in self.transcription_task.file_transcription_options.output_formats:
- default_path = get_output_file_path(
- file_path=self.transcription_task.file_path,
- output_format=output_format,
- language=self.transcription_task.transcription_options.language,
- output_directory=self.transcription_task.output_directory,
- model=self.transcription_task.transcription_options.model,
- task=self.transcription_task.transcription_options.task,
- )
-
- write_output(
- path=default_path, segments=segments, output_format=output_format
- )
-
- if self.transcription_task.source == FileTranscriptionTask.Source.FOLDER_WATCH:
- # Use original_file_path if available (before speech extraction changed file_path)
- source_path = (
- self.transcription_task.original_file_path
- or self.transcription_task.file_path
- )
- if source_path and os.path.exists(source_path):
- if self.transcription_task.delete_source_file:
- os.remove(source_path)
- else:
- shutil.move(
- source_path,
- os.path.join(
- self.transcription_task.output_directory,
- os.path.basename(source_path),
- ),
- )
-
- def on_download_progress(self, data: dict):
- if data["status"] == "downloading":
- self.download_progress.emit(data["downloaded_bytes"] / data["total_bytes"])
-
- @abstractmethod
- def transcribe(self) -> List[Segment]:
- ...
-
- @abstractmethod
- def stop(self):
- ...
-
-
-def write_output(
- path: str,
- segments: List[Segment],
- output_format: OutputFormat,
- segment_key: str = 'text'
-):
- logging.debug(
- "Writing transcription output, path = %s, output format = %s, number of segments = %s",
- path,
- output_format,
- len(segments),
- )
-
- with open(os.fsencode(path), "w", encoding="utf-8") as file:
- if output_format == OutputFormat.TXT:
- combined_text = ""
- previous_end_time = None
-
- paragraph_split_time = int(os.getenv("BUZZ_PARAGRAPH_SPLIT_TIME", "2000"))
-
- for segment in segments:
- if previous_end_time is not None and (segment.start - previous_end_time) >= paragraph_split_time:
- combined_text += "\n\n"
- combined_text += getattr(segment, segment_key).strip() + " "
- previous_end_time = segment.end
-
- file.write(combined_text)
-
- elif output_format == OutputFormat.VTT:
- file.write("WEBVTT\n\n")
- for segment in segments:
- file.write(
- f"{to_timestamp(segment.start)} --> {to_timestamp(segment.end)}\n"
- )
- file.write(f"{getattr(segment, segment_key)}\n\n")
-
- elif output_format == OutputFormat.SRT:
- for i, segment in enumerate(segments):
- file.write(f"{i + 1}\n")
- file.write(
- f'{to_timestamp(segment.start, ms_separator=",")} --> {to_timestamp(segment.end, ms_separator=",")}\n'
- )
- file.write(f"{getattr(segment, segment_key)}\n\n")
-
- logging.debug("Written transcription output")
-
-
-def to_timestamp(ms: float, ms_separator=".") -> str:
- hr = int(ms / (1000 * 60 * 60))
- ms -= hr * (1000 * 60 * 60)
- min = int(ms / (1000 * 60))
- ms -= min * (1000 * 60)
- sec = int(ms / 1000)
- ms = int(ms - sec * 1000)
- return f"{hr:02d}:{min:02d}:{sec:02d}{ms_separator}{ms:03d}"
-
-# To detect when transcription source is a video
-VIDEO_EXTENSIONS = {".mp4", ".mov", ".mkv", ".avi", ".m4v", ".webm", ".ogm", ".wmv"}
-
-def is_video_file(path: str) -> bool:
- return Path(path).suffix.lower() in VIDEO_EXTENSIONS
diff --git a/buzz/transcriber/local_whisper_cpp_server_transcriber.py b/buzz/transcriber/local_whisper_cpp_server_transcriber.py
deleted file mode 100644
index d57252fe..00000000
--- a/buzz/transcriber/local_whisper_cpp_server_transcriber.py
+++ /dev/null
@@ -1,94 +0,0 @@
-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
-
-
-# Currently unused, but kept for future reference
-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
- cmd = [
- 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)),
- "--model", task.model_path,
- "--suppress-nst"
- ]
-
- if task.transcription_options.language is not None:
- cmd.extend(["--language", task.transcription_options.language])
-
- logging.debug(f"Starting Whisper server with command: {' '.join(cmd)}")
-
- self.process = subprocess.Popen(
- cmd,
- 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 = ""
- if self.process.stderr is not None:
- 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",
- max_retries=0
- )
-
- 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()
\ No newline at end of file
diff --git a/buzz/transcriber/openai_whisper_api_file_transcriber.py b/buzz/transcriber/openai_whisper_api_file_transcriber.py
deleted file mode 100644
index b86b51ba..00000000
--- a/buzz/transcriber/openai_whisper_api_file_transcriber.py
+++ /dev/null
@@ -1,293 +0,0 @@
-import logging
-import math
-import os
-import sys
-import subprocess
-import tempfile
-
-from pathlib import Path
-from typing import Optional, List
-
-from PyQt6.QtCore import QObject
-from openai import OpenAI
-
-from buzz.settings.settings import Settings
-from buzz.transcriber.file_transcriber import FileTranscriber, app_env
-from buzz.transcriber.transcriber import FileTranscriptionTask, Segment, Task
-
-
-def append_segment(result, txt: bytes, start: int, end: int):
- if txt == b'':
- return True
-
- # try-catch will guard against multi-byte utf-8 characters
- # https://github.com/ggerganov/whisper.cpp/issues/1798
- try:
- result.append(
- Segment(
- start=start * 10, # centisecond to ms
- end=end * 10, # centisecond to ms
- text=txt.decode("utf-8"),
- )
- )
-
- return True
- except UnicodeDecodeError:
- return False
-
-class OpenAIWhisperAPIFileTranscriber(FileTranscriber):
- def __init__(self, task: FileTranscriptionTask, parent: Optional["QObject"] = None):
- super().__init__(task=task, parent=parent)
- settings = Settings()
- custom_openai_base_url = settings.value(
- key=Settings.Key.CUSTOM_OPENAI_BASE_URL, default_value=""
- )
- self.task = task.transcription_options.task
- self.openai_client = OpenAI(
- api_key=self.transcription_task.transcription_options.openai_access_token,
- base_url=custom_openai_base_url if custom_openai_base_url else None,
- max_retries=0
- )
- self.whisper_api_model = settings.value(
- key=Settings.Key.OPENAI_API_MODEL, default_value="whisper-1"
- )
- self.word_level_timings = self.transcription_task.transcription_options.word_level_timings
- logging.debug("Will use whisper API on %s, %s",
- custom_openai_base_url, self.whisper_api_model)
-
- def transcribe(self) -> List[Segment]:
- logging.debug(
- "Starting OpenAI Whisper API file transcription, file path = %s, task = %s",
- self.transcription_task.file_path,
- self.task,
- )
-
- mp3_file = tempfile.mktemp() + ".mp3"
- mp3_file = str(Path(mp3_file).resolve())
-
- cmd = [
- "ffmpeg",
- "-threads", "0",
- "-loglevel", "panic",
- "-i", self.transcription_task.file_path, mp3_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,
- env=app_env,
- creationflags = subprocess.CREATE_NO_WINDOW
- )
- else:
- result = subprocess.run(cmd, capture_output=True)
-
- if result.returncode != 0:
- logging.warning(f"FFMPEG audio load warning. Process return code was not zero: {result.returncode}")
-
- if len(result.stderr):
- logging.warning(f"FFMPEG audio load error. Error: {result.stderr.decode()}")
- raise Exception(f"FFMPEG Failed to load audio: {result.stderr.decode()}")
-
- # fmt: off
- cmd = [
- "ffprobe",
- "-v", "error",
- "-show_entries", "format=duration",
- "-of", "default=noprint_wrappers=1:nokey=1",
- mp3_file,
- ]
-
- # fmt: on
- if sys.platform == "win32":
- si = subprocess.STARTUPINFO()
- si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
- si.wShowWindow = subprocess.SW_HIDE
-
- duration_secs = float(
- subprocess.run(
- cmd,
- capture_output=True,
- check=True,
- startupinfo=si,
- env=app_env,
- creationflags=subprocess.CREATE_NO_WINDOW
- ).stdout.decode("utf-8"),
- )
- else:
- duration_secs = float(
- subprocess.run(cmd, capture_output=True, check=True).stdout.decode("utf-8")
- )
-
- total_size = os.path.getsize(mp3_file)
- max_chunk_size = 25 * 1024 * 1024
-
- self.progress.emit((0, 100))
-
- if total_size < max_chunk_size:
- return self.get_segments_for_file(mp3_file)
-
- # If the file is larger than 25MB, split into chunks
- # and transcribe each chunk separately
- num_chunks = math.ceil(total_size / max_chunk_size)
- chunk_duration = duration_secs / num_chunks
-
- segments = []
-
- for i in range(num_chunks):
- chunk_start = i * chunk_duration
- chunk_end = min((i + 1) * chunk_duration, duration_secs)
-
- chunk_file = tempfile.mktemp() + ".mp3"
- chunk_file = str(Path(chunk_file).resolve())
-
- # fmt: off
- cmd = [
- "ffmpeg",
- "-i", mp3_file,
- "-ss", str(chunk_start),
- "-to", str(chunk_end),
- "-c", "copy",
- chunk_file,
- ]
- # fmt: on
- if sys.platform == "win32":
- si = subprocess.STARTUPINFO()
- si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
- si.wShowWindow = subprocess.SW_HIDE
- subprocess.run(
- cmd,
- capture_output=True,
- check=True,
- startupinfo=si,
- env=app_env,
- creationflags=subprocess.CREATE_NO_WINDOW
- )
- else:
- subprocess.run(cmd, capture_output=True, check=True)
-
- logging.debug('Created chunk file "%s"', chunk_file)
-
- segments.extend(
- self.get_segments_for_file(
- chunk_file, offset_ms=int(chunk_start * 1000)
- )
- )
- os.remove(chunk_file)
- self.progress.emit((i + 1, num_chunks))
-
- return segments
-
- @staticmethod
- def get_value(segment, key, default=None):
- if hasattr(segment, key):
- return getattr(segment, key)
- if isinstance(segment, dict):
- return segment.get(key, default)
- return default
-
- def get_segments_for_file(self, file: str, offset_ms: int = 0):
- with open(file, "rb") as file:
- # gpt-4o models don't support verbose_json format
- response_format = "json" if self.whisper_api_model.startswith("gpt-4o") else "verbose_json"
-
- options = {
- "model": self.whisper_api_model,
- "file": file,
- "response_format": response_format,
- "prompt": self.transcription_task.transcription_options.initial_prompt,
- }
-
- if self.word_level_timings:
- options["timestamp_granularities"] = ["word"]
-
- transcript = (
- self.openai_client.audio.transcriptions.create(
- **options,
- language=self.transcription_task.transcription_options.language,
- )
- if self.transcription_task.transcription_options.task == Task.TRANSCRIBE
- else self.openai_client.audio.translations.create(**options)
- )
-
- segments = getattr(transcript, "segments", None)
-
- words = getattr(transcript, "words", None)
- if words is None and "words" in transcript.model_extra:
- words = transcript.model_extra["words"]
-
- if segments is None:
- if "segments" in transcript.model_extra:
- segments = transcript.model_extra["segments"]
- else:
- # gpt-4o models return only text without segments/timestamps
- segments = [{"text": transcript.text, "start": 0, "end": 0, "words": words}]
-
- result_segments = []
- if self.word_level_timings:
-
- # Detect response from whisper.cpp API
- first_segment = segments[0] if segments else None
- is_whisper_cpp = (first_segment and hasattr(first_segment, "tokens")
- and hasattr(first_segment, "avg_logprob") and hasattr(first_segment, "no_speech_prob"))
-
- if is_whisper_cpp:
- txt_buffer = b''
- txt_start = 0
- txt_end = 0
-
- for segment in segments:
- for word in self.get_value(segment, "words"):
-
- txt = self.get_value(word, "word").encode("utf-8")
- start = self.get_value(word, "start")
- end = self.get_value(word, "end")
-
- if txt.startswith(b' ') and append_segment(result_segments, txt_buffer, txt_start, txt_end):
- txt_buffer = txt
- txt_start = start
- txt_end = end
- continue
-
- if txt.startswith(b', '):
- txt_buffer += b','
- append_segment(result_segments, txt_buffer, txt_start, txt_end)
- txt_buffer = txt.lstrip(b',')
- txt_start = start
- txt_end = end
- continue
-
- txt_buffer += txt
- txt_end = end
-
- # Append the last segment
- append_segment(result_segments, txt_buffer, txt_start, txt_end)
-
- else:
- for segment in segments:
- for word in self.get_value(segment, "words"):
- result_segments.append(
- Segment(
- int(self.get_value(word, "start") * 1000 + offset_ms),
- int(self.get_value(word, "end") * 1000 + offset_ms),
- self.get_value(word, "word"),
- )
- )
- else:
- result_segments = [
- Segment(
- int(self.get_value(segment, "start", 0) * 1000 + offset_ms),
- int(self.get_value(segment, "end", 0) * 1000 + offset_ms),
- self.get_value(segment, "text", ""),
- )
- for segment in segments
- ]
-
- return result_segments
-
- def stop(self):
- pass
diff --git a/buzz/transcriber/recording_transcriber.py b/buzz/transcriber/recording_transcriber.py
deleted file mode 100644
index 3d0105ba..00000000
--- a/buzz/transcriber/recording_transcriber.py
+++ /dev/null
@@ -1,524 +0,0 @@
-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
-
-# Preload CUDA libraries before importing torch
-from buzz import cuda_setup # noqa: F401
-
-import torch
-import numpy as np
-import sounddevice
-from sounddevice import PortAudioError
-from openai import OpenAI
-from PyQt6.QtCore import QObject, pyqtSignal
-
-from buzz import whisper_audio
-from buzz.locale import _
-from buzz.assets import APP_BASE_DIR
-from buzz.model_loader import ModelType, map_language_to_mms
-from buzz.settings.settings import Settings
-from buzz.transcriber.transcriber import TranscriptionOptions, Task, DEFAULT_WHISPER_TEMPERATURE
-from buzz.transformers_whisper import TransformersTranscriber
-from buzz.settings.recording_transcriber_mode import RecordingTranscriberMode
-
-import whisper
-import faster_whisper
-
-
-class RecordingTranscriber(QObject):
- transcription = pyqtSignal(str)
- finished = pyqtSignal()
- error = pyqtSignal(str)
- amplitude_changed = pyqtSignal(float)
- average_amplitude_changed = pyqtSignal(float)
- queue_size_changed = pyqtSignal(int)
- is_running = False
- SAMPLE_RATE = whisper_audio.SAMPLE_RATE
-
- def __init__(
- self,
- transcription_options: TranscriptionOptions,
- input_device_index: Optional[int],
- sample_rate: int,
- model_path: str,
- sounddevice: sounddevice,
- parent: Optional[QObject] = None,
- ) -> None:
- super().__init__(parent)
- self.settings = Settings()
- self.transcriber_mode = list(RecordingTranscriberMode)[
- self.settings.value(key=Settings.Key.RECORDING_TRANSCRIBER_MODE, default_value=0)]
- self.transcription_options = transcription_options
- self.current_stream = None
- self.input_device_index = input_device_index
- self.sample_rate = sample_rate if sample_rate is not None else whisper_audio.SAMPLE_RATE
- self.model_path = model_path
- self.n_batch_samples = int(5 * self.sample_rate) # 5 seconds
- self.keep_sample_seconds = 0.15
- if self.transcriber_mode == RecordingTranscriberMode.APPEND_AND_CORRECT:
- self.n_batch_samples = int(transcription_options.transcription_step * self.sample_rate)
- self.keep_sample_seconds = 1.5
- # pause queueing if more than 3 batches behind
- self.max_queue_size = 3 * self.n_batch_samples
- self.queue = np.ndarray([], dtype=np.float32)
- self.mutex = threading.Lock()
- self.sounddevice = sounddevice
- self.openai_client = None
- self.whisper_api_model = self.settings.value(
- key=Settings.Key.OPENAI_API_MODEL, default_value="whisper-1"
- )
- self.process = None
- self._stderr_lines: list[bytes] = []
-
- def start(self):
- self.is_running = True
- model = None
- model_path = self.model_path
- keep_samples = int(self.keep_sample_seconds * self.sample_rate)
-
- force_cpu = os.getenv("BUZZ_FORCE_CPU", "false")
- use_cuda = torch.cuda.is_available() and force_cpu == "false"
-
- if torch.cuda.is_available():
- logging.debug(f"CUDA version detected: {torch.version.cuda}")
-
- if self.transcription_options.model.model_type == ModelType.WHISPER:
- 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:
- self.start_local_whisper_server()
- if self.openai_client is None:
- if not self.is_running:
- self.finished.emit()
- else:
- self.error.emit(_("Whisper server failed to start. Check logs for details."))
- return
- 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")
- model_root_dir = os.getenv("BUZZ_MODEL_ROOT", model_root_dir)
-
- device = "auto"
- if torch.cuda.is_available() and torch.version.cuda < "12":
- 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"
-
- # Check if user wants reduced GPU memory usage (int8 quantization)
- reduce_gpu_memory = os.getenv("BUZZ_REDUCE_GPU_MEMORY", "false") != "false"
- compute_type = "default"
- if reduce_gpu_memory:
- compute_type = "int8" if device == "cpu" else "int8_float16"
- logging.debug(f"Using {compute_type} compute type for reduced memory usage")
-
- model = faster_whisper.WhisperModel(
- model_size_or_path=model_path,
- download_root=model_root_dir,
- device=device,
- compute_type=compute_type,
- cpu_threads=(os.cpu_count() or 8)//2,
- )
-
- elif self.transcription_options.model.model_type == ModelType.OPEN_AI_WHISPER_API:
- custom_openai_base_url = self.settings.value(
- key=Settings.Key.CUSTOM_OPENAI_BASE_URL, default_value=""
- )
- self.openai_client = OpenAI(
- api_key=self.transcription_options.openai_access_token,
- base_url=custom_openai_base_url if custom_openai_base_url else None,
- max_retries=0
- )
- logging.debug("Will use whisper API on %s, %s",
- custom_openai_base_url, self.whisper_api_model)
- else: # ModelType.HUGGING_FACE
- model = TransformersTranscriber(model_path)
-
- initial_prompt = self.transcription_options.initial_prompt
-
- logging.debug(
- "Recording, transcription options = %s, model path = %s, sample rate = %s, device = %s",
- self.transcription_options,
- model_path,
- self.sample_rate,
- self.input_device_index,
- )
-
- try:
- with self.sounddevice.InputStream(
- samplerate=self.sample_rate,
- device=self.input_device_index,
- dtype="float32",
- channels=1,
- callback=self.stream_callback,
- ):
- while self.is_running:
- if self.queue.size >= self.n_batch_samples:
- self.mutex.acquire()
- cut = self.find_silence_cut_point(
- self.queue[:self.n_batch_samples], self.sample_rate
- )
- samples = self.queue[:cut]
- if self.transcriber_mode == RecordingTranscriberMode.APPEND_AND_CORRECT:
- self.queue = self.queue[cut - keep_samples:]
- else:
- self.queue = self.queue[cut:]
- self.mutex.release()
-
- amplitude = self.amplitude(samples)
- self.average_amplitude_changed.emit(amplitude)
- self.queue_size_changed.emit(self.queue.size)
-
- logging.debug(
- "Processing next frame, sample size = %s, queue size = %s, amplitude = %s",
- samples.size,
- self.queue.size,
- amplitude,
- )
-
- if amplitude < self.transcription_options.silence_threshold:
- time.sleep(0.5)
- continue
-
- time_started = datetime.datetime.now()
-
- if (
- self.transcription_options.model.model_type
- == ModelType.WHISPER
- ):
- assert isinstance(model, whisper.Whisper)
- result = model.transcribe(
- audio=samples,
- language=self.transcription_options.language,
- task=self.transcription_options.task.value,
- initial_prompt=initial_prompt,
- temperature=DEFAULT_WHISPER_TEMPERATURE,
- no_speech_threshold=0.4,
- fp16=False,
- )
- elif (
- self.transcription_options.model.model_type
- == ModelType.FASTER_WHISPER
- ):
- assert isinstance(model, faster_whisper.WhisperModel)
- whisper_segments, info = model.transcribe(
- audio=samples,
- language=self.transcription_options.language
- if self.transcription_options.language != ""
- else None,
- task=self.transcription_options.task.value,
- # Prevent crash on Windows https://github.com/SYSTRAN/faster-whisper/issues/71#issuecomment-1526263764
- temperature=0 if platform.system() == "Windows" else DEFAULT_WHISPER_TEMPERATURE,
- initial_prompt=self.transcription_options.initial_prompt,
- word_timestamps=False,
- without_timestamps=True,
- no_speech_threshold=0.4,
- )
- result = {"text": " ".join([segment.text for segment in whisper_segments])}
- elif (
- self.transcription_options.model.model_type
- == ModelType.HUGGING_FACE
- ):
- assert isinstance(model, TransformersTranscriber)
- # Handle MMS-specific language and task
- if model.is_mms_model:
- language = map_language_to_mms(
- self.transcription_options.language or "eng"
- )
- effective_task = Task.TRANSCRIBE.value
- else:
- language = (
- self.transcription_options.language
- if self.transcription_options.language is not None
- else "en"
- )
- effective_task = self.transcription_options.task.value
-
- result = model.transcribe(
- audio=samples,
- language=language,
- task=effective_task,
- )
- else: # OPEN_AI_WHISPER_API, also used for WHISPER_CPP
- if self.openai_client is None:
- self.error.emit(_("A connection error occurred"))
- return
-
- # scale samples to 16-bit PCM
- pcm_data = (samples * 32767).astype(np.int16).tobytes()
-
- temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".wav")
- temp_filename = temp_file.name
-
- with wave.open(temp_filename, 'wb') as wf:
- wf.setnchannels(1)
- wf.setsampwidth(2)
- wf.setframerate(self.sample_rate)
- wf.writeframes(pcm_data)
-
- with open(temp_filename, 'rb') as temp_file:
- options = {
- "model": self.whisper_api_model,
- "file": temp_file,
- "response_format": "json",
- "prompt": self.transcription_options.initial_prompt,
- }
-
- try:
- transcript = (
- self.openai_client.audio.transcriptions.create(
- **options,
- language=self.transcription_options.language,
- )
- if self.transcription_options.task == Task.TRANSCRIBE
- else self.openai_client.audio.translations.create(**options)
- )
-
- 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:
- if self.is_running:
- result = {"text": f"Error: {str(e)}"}
- else:
- result = {"text": ""}
-
- os.unlink(temp_filename)
-
- next_text: str = result.get("text")
-
- # Update initial prompt between successive recording chunks
- initial_prompt = next_text
-
- logging.debug(
- "Received next result, length = %s, time taken = %s",
- len(next_text),
- datetime.datetime.now() - time_started,
- )
- self.transcription.emit(next_text)
- else:
- time.sleep(0.5)
-
- except PortAudioError as exc:
- self.error.emit(str(exc))
- logging.exception("PortAudio error during recording")
- return
- except Exception as exc:
- logging.exception("Unexpected error during recording")
- self.error.emit(str(exc))
- return
-
- # Cleanup before emitting finished to avoid destroying QThread
- # while this function is still on the call stack
- if model:
- del model
- if torch.cuda.is_available():
- torch.cuda.empty_cache()
-
- self.finished.emit()
-
- @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
- provided by Whisper if the microphone supports it, or else it uses the device's default
- sample rate.
- """
- sample_rate = whisper_audio.SAMPLE_RATE
- try:
- sounddevice.check_input_settings(device=device_id, samplerate=sample_rate)
- return sample_rate
- except PortAudioError:
- device_info = sounddevice.query_devices(device=device_id)
- if isinstance(device_info, dict):
- return int(device_info.get("default_samplerate", sample_rate))
- return sample_rate
-
- def stream_callback(self, in_data: np.ndarray, frame_count, time_info, status):
- # Try to enqueue the next block. If the queue is already full, drop the block.
- chunk: np.ndarray = in_data.ravel()
-
- amplitude = self.amplitude(chunk)
- self.amplitude_changed.emit(amplitude)
-
- with self.mutex:
- if self.queue.size < self.max_queue_size:
- self.queue = np.append(self.queue, chunk)
-
- @staticmethod
- def find_silence_cut_point(samples: np.ndarray, sample_rate: int,
- search_seconds: float = 1.5,
- window_seconds: float = 0.02,
- silence_ratio: float = 0.5) -> int:
- """Return index of the last quiet point in the final search_seconds of samples.
-
- Scans backwards through short windows; returns the midpoint of the rightmost
- window whose RMS is below silence_ratio * mean_rms of the search region.
- Falls back to len(samples) if no quiet window is found.
- """
- window = int(window_seconds * sample_rate)
- search_start = max(0, len(samples) - int(search_seconds * sample_rate))
- region = samples[search_start:]
- n_windows = (len(region) - window) // window
- if n_windows < 1:
- return len(samples)
-
- energies = np.array([
- np.sqrt(np.mean(region[i * window:(i + 1) * window] ** 2))
- for i in range(n_windows)
- ])
- mean_energy = energies.mean()
- threshold = silence_ratio * mean_energy
-
- for i in range(n_windows - 1, -1, -1):
- if energies[i] < threshold:
- cut = search_start + i * window + window // 2
- return cut
-
- return len(samples)
-
- @staticmethod
- def amplitude(arr: np.ndarray):
- return float(np.sqrt(np.mean(arr**2)))
-
- def _drain_stderr(self):
- if self.process and self.process.stderr:
- for line in self.process.stderr:
- self._stderr_lines.append(line)
-
- def stop_recording(self):
- self.is_running = False
- if self.process and self.process.poll() is None:
- self.process.terminate()
- try:
- self.process.wait(timeout=5)
- except subprocess.TimeoutExpired:
- self.process.kill()
- logging.warning("Whisper server process had to be killed after timeout")
-
- def start_local_whisper_server(self):
- # Reduce verbose HTTP client logging from OpenAI/httpx
- logging.getLogger("httpx").setLevel(logging.WARNING)
- logging.getLogger("httpcore").setLevel(logging.WARNING)
- logging.getLogger("openai").setLevel(logging.WARNING)
-
- self.transcription.emit(_("Starting Whisper.cpp..."))
-
- if platform.system() == "Darwin" and platform.machine() == "arm64":
- self.transcription.emit(_("First time use of a model may take up to several minutest to load."))
-
- self.process = None
-
- server_executable = "whisper-server.exe" if sys.platform == "win32" else "whisper-server"
- server_path = os.path.join(APP_BASE_DIR, "whisper_cpp", server_executable)
-
- # If running Mac and Windows installed version
- if not os.path.exists(server_path):
- server_path = os.path.join(APP_BASE_DIR, "buzz", "whisper_cpp", server_executable)
-
- cmd = [
- server_path,
- "--port", "3003",
- "--inference-path", "/audio/transcriptions",
- "--threads", str(os.getenv("BUZZ_WHISPERCPP_N_THREADS", (os.cpu_count() or 8) // 2)),
- "--model", self.model_path,
- "--no-timestamps",
- # Protections against hallucinated repetition. Seems to be problem on macOS
- # https://github.com/ggml-org/whisper.cpp/issues/1507
- "--max-context", "64",
- "--entropy-thold", "2.8",
- "--suppress-nst"
- ]
-
- if self.transcription_options.language is not None:
- cmd.extend(["--language", self.transcription_options.language])
- else:
- cmd.extend(["--language", "auto"])
-
- logging.debug(f"Starting Whisper server with command: {' '.join(cmd)}")
-
- try:
- if sys.platform == "win32":
- self.process = subprocess.Popen(
- cmd,
- stdout=subprocess.DEVNULL,
- stderr=subprocess.PIPE,
- shell=False,
- creationflags=subprocess.CREATE_NO_WINDOW
- )
- else:
- self.process = subprocess.Popen(
- cmd,
- stdout=subprocess.DEVNULL,
- stderr=subprocess.PIPE,
- shell=False,
- )
- except Exception as e:
- error_msg = f"Failed to start whisper-server subprocess: {str(e)}"
- logging.error(error_msg)
- return
-
- # Drain stderr in a background thread to prevent pipe buffer from filling
- # up and blocking the subprocess (especially on Windows with compiled exe).
- self._stderr_lines = []
- stderr_thread = threading.Thread(target=self._drain_stderr, daemon=True)
- stderr_thread.start()
-
- # Wait for server to start and load model, checking periodically
- for i in range(100): # 10 seconds total, in 0.1s increments
- if not self.is_running or self.process.poll() is not None:
- break
- time.sleep(0.1)
-
- if self.process is not None and self.process.poll() is None:
- self.transcription.emit(_("Starting transcription..."))
- logging.debug(f"Whisper server started successfully.")
- logging.debug(f"Model: {self.model_path}")
- else:
- stderr_thread.join(timeout=2)
- stderr_output = b"".join(self._stderr_lines).decode(errors="replace")
- 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)
-
- return
-
- self.openai_client = OpenAI(
- api_key="not-used",
- base_url="http://127.0.0.1:3003",
- timeout=30.0,
- max_retries=0
- )
-
- def __del__(self):
- if self.process and self.process.poll() is None:
- self.process.terminate()
- try:
- self.process.wait(timeout=5)
- except subprocess.TimeoutExpired:
- self.process.kill()
\ No newline at end of file
diff --git a/buzz/transcriber/transcriber.py b/buzz/transcriber/transcriber.py
deleted file mode 100644
index 7e803e80..00000000
--- a/buzz/transcriber/transcriber.py
+++ /dev/null
@@ -1,264 +0,0 @@
-import datetime
-import enum
-import os
-import uuid
-from dataclasses import dataclass, field
-from random import randint
-from typing import List, Optional, Tuple, Set
-
-from dataclasses_json import dataclass_json, config, Exclude
-
-from buzz.locale import _
-from buzz.model_loader import TranscriptionModel
-from buzz.settings.settings import Settings
-
-DEFAULT_WHISPER_TEMPERATURE = (0.0, 0.2, 0.4, 0.6, 0.8, 1.0)
-
-
-class Task(enum.Enum):
- TRANSLATE = "translate"
- TRANSCRIBE = "transcribe"
-
-
-TASK_LABEL_TRANSLATIONS = {
- Task.TRANSLATE: _("Translate to English"),
- Task.TRANSCRIBE: _("Transcribe"),
-}
-
-
-@dataclass
-class Segment:
- start: int # start time in ms
- end: int # end time in ms
- text: str
- translation: str = ""
-
-
-LANGUAGES = {
- "en": _("English"),
- "zh": _("Chinese"),
- "de": _("German"),
- "es": _("Spanish"),
- "ru": _("Russian"),
- "ko": _("Korean"),
- "fr": _("French"),
- "ja": _("Japanese"),
- "pt": _("Portuguese"),
- "tr": _("Turkish"),
- "pl": _("Polish"),
- "ca": _("Catalan"),
- "nl": _("Dutch"),
- "ar": _("Arabic"),
- "sv": _("Swedish"),
- "it": _("Italian"),
- "id": _("Indonesian"),
- "hi": _("Hindi"),
- "fi": _("Finnish"),
- "vi": _("Vietnamese"),
- "he": _("Hebrew"),
- "uk": _("Ukrainian"),
- "el": _("Greek"),
- "ms": _("Malay"),
- "cs": _("Czech"),
- "ro": _("Romanian"),
- "da": _("Danish"),
- "hu": _("Hungarian"),
- "ta": _("Tamil"),
- "no": _("Norwegian"),
- "th": _("Thai"),
- "ur": _("Urdu"),
- "hr": _("Croatian"),
- "bg": _("Bulgarian"),
- "lt": _("Lithuanian"),
- "la": _("Latin"),
- "mi": _("Maori"),
- "ml": _("Malayalam"),
- "cy": _("Welsh"),
- "sk": _("Slovak"),
- "te": _("Telugu"),
- "fa": _("Persian"),
- "lv": _("Latvian"),
- "bn": _("Bengali"),
- "sr": _("Serbian"),
- "az": _("Azerbaijani"),
- "sl": _("Slovenian"),
- "kn": _("Kannada"),
- "et": _("Estonian"),
- "mk": _("Macedonian"),
- "br": _("Breton"),
- "eu": _("Basque"),
- "is": _("Icelandic"),
- "hy": _("Armenian"),
- "ne": _("Nepali"),
- "mn": _("Mongolian"),
- "bs": _("Bosnian"),
- "kk": _("Kazakh"),
- "sq": _("Albanian"),
- "sw": _("Swahili"),
- "gl": _("Galician"),
- "mr": _("Marathi"),
- "pa": _("Punjabi"),
- "si": _("Sinhala"),
- "km": _("Khmer"),
- "sn": _("Shona"),
- "yo": _("Yoruba"),
- "so": _("Somali"),
- "af": _("Afrikaans"),
- "oc": _("Occitan"),
- "ka": _("Georgian"),
- "be": _("Belarusian"),
- "tg": _("Tajik"),
- "sd": _("Sindhi"),
- "gu": _("Gujarati"),
- "am": _("Amharic"),
- "yi": _("Yiddish"),
- "lo": _("Lao"),
- "uz": _("Uzbek"),
- "fo": _("Faroese"),
- "ht": _("Haitian Creole"),
- "ps": _("Pashto"),
- "tk": _("Turkmen"),
- "nn": _("Nynorsk"),
- "mt": _("Maltese"),
- "sa": _("Sanskrit"),
- "lb": _("Luxembourgish"),
- "my": _("Myanmar"),
- "bo": _("Tibetan"),
- "tl": _("Tagalog"),
- "mg": _("Malagasy"),
- "as": _("Assamese"),
- "tt": _("Tatar"),
- "haw": _("Hawaiian"),
- "ln": _("Lingala"),
- "ha": _("Hausa"),
- "ba": _("Bashkir"),
- "jw": _("Javanese"),
- "su": _("Sundanese"),
- "yue": _("Cantonese"),
-}
-
-
-@dataclass()
-class TranscriptionOptions:
- language: Optional[str] = None
- task: Task = Task.TRANSCRIBE
- model: TranscriptionModel = field(default_factory=TranscriptionModel)
- word_level_timings: bool = False
- extract_speech: bool = False
- temperature: Tuple[float, ...] = DEFAULT_WHISPER_TEMPERATURE
- initial_prompt: str = ""
- openai_access_token: str = field(
- default="", metadata=config(exclude=Exclude.ALWAYS)
- )
- enable_llm_translation: bool = False
- llm_prompt: str = ""
- llm_model: str = ""
- silence_threshold: float = 0.0025
- line_separator: str = "\n\n"
- transcription_step: float = 3.5
-
-
-def humanize_language(language: str) -> str:
- if language == "":
- return _("Detect Language")
- return LANGUAGES[language].title()
-
-
-@dataclass()
-class FileTranscriptionOptions:
- file_paths: Optional[List[str]] = None
- url: Optional[str] = None
- output_formats: Set["OutputFormat"] = field(default_factory=set)
-
-
-@dataclass_json
-@dataclass
-class FileTranscriptionTask:
- class Status(enum.Enum):
- QUEUED = "queued"
- IN_PROGRESS = "in_progress"
- COMPLETED = "completed"
- FAILED = "failed"
- CANCELED = "canceled"
-
- class Source(enum.Enum):
- FILE_IMPORT = "file_import"
- URL_IMPORT = "url_import"
- FOLDER_WATCH = "folder_watch"
-
- transcription_options: TranscriptionOptions
- file_transcription_options: FileTranscriptionOptions
- model_path: str
- # deprecated: use uid
- id: int = field(default_factory=lambda: randint(0, 100_000_000))
- uid: uuid.UUID = field(default_factory=uuid.uuid4)
- segments: List[Segment] = field(default_factory=list)
- status: Optional[Status] = None
- fraction_completed = 0.0
- error: Optional[str] = None
- queued_at: Optional[datetime.datetime] = None
- started_at: Optional[datetime.datetime] = None
- completed_at: Optional[datetime.datetime] = None
- output_directory: Optional[str] = None
- source: Source = Source.FILE_IMPORT
- file_path: Optional[str] = None
- original_file_path: Optional[str] = None # Original path before speech extraction
- delete_source_file: bool = False
- url: Optional[str] = None
- fraction_downloaded: float = 0.0
-
-
-class OutputFormat(enum.Enum):
- TXT = "txt"
- SRT = "srt"
- VTT = "vtt"
-
-
-class Stopped(Exception):
- pass
-
-
-SUPPORTED_AUDIO_FORMATS = "Media files (*.mp3 *.wav *.m4a *.ogg *.opus *.flac *.mp4 *.webm *.ogm *.mov *.mkv *.avi *.wmv);;\
-Audio files (*.mp3 *.wav *.m4a *.ogg *.opus *.flac);;\
-Video files (*.mp4 *.webm *.ogm *.mov *.mkv *.avi *.wmv);;\
-All files (*.*)"
-
-
-def get_output_file_path(
- file_path: str,
- task: Task,
- language: Optional[str],
- model: TranscriptionModel,
- output_format: OutputFormat,
- output_directory: str | None = None,
- export_file_name_template: str | None = None,
-):
- input_file_name = os.path.splitext(os.path.basename(file_path))[0]
- # Remove "_speech" suffix from extracted speech files
- if input_file_name.endswith("_speech"):
- input_file_name = input_file_name[:-7]
- date_time_now = datetime.datetime.now().strftime("%d-%b-%Y %H-%M-%S")
-
- export_file_name_template = (
- export_file_name_template
- if export_file_name_template is not None
- else Settings().get_default_export_file_template()
- )
-
- output_file_name = (
- export_file_name_template.replace("{{ input_file_name }}", input_file_name)
- .replace("{{ task }}", task.value)
- .replace("{{ language }}", language or "")
- .replace("{{ model_type }}", model.model_type.value)
- .replace(
- "{{ model_size }}",
- model.whisper_model_size.value
- if model.whisper_model_size is not None
- else "",
- )
- .replace("{{ date_time }}", date_time_now)
- + f".{output_format.value}"
- )
-
- output_directory = output_directory or os.path.dirname(file_path)
- return os.path.join(output_directory, output_file_name)
diff --git a/buzz/transcriber/whisper_cpp.py b/buzz/transcriber/whisper_cpp.py
deleted file mode 100644
index db4aff84..00000000
--- a/buzz/transcriber/whisper_cpp.py
+++ /dev/null
@@ -1,383 +0,0 @@
-import platform
-import os
-import sys
-import logging
-import subprocess
-import json
-from typing import List
-from buzz.assets import APP_BASE_DIR
-from buzz.transcriber.transcriber import Segment, Task, FileTranscriptionTask
-from buzz.transcriber.file_transcriber import app_env
-
-
-IS_VULKAN_SUPPORTED = False
-try:
- import vulkan
-
- 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).
- if platform.system() in ("Linux", "Windows") and ((major > 1) or (major == 1 and minor >= 2)):
- IS_VULKAN_SUPPORTED = True
-
-except (ImportError, Exception) as e:
- logging.debug(f"Vulkan import error: {e}")
-
- IS_VULKAN_SUPPORTED = False
-
-
-class WhisperCpp:
- @staticmethod
- def transcribe(task: FileTranscriptionTask) -> List[Segment]:
- """Transcribe audio using whisper-cli subprocess."""
- cli_executable = "whisper-cli.exe" if sys.platform == "win32" else "whisper-cli"
- whisper_cli_path = os.path.join(APP_BASE_DIR, "whisper_cpp", cli_executable)
-
- # If running Mac and Windows installed version
- if not os.path.exists(whisper_cli_path):
- whisper_cli_path = os.path.join(APP_BASE_DIR, "buzz", "whisper_cpp", cli_executable)
-
- language = (
- task.transcription_options.language
- if task.transcription_options.language is not None
- else "en"
- )
-
- # Check if file format is supported, convert to WAV if not
- supported_formats = ('.mp3', '.wav', '.flac')
- file_ext = os.path.splitext(task.file_path)[1].lower()
-
- temp_file = None
- file_to_process = task.file_path
-
- if file_ext not in supported_formats:
- temp_file = task.file_path + ".wav"
-
- logging.info(f"Converting {task.file_path} to WAV format")
-
- # Convert using ffmpeg
- ffmpeg_cmd = [
- "ffmpeg",
- "-i", task.file_path,
- "-ar", "16000", # 16kHz sample rate (whisper standard)
- "-ac", "1", # mono
- "-y", # overwrite output file
- temp_file
- ]
-
- try:
- if sys.platform == "win32":
- si = subprocess.STARTUPINFO()
- si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
- si.wShowWindow = subprocess.SW_HIDE
- result = subprocess.run(
- ffmpeg_cmd,
- capture_output=True,
- startupinfo=si,
- env=app_env,
- creationflags=subprocess.CREATE_NO_WINDOW,
- check = True
- )
- else:
- result = subprocess.run(ffmpeg_cmd, capture_output=True, check=True)
-
- file_to_process = temp_file
- except subprocess.CalledProcessError as e:
- raise Exception(f"Failed to convert audio file: {e.stderr.decode()}")
- except FileNotFoundError:
- raise Exception("ffmpeg not found. Please install ffmpeg to process this audio format.")
-
- # Build the command
- cmd = [
- whisper_cli_path,
- "--model", task.model_path,
- "--language", language,
- "--print-progress",
- "--suppress-nst",
- # Protections against hallucinated repetition. Seems to be problem on macOS
- # https://github.com/ggml-org/whisper.cpp/issues/1507
- "--max-context", "64",
- "--entropy-thold", "2.8",
- "--output-json-full",
- "--threads", str(os.getenv("BUZZ_WHISPERCPP_N_THREADS", (os.cpu_count() or 8) // 2)),
- "-f", file_to_process,
- ]
-
- # Add VAD if the model is available
- vad_model_path = os.path.join(os.path.dirname(whisper_cli_path), "ggml-silero-v6.2.0.bin")
- if os.path.exists(vad_model_path):
- cmd.extend(["--vad", "--vad-model", vad_model_path])
-
- # Add translate flag if needed
- if task.transcription_options.task == Task.TRANSLATE:
- cmd.extend(["--translate"])
-
- # Force CPU if specified
- force_cpu = os.getenv("BUZZ_FORCE_CPU", "false")
- if force_cpu != "false" or (not IS_VULKAN_SUPPORTED and platform.system() != "Darwin"):
- cmd.extend(["--no-gpu"])
-
- print(f"Running Whisper CLI: {' '.join(cmd)}")
-
- # Run the whisper-cli process
- if sys.platform == "win32":
- si = subprocess.STARTUPINFO()
- si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
- si.wShowWindow = subprocess.SW_HIDE
- process = subprocess.Popen(
- cmd,
- stdout=subprocess.DEVNULL,
- stderr=subprocess.PIPE,
- text=True,
- startupinfo=si,
- env=app_env,
- creationflags=subprocess.CREATE_NO_WINDOW
- )
- else:
- process = subprocess.Popen(
- cmd,
- stdout=subprocess.DEVNULL,
- stderr=subprocess.PIPE,
- text=True,
- )
-
- # Capture stderr for progress updates
- stderr_output = []
- while True:
- line = process.stderr.readline()
- if not line:
- break
- stderr_output.append(line.strip())
- # Progress is written to stderr
- sys.stderr.write(line)
-
- process.wait()
-
- if process.returncode != 0:
- # Clean up temp file if conversion was done
- if temp_file and os.path.exists(temp_file):
- try:
- os.remove(temp_file)
- except Exception as e:
- print(f"Failed to remove temporary file {temp_file}: {e}")
- raise Exception(f"whisper-cli failed with return code {process.returncode}")
-
- # Find and read the generated JSON file
- # whisper-cli generates: input_file.ext.json (e.g., file.mp3.json)
- json_output_path = f"{file_to_process}.json"
-
- try:
- # Read JSON with latin-1 to preserve raw bytes, then handle encoding per field
- # This is needed because whisper-cli can write invalid UTF-8 sequences for multi-byte characters
- with open(json_output_path, 'r', encoding='latin-1') as f:
- result = json.load(f)
-
- segments = []
-
- # Handle word-level timings
- if task.transcription_options.word_level_timings:
- # Extract word-level timestamps from tokens array
- # Combine tokens into words using similar logic as whisper_cpp.py
- transcription = result.get("transcription", [])
-
- # Languages that don't use spaces between words
- # For these, each token is treated as a separate word
- non_space_languages = {"zh", "ja", "th", "lo", "km", "my"}
- is_non_space_language = language in non_space_languages
-
- for segment_data in transcription:
- tokens = segment_data.get("tokens", [])
-
- if is_non_space_language:
- # For languages without spaces (Chinese, Japanese, etc.),
- # each complete UTF-8 character is treated as a separate word.
- # Some characters may be split across multiple tokens as raw bytes.
- char_buffer = b""
- char_start = 0
- char_end = 0
-
- def flush_complete_chars(buffer: bytes, start: int, end: int):
- """Extract and output all complete UTF-8 characters from buffer.
- Returns any remaining incomplete bytes."""
- nonlocal segments
- remaining = buffer
- pos = 0
-
- while pos < len(remaining):
- # Try to decode one character at a time
- for char_len in range(1, min(5, len(remaining) - pos + 1)):
- try:
- char = remaining[pos:pos + char_len].decode("utf-8")
- # Successfully decoded a character
- if char.strip():
- segments.append(
- Segment(
- start=start,
- end=end,
- text=char,
- translation=""
- )
- )
- pos += char_len
- break
- except UnicodeDecodeError:
- if char_len == 4 or pos + char_len >= len(remaining):
- # Incomplete character at end - return as remaining
- return remaining[pos:]
- else:
- # Couldn't decode, might be incomplete at end
- return remaining[pos:]
-
- return b""
-
- for token_data in tokens:
- token_text = token_data.get("text", "")
-
- # Skip special tokens like [_TT_], [_BEG_]
- if token_text.startswith("[_"):
- continue
-
- if not token_text:
- continue
-
- token_start = int(token_data.get("offsets", {}).get("from", 0))
- token_end = int(token_data.get("offsets", {}).get("to", 0))
-
- # Convert latin-1 string back to original bytes
- token_bytes = token_text.encode("latin-1")
-
- if not char_buffer:
- char_start = token_start
-
- char_buffer += token_bytes
- char_end = token_end
-
- # Try to flush complete characters
- char_buffer = flush_complete_chars(char_buffer, char_start, char_end)
-
- # If buffer was fully flushed, reset start time for next char
- if not char_buffer:
- char_start = token_end
-
- # Flush any remaining buffer at end of segment
- if char_buffer:
- flush_complete_chars(char_buffer, char_start, char_end)
- else:
- # For space-separated languages, accumulate tokens into words
- word_buffer = b""
- word_start = 0
- word_end = 0
-
- def append_word(buffer: bytes, start: int, end: int):
- """Try to decode and append a word segment, handling multi-byte UTF-8"""
- if not buffer:
- return True
-
- # Try to decode as UTF-8
- # https://github.com/ggerganov/whisper.cpp/issues/1798
- try:
- text = buffer.decode("utf-8").strip()
- if text:
- segments.append(
- Segment(
- start=start,
- end=end,
- text=text,
- translation=""
- )
- )
- return True
- except UnicodeDecodeError:
- # Multi-byte character is split, continue accumulating
- return False
-
- for token_data in tokens:
- # Token text is read as latin-1, need to convert to bytes to get original data
- token_text = token_data.get("text", "")
-
- # Skip special tokens like [_TT_], [_BEG_]
- if token_text.startswith("[_"):
- continue
-
- if not token_text:
- continue
-
- # Skip low probability tokens
- token_p = token_data.get("p", 1.0)
- if token_p < 0.01:
- continue
-
- token_start = int(token_data.get("offsets", {}).get("from", 0))
- token_end = int(token_data.get("offsets", {}).get("to", 0))
-
- # Convert latin-1 string back to original bytes
- # (latin-1 preserves byte values as code points)
- token_bytes = token_text.encode("latin-1")
-
- # Check if token starts with space - indicates new word
- if token_bytes.startswith(b" ") and word_buffer:
- # Save previous word
- append_word(word_buffer, word_start, word_end)
- # Start new word
- word_buffer = token_bytes
- word_start = token_start
- word_end = token_end
- elif token_bytes.startswith(b", "):
- # Handle comma - save word with comma, then start new word
- word_buffer += b","
- append_word(word_buffer, word_start, word_end)
- word_buffer = token_bytes.lstrip(b",")
- word_start = token_start
- word_end = token_end
- else:
- # Accumulate token into current word
- if not word_buffer:
- word_start = token_start
- word_buffer += token_bytes
- word_end = token_end
-
- # Add the last word
- append_word(word_buffer, word_start, word_end)
- else:
- # Use segment-level timestamps
- transcription = result.get("transcription", [])
- for segment_data in transcription:
- # Segment text is also read as latin-1, convert back to UTF-8
- segment_text_latin1 = segment_data.get("text", "")
- try:
- # Convert latin-1 string to bytes, then decode as UTF-8
- segment_text = segment_text_latin1.encode("latin-1").decode("utf-8").strip()
- except (UnicodeDecodeError, UnicodeEncodeError):
- # If conversion fails, use the original text
- segment_text = segment_text_latin1.strip()
-
- segments.append(
- Segment(
- start=int(segment_data.get("offsets", {}).get("from", 0)),
- end=int(segment_data.get("offsets", {}).get("to", 0)),
- text=segment_text,
- translation=""
- )
- )
-
- return segments
- finally:
- # Clean up the generated JSON file
- if os.path.exists(json_output_path):
- try:
- os.remove(json_output_path)
- except Exception as e:
- print(f"Failed to remove JSON output file {json_output_path}: {e}")
-
- # Clean up temporary audio file if conversion was done
- if temp_file and os.path.exists(temp_file):
- try:
- os.remove(temp_file)
- except Exception as e:
- print(f"Failed to remove temporary file {temp_file}: {e}")
\ No newline at end of file
diff --git a/buzz/transcriber/whisper_file_transcriber.py b/buzz/transcriber/whisper_file_transcriber.py
deleted file mode 100644
index 8633043c..00000000
--- a/buzz/transcriber/whisper_file_transcriber.py
+++ /dev/null
@@ -1,457 +0,0 @@
-import datetime
-import json
-import logging
-import multiprocessing
-import re
-import os
-import sys
-
-# Preload CUDA libraries before importing torch - required for subprocess contexts
-from buzz import cuda_setup # noqa: F401
-
-import torch
-import platform
-import subprocess
-from platformdirs import user_cache_dir
-from multiprocessing.connection import Connection
-from threading import Thread
-from typing import Optional, List
-
-import tqdm
-from PyQt6.QtCore import QObject
-
-from buzz import whisper_audio
-from buzz.conn import pipe_stderr
-from buzz.model_loader import ModelType, WhisperModelSize, map_language_to_mms
-from buzz.transformers_whisper import TransformersTranscriber
-from buzz.transcriber.file_transcriber import FileTranscriber
-from buzz.transcriber.transcriber import FileTranscriptionTask, Segment, Task, DEFAULT_WHISPER_TEMPERATURE
-from buzz.transcriber.whisper_cpp import WhisperCpp
-
-import av
-import faster_whisper
-import whisper
-import stable_whisper
-from stable_whisper import WhisperResult
-
-PROGRESS_REGEX = re.compile(r"\d+(\.\d+)?%")
-
-
-def check_file_has_audio_stream(file_path: str) -> None:
- """Check if a media file has at least one audio stream.
-
- Raises:
- ValueError: If the file has no audio streams.
- """
- try:
- with av.open(file_path) as container:
- if len(container.streams.audio) == 0:
- raise ValueError("No audio streams found")
- except av.error.InvalidDataError as e:
- raise ValueError(f"Invalid media file: {e}")
- except av.error.FileNotFoundError:
- raise ValueError("File not found")
-
-
-class WhisperFileTranscriber(FileTranscriber):
- """WhisperFileTranscriber transcribes an audio file to text, writes the text to a file, and then opens the file
- using the default program for opening txt files."""
-
- current_process: multiprocessing.Process
- running = False
- read_line_thread: Optional[Thread] = None
- READ_LINE_THREAD_STOP_TOKEN = "--STOP--"
-
- def __init__(
- self, task: FileTranscriptionTask, parent: Optional["QObject"] = None
- ) -> None:
- super().__init__(task, parent)
- self.segments = []
- self.started_process = False
- self.stopped = False
- self.recv_pipe = None
- self.send_pipe = None
- self.error_message = None
-
- def transcribe(self) -> List[Segment]:
- time_started = datetime.datetime.now()
- logging.debug(
- "Starting whisper file transcription, task = %s", self.transcription_task
- )
-
- if torch.cuda.is_available():
- logging.debug(f"CUDA version detected: {torch.version.cuda}")
-
- self.recv_pipe, self.send_pipe = multiprocessing.Pipe(duplex=False)
-
- self.current_process = multiprocessing.Process(
- 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=(self.recv_pipe,))
- self.read_line_thread.start()
-
- # Only join the process if it was actually started
- if self.started_process:
- self.current_process.join()
-
- # Close the send pipe after process ends to signal read_line thread to stop
- # This prevents the read thread from blocking on recv() after the process is gone
- try:
- if self.send_pipe and not self.send_pipe.closed:
- self.send_pipe.close()
- except OSError:
- pass
-
- # Close the receive pipe to unblock the read_line thread
- try:
- if self.recv_pipe and not self.recv_pipe.closed:
- self.recv_pipe.close()
- except OSError:
- pass
-
- # 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,"
- " number of segments = %s",
- self.current_process.exitcode,
- datetime.datetime.now() - time_started,
- len(self.segments),
- )
-
- if self.current_process.exitcode != 0:
- # Check if the process was terminated (likely due to cancellation)
- # Exit codes 124-128 are often used for termination signals
- if self.current_process.exitcode in [124, 125, 126, 127, 128, 130, 137, 143]:
- # Process was likely terminated, treat as cancellation
- logging.debug("Whisper process was terminated (exit code: %s), treating as cancellation", self.current_process.exitcode)
- raise Exception("Transcription was canceled")
- else:
- raise Exception(self.error_message or "Unknown error")
-
- return self.segments
-
- @classmethod
- def transcribe_whisper(
- cls, stderr_conn: Connection, task: FileTranscriptionTask
- ) -> None:
- # Patch subprocess on Windows to prevent console window flash
- # This is needed because multiprocessing spawns a new process without the main process patches
- if sys.platform == "win32":
- import subprocess
- _original_run = subprocess.run
- _original_popen = subprocess.Popen
-
- def _patched_run(*args, **kwargs):
- if 'startupinfo' not in kwargs:
- si = subprocess.STARTUPINFO()
- si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
- si.wShowWindow = subprocess.SW_HIDE
- kwargs['startupinfo'] = si
- if 'creationflags' not in kwargs:
- kwargs['creationflags'] = subprocess.CREATE_NO_WINDOW
- return _original_run(*args, **kwargs)
-
- class _PatchedPopen(subprocess.Popen):
- def __init__(self, *args, **kwargs):
- if 'startupinfo' not in kwargs:
- si = subprocess.STARTUPINFO()
- si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
- si.wShowWindow = subprocess.SW_HIDE
- kwargs['startupinfo'] = si
- if 'creationflags' not in kwargs:
- kwargs['creationflags'] = subprocess.CREATE_NO_WINDOW
- super().__init__(*args, **kwargs)
-
- subprocess.run = _patched_run
- subprocess.Popen = _PatchedPopen
-
- try:
- # Check if the file has audio streams before processing
- check_file_has_audio_stream(task.file_path)
-
- with pipe_stderr(stderr_conn):
- if task.transcription_options.model.model_type == ModelType.WHISPER_CPP:
- segments = cls.transcribe_whisper_cpp(task)
- elif task.transcription_options.model.model_type == ModelType.HUGGING_FACE:
- sys.stderr.write("0%\n")
- segments = cls.transcribe_hugging_face(task)
- sys.stderr.write("100%\n")
- elif (
- task.transcription_options.model.model_type == ModelType.FASTER_WHISPER
- ):
- segments = cls.transcribe_faster_whisper(task)
- elif task.transcription_options.model.model_type == ModelType.WHISPER:
- segments = cls.transcribe_openai_whisper(task)
- else:
- raise Exception(
- f"Invalid model type: {task.transcription_options.model.model_type}"
- )
-
- segments_json = json.dumps(segments, ensure_ascii=True, default=vars)
- sys.stderr.write(f"segments = {segments_json}\n")
- sys.stderr.write(WhisperFileTranscriber.READ_LINE_THREAD_STOP_TOKEN + "\n")
- except Exception as e:
- # Send error message back to the parent process
- stderr_conn.send(f"error = {str(e)}\n")
- stderr_conn.send(WhisperFileTranscriber.READ_LINE_THREAD_STOP_TOKEN + "\n")
- raise
-
- @classmethod
- def transcribe_whisper_cpp(cls, task: FileTranscriptionTask) -> List[Segment]:
- return WhisperCpp.transcribe(task)
-
- @classmethod
- def transcribe_hugging_face(cls, task: FileTranscriptionTask) -> List[Segment]:
- model = TransformersTranscriber(task.model_path)
-
- # Handle language - MMS uses ISO 639-3 codes, Whisper uses ISO 639-1
- if model.is_mms_model:
- language = map_language_to_mms(task.transcription_options.language or "eng")
- # MMS only supports transcription, ignore translation task
- effective_task = Task.TRANSCRIBE.value
- # MMS doesn't support word-level timestamps
- word_timestamps = False
- else:
- language = (
- task.transcription_options.language
- if task.transcription_options.language is not None
- else "en"
- )
- effective_task = task.transcription_options.task.value
- word_timestamps = task.transcription_options.word_level_timings
-
- result = model.transcribe(
- audio=task.file_path,
- language=language,
- task=effective_task,
- word_timestamps=word_timestamps,
- )
- return [
- Segment(
- start=int(segment.get("start") * 1000),
- end=int(segment.get("end") * 1000),
- text=segment.get("text"),
- translation=""
- )
- for segment in result.get("segments")
- ]
-
- @classmethod
- def transcribe_faster_whisper(cls, task: FileTranscriptionTask) -> List[Segment]:
- if task.transcription_options.model.whisper_model_size == WhisperModelSize.CUSTOM:
- model_size_or_path = task.transcription_options.model.hugging_face_model_id
- else:
- model_size_or_path = task.transcription_options.model.whisper_model_size.to_faster_whisper_model_size()
-
- model_root_dir = user_cache_dir("Buzz")
- model_root_dir = os.path.join(model_root_dir, "models")
- model_root_dir = os.getenv("BUZZ_MODEL_ROOT", model_root_dir)
- force_cpu = os.getenv("BUZZ_FORCE_CPU", "false")
-
- device = "auto"
- if torch.cuda.is_available() and torch.version.cuda < "12":
- 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"
-
- # Check if user wants reduced GPU memory usage (int8 quantization)
- reduce_gpu_memory = os.getenv("BUZZ_REDUCE_GPU_MEMORY", "false") != "false"
- compute_type = "default"
- if reduce_gpu_memory:
- compute_type = "int8" if device == "cpu" else "int8_float16"
- logging.debug(f"Using {compute_type} compute type for reduced memory usage")
-
- model = faster_whisper.WhisperModel(
- model_size_or_path=model_size_or_path,
- download_root=model_root_dir,
- device=device,
- compute_type=compute_type,
- cpu_threads=(os.cpu_count() or 8)//2,
- )
-
- batched_model = faster_whisper.BatchedInferencePipeline(model=model)
- whisper_segments, info = batched_model.transcribe(
- audio=task.file_path,
- language=task.transcription_options.language,
- task=task.transcription_options.task.value,
- # Prevent crash on Windows https://github.com/SYSTRAN/faster-whisper/issues/71#issuecomment-1526263764
- temperature = 0 if platform.system() == "Windows" else DEFAULT_WHISPER_TEMPERATURE,
- initial_prompt=task.transcription_options.initial_prompt,
- word_timestamps=task.transcription_options.word_level_timings,
- no_speech_threshold=0.4,
- log_progress=True,
- )
- segments = []
- for segment in whisper_segments:
- # Segment will contain words if word-level timings is True
- if segment.words:
- for word in segment.words:
- segments.append(
- Segment(
- start=int(word.start * 1000),
- end=int(word.end * 1000),
- text=word.word,
- translation=""
- )
- )
- else:
- segments.append(
- Segment(
- start=int(segment.start * 1000),
- end=int(segment.end * 1000),
- text=segment.text,
- translation=""
- )
- )
-
- return segments
-
- @classmethod
- def transcribe_openai_whisper(cls, task: FileTranscriptionTask) -> List[Segment]:
- force_cpu = os.getenv("BUZZ_FORCE_CPU", "false")
- use_cuda = torch.cuda.is_available() and force_cpu == "false"
-
- device = "cuda" if use_cuda else "cpu"
-
- # Monkeypatch torch.load to use weights_only=False for PyTorch 2.6+
- # This is required for loading Whisper models with the newer PyTorch versions
- original_torch_load = torch.load
- def patched_torch_load(*args, **kwargs):
- kwargs.setdefault('weights_only', False)
- return original_torch_load(*args, **kwargs)
-
- torch.load = patched_torch_load
- try:
- model = whisper.load_model(task.model_path, device=device)
- finally:
- torch.load = original_torch_load
-
- if task.transcription_options.word_level_timings:
- stable_whisper.modify_model(model)
- result: WhisperResult = model.transcribe(
- audio=whisper_audio.load_audio(task.file_path),
- language=task.transcription_options.language,
- task=task.transcription_options.task.value,
- temperature=DEFAULT_WHISPER_TEMPERATURE,
- initial_prompt=task.transcription_options.initial_prompt,
- no_speech_threshold=0.4,
- fp16=False,
- )
- return [
- Segment(
- start=int(word.start * 1000),
- end=int(word.end * 1000),
- text=word.word.strip(),
- translation=""
- )
- for segment in result.segments
- for word in segment.words
- ]
-
- result: dict = model.transcribe(
- audio=task.file_path,
- language=task.transcription_options.language,
- task=task.transcription_options.task.value,
- temperature=task.transcription_options.temperature,
- initial_prompt=task.transcription_options.initial_prompt,
- verbose=False,
- fp16=False,
- )
- segments = result.get("segments")
- return [
- Segment(
- start=int(segment.get("start") * 1000),
- end=int(segment.get("end") * 1000),
- text=segment.get("text"),
- translation=""
- )
- for segment in segments
- ]
-
- def stop(self):
- self.stopped = True
-
- if self.started_process:
- self.current_process.terminate()
-
- if self.read_line_thread and self.read_line_thread.is_alive():
- self.read_line_thread.join(timeout=5)
- if self.read_line_thread.is_alive():
- logging.warning("Read line thread still alive after 5s")
-
- self.current_process.join(timeout=10)
- if self.current_process.is_alive():
- logging.warning("Process didn't terminate gracefully, force killing")
- self.current_process.kill()
- self.current_process.join(timeout=5)
-
- try:
- if hasattr(self, 'send_pipe') and self.send_pipe:
- self.send_pipe.close()
- except Exception as e:
- logging.debug(f"Error closing send_pipe: {e}")
-
- try:
- if hasattr(self, 'recv_pipe') and self.recv_pipe:
- self.recv_pipe.close()
- except Exception as e:
- logging.debug(f"Error closing recv_pipe: {e}")
-
- def read_line(self, pipe: Connection):
- while True:
- try:
- line = pipe.recv().strip()
-
- # Uncomment to debug
- # print(f"*** DEBUG ***: {line}")
-
- except (EOFError, BrokenPipeError, ConnectionResetError, OSError):
- # Connection closed, broken, or process crashed (Windows RPC errors raise OSError)
- break
- except Exception as e:
- logging.debug(f"Error reading from pipe: {e}")
- break
-
- if line == self.READ_LINE_THREAD_STOP_TOKEN:
- return
-
- if line.startswith("segments = "):
- segments_dict = json.loads(line[11:])
- segments = [
- Segment(
- start=segment.get("start"),
- end=segment.get("end"),
- text=segment.get("text"),
- translation=""
- )
- for segment in segments_dict
- ]
- self.segments = segments
- elif line.startswith("error = "):
- self.error_message = line[8:]
- else:
- try:
- match = PROGRESS_REGEX.search(line)
- if match is not None:
- progress = int(match.group().strip("%"))
- self.progress.emit((progress, 100))
- except ValueError:
- logging.debug("whisper (stderr): %s", line)
- continue
diff --git a/buzz/transformers_whisper.py b/buzz/transformers_whisper.py
deleted file mode 100644
index d9995d5a..00000000
--- a/buzz/transformers_whisper.py
+++ /dev/null
@@ -1,520 +0,0 @@
-import os
-import sys
-import logging
-import platform
-import numpy as np
-
-# Preload CUDA libraries before importing torch
-from buzz import cuda_setup # noqa: F401
-
-import torch
-import requests
-from typing import Union
-from transformers import AutoModelForSpeechSeq2Seq, AutoProcessor, pipeline, BitsAndBytesConfig
-from transformers.pipelines import AutomaticSpeechRecognitionPipeline
-from transformers.pipelines.audio_utils import ffmpeg_read
-from transformers.pipelines.automatic_speech_recognition import is_torchaudio_available
-
-from buzz.model_loader import is_mms_model, map_language_to_mms
-
-
-def is_intel_mac() -> bool:
- """Check if running on Intel Mac (x86_64)."""
- return sys.platform == 'darwin' and platform.machine() == 'x86_64'
-
-
-def is_peft_model(model_id: str) -> bool:
- """Check if model is a PEFT model based on model ID containing '-peft'."""
- return "-peft" in model_id.lower()
-
-
-class PipelineWithProgress(AutomaticSpeechRecognitionPipeline): # pragma: no cover
- # Copy of transformers `AutomaticSpeechRecognitionPipeline.chunk_iter` method with custom progress output
- @staticmethod
- def chunk_iter(inputs, feature_extractor, chunk_len, stride_left, stride_right, dtype=None):
- inputs_len = inputs.shape[0]
- step = chunk_len - stride_left - stride_right
- for chunk_start_idx in range(0, inputs_len, step):
-
- # Buzz will print progress to stderr
- progress = int((chunk_start_idx / inputs_len) * 100)
- sys.stderr.write(f"{progress}%\n")
-
- chunk_end_idx = chunk_start_idx + chunk_len
- chunk = inputs[chunk_start_idx:chunk_end_idx]
- processed = feature_extractor(chunk, sampling_rate=feature_extractor.sampling_rate, return_tensors="pt")
- if dtype is not None:
- processed = processed.to(dtype=dtype)
- _stride_left = 0 if chunk_start_idx == 0 else stride_left
- is_last = chunk_end_idx >= inputs_len
- _stride_right = 0 if is_last else stride_right
-
- chunk_len = chunk.shape[0]
- stride = (chunk_len, _stride_left, _stride_right)
- if chunk.shape[0] > _stride_left:
- yield {"is_last": is_last, "stride": stride, **processed}
- if is_last:
- break
-
- # Copy of transformers `AutomaticSpeechRecognitionPipeline.preprocess` method with call to custom `chunk_iter`
- def preprocess(self, inputs, chunk_length_s=0, stride_length_s=None):
- if isinstance(inputs, str):
- if inputs.startswith("http://") or inputs.startswith("https://"):
- # We need to actually check for a real protocol, otherwise it's impossible to use a local file
- # like http_huggingface_co.png
- inputs = requests.get(inputs).content
- else:
- with open(inputs, "rb") as f:
- inputs = f.read()
-
- if isinstance(inputs, bytes):
- inputs = ffmpeg_read(inputs, self.feature_extractor.sampling_rate)
-
- stride = None
- extra = {}
- if isinstance(inputs, dict):
- stride = inputs.pop("stride", None)
- # Accepting `"array"` which is the key defined in `datasets` for
- # better integration
- if not ("sampling_rate" in inputs and ("raw" in inputs or "array" in inputs)):
- raise ValueError(
- "When passing a dictionary to AutomaticSpeechRecognitionPipeline, the dict needs to contain a "
- '"raw" key containing the numpy array representing the audio and a "sampling_rate" key, '
- "containing the sampling_rate associated with that array"
- )
-
- _inputs = inputs.pop("raw", None)
- if _inputs is None:
- # Remove path which will not be used from `datasets`.
- inputs.pop("path", None)
- _inputs = inputs.pop("array", None)
- in_sampling_rate = inputs.pop("sampling_rate")
- extra = inputs
- inputs = _inputs
- if in_sampling_rate != self.feature_extractor.sampling_rate:
- if is_torchaudio_available():
- from torchaudio import functional as F
- else:
- raise ImportError(
- "torchaudio is required to resample audio samples in AutomaticSpeechRecognitionPipeline. "
- "The torchaudio package can be installed through: `pip install torchaudio`."
- )
-
- inputs = F.resample(
- torch.from_numpy(inputs), in_sampling_rate, self.feature_extractor.sampling_rate
- ).numpy()
- ratio = self.feature_extractor.sampling_rate / in_sampling_rate
- else:
- ratio = 1
- if stride is not None:
- if stride[0] + stride[1] > inputs.shape[0]:
- raise ValueError("Stride is too large for input")
-
- # Stride needs to get the chunk length here, it's going to get
- # swallowed by the `feature_extractor` later, and then batching
- # can add extra data in the inputs, so we need to keep track
- # 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 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")
-
- if chunk_length_s:
- if stride_length_s is None:
- stride_length_s = chunk_length_s / 6
-
- if isinstance(stride_length_s, (int, float)):
- stride_length_s = [stride_length_s, stride_length_s]
-
- # 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)
- chunk_len = int(round(chunk_length_s * self.feature_extractor.sampling_rate / align_to) * align_to)
- stride_left = int(round(stride_length_s[0] * self.feature_extractor.sampling_rate / align_to) * align_to)
- stride_right = int(round(stride_length_s[1] * self.feature_extractor.sampling_rate / align_to) * align_to)
-
- if chunk_len < stride_left + stride_right:
- raise ValueError("Chunk length must be superior to stride length")
-
- # 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, **extra}
- else:
- if self.type == "seq2seq_whisper" and inputs.shape[0] > self.feature_extractor.n_samples:
- processed = self.feature_extractor(
- inputs,
- sampling_rate=self.feature_extractor.sampling_rate,
- truncation=False,
- padding="longest",
- return_tensors="pt",
- return_attention_mask=True,
- )
- else:
- 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:
- if self.type == "seq2seq":
- raise ValueError("Stride is only usable with CTC models, try removing it !")
-
- processed["stride"] = stride
- yield {"is_last": True, **processed, **extra}
-
-
-class TransformersTranscriber:
- """Unified transcriber for HuggingFace models (Whisper and MMS)."""
-
- def __init__(self, model_id: str):
- self.model_id = model_id
- self._is_mms = is_mms_model(model_id)
- self._is_peft = is_peft_model(model_id)
-
- @property
- def is_mms_model(self) -> bool:
- """Returns True if this is an MMS model."""
- return self._is_mms
-
- @property
- def is_peft_model(self) -> bool:
- """Returns True if this is a PEFT model."""
- return self._is_peft
-
- def transcribe(
- self,
- audio: Union[str, np.ndarray],
- language: str,
- task: str,
- word_timestamps: bool = False,
- ):
- """Transcribe audio using either Whisper or MMS model."""
- if self._is_mms:
- return self._transcribe_mms(audio, language)
- else:
- return self._transcribe_whisper(audio, language, task, word_timestamps)
-
- def _transcribe_whisper(
- self,
- audio: Union[str, np.ndarray],
- language: str,
- task: str,
- word_timestamps: bool = False,
- ):
- """Transcribe using Whisper model."""
- force_cpu = os.getenv("BUZZ_FORCE_CPU", "false")
- use_cuda = torch.cuda.is_available() and force_cpu == "false"
- device = "cuda" if use_cuda else "cpu"
- torch_dtype = torch.float16 if use_cuda else torch.float32
-
- # Check if this is a PEFT model
- if is_peft_model(self.model_id):
- model, processor, use_8bit = self._load_peft_model(device, torch_dtype)
- else:
- use_safetensors = True
- if os.path.isdir(self.model_id):
- safetensors_files = [f for f in os.listdir(self.model_id) if f.endswith(".safetensors")]
- use_safetensors = len(safetensors_files) > 0
-
- # Check if user wants reduced GPU memory usage (8-bit quantization)
- # Skip on Intel Macs as bitsandbytes is not available there
- reduce_gpu_memory = os.getenv("BUZZ_REDUCE_GPU_MEMORY", "false") != "false"
- use_8bit = False
- if device == "cuda" and reduce_gpu_memory and not is_intel_mac():
- try:
- import bitsandbytes # noqa: F401
- use_8bit = True
- print("Using 8-bit quantization for reduced GPU memory usage")
- except ImportError:
- print("bitsandbytes not available, using standard precision")
-
- if use_8bit:
- quantization_config = BitsAndBytesConfig(load_in_8bit=True)
- model = AutoModelForSpeechSeq2Seq.from_pretrained(
- self.model_id,
- quantization_config=quantization_config,
- device_map="auto",
- use_safetensors=use_safetensors
- )
- else:
- model = AutoModelForSpeechSeq2Seq.from_pretrained(
- self.model_id, torch_dtype=torch_dtype, low_cpu_mem_usage=True, use_safetensors=use_safetensors
- )
- model.to(device)
-
- model.generation_config.language = language
-
- processor = AutoProcessor.from_pretrained(self.model_id)
-
- pipeline_kwargs = {
- "task": "automatic-speech-recognition",
- "pipeline_class": PipelineWithProgress,
- "generate_kwargs": {
- "language": language,
- "task": task,
- "no_repeat_ngram_size": 3,
- "repetition_penalty": 1.2,
- },
- "model": model,
- "tokenizer": processor.tokenizer,
- "feature_extractor": processor.feature_extractor,
- # pipeline has built in chunking, works faster, but we loose progress output
- # needed for word level timestamps, otherwise there is huge RAM usage on longer audios
- "chunk_length_s": 30 if word_timestamps else None,
- "torch_dtype": torch_dtype,
- "ignore_warning": True, # Ignore warning about chunk_length_s being experimental for seq2seq models
- }
- if not use_8bit:
- pipeline_kwargs["device"] = device
- pipe = pipeline(**pipeline_kwargs)
-
- transcript = pipe(
- audio,
- return_timestamps="word" if word_timestamps else True,
- )
-
- segments = []
- for chunk in transcript['chunks']:
- start, end = chunk['timestamp']
- text = chunk['text']
-
- # Last segment may not have an end timestamp
- if start is None:
- start = 0
- if end is None:
- end = start + 0.1
-
- if end > start and text.strip() != "":
- segments.append({
- "start": 0 if start is None else start,
- "end": 0 if end is None else end,
- "text": text.strip(),
- "translation": ""
- })
-
- return {
- "text": transcript['text'],
- "segments": segments,
- }
-
- def _load_peft_model(self, device: str, torch_dtype):
- """Load a PEFT (Parameter-Efficient Fine-Tuning) model.
-
- PEFT models require loading the base model first, then applying the adapter.
- The base model path is extracted from the PEFT config.
-
- Returns:
- Tuple of (model, processor, use_8bit)
- """
- from peft import PeftModel, PeftConfig
- from transformers import WhisperForConditionalGeneration, WhisperFeatureExtractor, WhisperTokenizer
-
- print(f"Loading PEFT model: {self.model_id}")
-
- # Get the PEFT model ID (handle both local paths and repo IDs)
- peft_model_id = self._get_peft_repo_id()
-
- # Load PEFT config to get base model path
- peft_config = PeftConfig.from_pretrained(peft_model_id)
- base_model_path = peft_config.base_model_name_or_path
- print(f"PEFT base model: {base_model_path}")
-
- # Load the base Whisper model
- # Use 8-bit quantization on CUDA if user enabled "Reduce GPU RAM" and bitsandbytes is available
- # Skip on Intel Macs as bitsandbytes is not available there
- reduce_gpu_memory = os.getenv("BUZZ_REDUCE_GPU_MEMORY", "false") != "false"
- use_8bit = False
- if device == "cuda" and reduce_gpu_memory and not is_intel_mac():
- try:
- import bitsandbytes # noqa: F401
- use_8bit = True
- print("Using 8-bit quantization for reduced GPU memory usage")
- except ImportError:
- print("bitsandbytes not available, using standard precision for PEFT model")
-
- if use_8bit:
- quantization_config = BitsAndBytesConfig(load_in_8bit=True)
- model = WhisperForConditionalGeneration.from_pretrained(
- base_model_path,
- quantization_config=quantization_config,
- device_map="auto"
- )
- else:
- model = WhisperForConditionalGeneration.from_pretrained(
- base_model_path,
- torch_dtype=torch_dtype,
- low_cpu_mem_usage=True
- )
- model.to(device)
-
- # Apply the PEFT adapter
- model = PeftModel.from_pretrained(model, peft_model_id)
- model.config.use_cache = True
-
- # Load feature extractor and tokenizer from base model
- feature_extractor = WhisperFeatureExtractor.from_pretrained(base_model_path)
- tokenizer = WhisperTokenizer.from_pretrained(base_model_path, task="transcribe")
-
- # Create a simple processor-like object that the pipeline expects
- class PeftProcessor:
- def __init__(self, feature_extractor, tokenizer):
- self.feature_extractor = feature_extractor
- self.tokenizer = tokenizer
-
- processor = PeftProcessor(feature_extractor, tokenizer)
-
- return model, processor, use_8bit
-
- def _get_peft_repo_id(self) -> str:
- """Extract HuggingFace repo ID from local cache path for PEFT models."""
- model_id = self.model_id
-
- # If it's already a repo ID (contains / but not a file path), return as-is
- if "/" in model_id and not os.path.exists(model_id):
- return model_id
-
- # Extract repo ID from cache path
- if "models--" in model_id:
- parts = model_id.split("models--")
- if len(parts) > 1:
- repo_part = parts[1].split(os.sep + "snapshots")[0]
- repo_id = repo_part.replace("--", "/", 1)
- return repo_id
-
- # Fallback: return as-is
- return model_id
-
- def _get_mms_repo_id(self) -> str:
- """Extract HuggingFace repo ID from local cache path or return as-is if already a repo ID."""
- model_id = self.model_id
-
- # If it's already a repo ID (contains / but not a file path), return as-is
- if "/" in model_id and not os.path.exists(model_id):
- return model_id
-
- # Extract repo ID from cache path like:
- # Linux: /home/user/.cache/Buzz/models/models--facebook--mms-1b-all/snapshots/xxx
- # Windows: C:\Users\user\.cache\Buzz\models\models--facebook--mms-1b-all\snapshots\xxx
- if "models--" in model_id:
- # Extract the part after "models--" and before "/snapshots" or "\snapshots"
- parts = model_id.split("models--")
- if len(parts) > 1:
- # Split on os.sep to handle both Windows and Unix paths
- repo_part = parts[1].split(os.sep + "snapshots")[0]
- # Convert facebook--mms-1b-all to facebook/mms-1b-all
- repo_id = repo_part.replace("--", "/", 1)
- return repo_id
-
- # Fallback: return as-is
- return model_id
-
- def _transcribe_mms(
- self,
- audio: Union[str, np.ndarray],
- language: str,
- ):
- """Transcribe using MMS (Massively Multilingual Speech) model."""
- from transformers import Wav2Vec2ForCTC, AutoProcessor as MMSAutoProcessor
- from transformers.pipelines.audio_utils import ffmpeg_read as mms_ffmpeg_read
-
- force_cpu = os.getenv("BUZZ_FORCE_CPU", "false")
- use_cuda = torch.cuda.is_available() and force_cpu == "false"
- device = "cuda" if use_cuda else "cpu"
-
- # Map language code to ISO 639-3 for MMS
- mms_language = map_language_to_mms(language)
- print(f"MMS transcription with language: {mms_language} (original: {language})")
-
- sys.stderr.write("0%\n")
-
- # Use repo ID for MMS to allow adapter downloads
- # Local paths don't work for adapter downloads
- repo_id = self._get_mms_repo_id()
- print(f"MMS using repo ID: {repo_id} (from model_id: {self.model_id})")
-
- # Load processor and model with target language
- # This will download the language adapter if not cached
- processor = MMSAutoProcessor.from_pretrained(
- repo_id,
- target_lang=mms_language
- )
-
- model = Wav2Vec2ForCTC.from_pretrained(
- repo_id,
- target_lang=mms_language,
- ignore_mismatched_sizes=True
- )
- model.to(device)
-
- sys.stderr.write("25%\n")
-
- # Load and process audio
- if isinstance(audio, str):
- with open(audio, "rb") as f:
- audio_data = f.read()
- audio_array = mms_ffmpeg_read(audio_data, processor.feature_extractor.sampling_rate)
- else:
- audio_array = audio
-
- # Ensure audio is the right sample rate
- sampling_rate = processor.feature_extractor.sampling_rate
-
- sys.stderr.write("50%\n")
-
- # Process audio in chunks for progress reporting
- inputs = processor(
- audio_array,
- sampling_rate=sampling_rate,
- return_tensors="pt",
- padding=True
- )
- inputs = {k: v.to(device) for k, v in inputs.items()}
-
- sys.stderr.write("75%\n")
-
- # Run inference
- with torch.no_grad():
- outputs = model(**inputs).logits
-
- # Decode
- ids = torch.argmax(outputs, dim=-1)[0]
- transcription = processor.decode(ids)
-
- sys.stderr.write("100%\n")
-
- # Calculate approximate duration for segment
- duration = len(audio_array) / sampling_rate if isinstance(audio_array, np.ndarray) else 0
-
- # Return in same format as Whisper for consistency
- # MMS doesn't provide word-level timestamps, so we return a single segment
- return {
- "text": transcription,
- "segments": [{
- "start": 0,
- "end": duration,
- "text": transcription.strip(),
- "translation": ""
- }] if transcription.strip() else []
- }
-
-
-# Alias for backward compatibility
-TransformersWhisper = TransformersTranscriber
-
diff --git a/buzz/translator.py b/buzz/translator.py
deleted file mode 100644
index dfb0b948..00000000
--- a/buzz/translator.py
+++ /dev/null
@@ -1,198 +0,0 @@
-import os
-import re
-import logging
-import queue
-
-from typing import Optional, List, Tuple
-from openai import OpenAI, max_retries
-from PyQt6.QtCore import QObject, pyqtSignal
-
-from buzz.locale import _
-from buzz.settings.settings import Settings
-from buzz.store.keyring_store import get_password, Key
-from buzz.transcriber.transcriber import TranscriptionOptions
-from buzz.widgets.transcriber.advanced_settings_dialog import AdvancedSettingsDialog
-
-
-BATCH_SIZE = 10
-
-
-class Translator(QObject):
- translation = pyqtSignal(str, int)
- finished = pyqtSignal()
-
- def __init__(
- self,
- transcription_options: TranscriptionOptions,
- advanced_settings_dialog: AdvancedSettingsDialog,
- parent: Optional[QObject] = None,
- ) -> None:
- super().__init__(parent)
-
- logging.debug(f"Translator init: {transcription_options}")
-
- self.transcription_options = transcription_options
- self.advanced_settings_dialog = advanced_settings_dialog
- self.advanced_settings_dialog.transcription_options_changed.connect(
- self.on_transcription_options_changed
- )
-
- self.queue = queue.Queue()
-
- settings = Settings()
- custom_openai_base_url = os.getenv(
- "BUZZ_TRANSLATION_API_BASE_URl",
- settings.value(
- key=Settings.Key.CUSTOM_OPENAI_BASE_URL, default_value=""
- )
- )
- openai_api_key = os.getenv(
- "BUZZ_TRANSLATION_API_KEY",
- get_password(Key.OPENAI_API_KEY)
- )
- self.openai_client = OpenAI(
- api_key=openai_api_key,
- base_url=custom_openai_base_url if custom_openai_base_url else None,
- max_retries=0
- )
-
- def _translate_single(self, transcript: str, transcript_id: int) -> Tuple[str, int]:
- """Translate a single transcript via the API. Returns (translation, transcript_id)."""
- try:
- completion = self.openai_client.chat.completions.create(
- model=self.transcription_options.llm_model,
- messages=[
- {"role": "system", "content": self.transcription_options.llm_prompt},
- {"role": "user", "content": transcript}
- ],
- timeout=60.0,
- )
- except Exception as e:
- completion = None
- logging.error(f"Translation error! Server response: {e}")
-
- if completion and completion.choices and completion.choices[0].message:
- logging.debug(f"Received translation response: {completion}")
- return completion.choices[0].message.content, transcript_id
- else:
- logging.error(f"Translation error! Server response: {completion}")
- # Translation error
- return "", transcript_id
-
- def _translate_batch(self, items: List[Tuple[str, int]]) -> List[Tuple[str, int]]:
- """Translate multiple transcripts in a single API call.
- Returns list of (translation, transcript_id) in the same order as input."""
- numbered_parts = []
- for i, (transcript, _) in enumerate(items, 1):
- numbered_parts.append(f"[{i}] {transcript}")
- combined = "\n".join(numbered_parts)
-
- batch_prompt = (
- f"{self.transcription_options.llm_prompt}\n\n"
- f"You will receive {len(items)} numbered texts. "
- f"Process each one separately according to the instruction above "
- f"and return them in the exact same numbered format, e.g.:\n"
- f"[1] processed text\n[2] processed text"
- )
-
- try:
- completion = self.openai_client.chat.completions.create(
- model=self.transcription_options.llm_model,
- messages=[
- {"role": "system", "content": batch_prompt},
- {"role": "user", "content": combined}
- ],
- timeout=60.0,
- )
- except Exception as e:
- completion = None
- logging.error(f"Batch translation error! Server response: {e}")
-
- if not (completion and completion.choices and completion.choices[0].message):
- logging.error(f"Batch translation error! Server response: {completion}")
- # Translation error
- return [("", tid) for _, tid in items]
-
- response_text = completion.choices[0].message.content
- logging.debug(f"Received batch translation response: {response_text}")
-
- translations = self._parse_batch_response(response_text, len(items))
-
- results = []
- for i, (_, transcript_id) in enumerate(items):
- if i < len(translations):
- results.append((translations[i], transcript_id))
- else:
- # Translation error
- results.append(("", transcript_id))
- return results
-
- @staticmethod
- def _parse_batch_response(response: str, expected_count: int) -> List[str]:
- """Parse a numbered batch response like '[1] text\\n[2] text' into a list of strings."""
- # Split on [N] markers — re.split with a group returns: [before, group1, after1, group2, after2, ...]
- parts = re.split(r'\[(\d+)\]\s*', response)
-
- translations = {}
- for i in range(1, len(parts) - 1, 2):
- num = int(parts[i])
- text = parts[i + 1].strip()
- translations[num] = text
-
- return [
- translations.get(i, "")
- for i in range(1, expected_count + 1)
- ]
-
- def start(self):
- logging.debug("Starting translation queue")
-
- while True:
- item = self.queue.get() # Block until item available
-
- # Check for sentinel value (None means stop)
- if item is None:
- logging.debug("Translation queue received stop signal")
- break
-
- # Collect a batch: start with the first item, then drain more
- batch = [item]
- stop_after_batch = False
- while len(batch) < BATCH_SIZE:
- try:
- next_item = self.queue.get_nowait()
- if next_item is None:
- stop_after_batch = True
- break
- batch.append(next_item)
- except queue.Empty:
- break
-
- if len(batch) == 1:
- transcript, transcript_id = batch[0]
- translation, tid = self._translate_single(transcript, transcript_id)
- self.translation.emit(translation, tid)
- else:
- logging.debug(f"Translating batch of {len(batch)} in single request")
- results = self._translate_batch(batch)
- for translation, tid in results:
- self.translation.emit(translation, tid)
-
- if stop_after_batch:
- logging.debug("Translation queue received stop signal")
- break
-
- logging.debug("Translation queue stopped")
- self.finished.emit()
-
- def on_transcription_options_changed(
- self, transcription_options: TranscriptionOptions
- ):
- self.transcription_options = transcription_options
-
- def enqueue(self, transcript: str, transcript_id: Optional[int] = None):
- self.queue.put((transcript, transcript_id))
-
- def stop(self):
- # Send sentinel value to unblock and stop the worker thread
- self.queue.put(None)
diff --git a/buzz/update_checker.py b/buzz/update_checker.py
deleted file mode 100644
index ff052af4..00000000
--- a/buzz/update_checker.py
+++ /dev/null
@@ -1,163 +0,0 @@
-import json
-import logging
-import platform
-from datetime import datetime
-from typing import Optional
-from dataclasses import dataclass
-
-from PyQt6.QtCore import QObject, pyqtSignal, QUrl
-from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
-from buzz.__version__ import VERSION
-from buzz.settings.settings import Settings
-
-
-@dataclass
-class UpdateInfo:
- version: str
- release_notes: str
- download_urls: list
-
-class UpdateChecker(QObject):
- update_available = pyqtSignal(object)
-
- VERSION_JSON_URL = "https://github.com/chidiwilliams/buzz/releases/latest/download/version_info.json"
-
- CHECK_INTERVAL_DAYS = 7
-
- def __init__(
- self,
- settings: Settings,
- network_manager: Optional[QNetworkAccessManager] = None,
- parent: Optional[QObject] = None
- ):
- super().__init__(parent)
-
- self.settings = settings
-
- if network_manager is None:
- network_manager = QNetworkAccessManager(self)
- self.network_manager = network_manager
- self.network_manager.finished.connect(self._on_reply_finished)
-
- def should_check_for_updates(self) -> bool:
- """Check if we are on Windows/macOS and if 7 days passed"""
- system = platform.system()
- if system not in ("Windows", "Darwin"):
- logging.debug("Skipping update check on linux")
- return False
-
- last_check = self.settings.value(
- Settings.Key.LAST_UPDATE_CHECK,
- "",
- )
-
- if last_check:
- try:
- last_check_date = datetime.fromisoformat(last_check)
- days_since_check = (datetime.now() - last_check_date).days
- if days_since_check < self.CHECK_INTERVAL_DAYS:
- logging.debug(
- f"Skipping update check, last checked {days_since_check} days ago"
- )
- return False
- except ValueError:
- #Invalid date format
- pass
-
- return True
-
- def check_for_updates(self) -> None:
- """Start the network request"""
- if not self.should_check_for_updates():
- return
-
- logging.info("Checking for updates...")
-
- url = QUrl(self.VERSION_JSON_URL)
- request = QNetworkRequest(url)
- self.network_manager.get(request)
-
- def _on_reply_finished(self, reply: QNetworkReply) -> None:
- """Handles the network reply for version.json fetch"""
- self.settings.set_value(
- Settings.Key.LAST_UPDATE_CHECK,
- datetime.now().isoformat()
- )
-
- if reply.error() != QNetworkReply.NetworkError.NoError:
- error_msg = f"Failed to check for updates: {reply.errorString()}"
- logging.error(error_msg)
- reply.deleteLater()
- return
-
- try:
- data = json.loads(reply.readAll().data().decode("utf-8"))
- reply.deleteLater()
-
- remote_version = data.get("version", "")
- release_notes = data.get("release_notes", "")
- download_urls = data.get("download_urls", {})
-
- #Get the download url for current platform
- download_url = self._get_download_url(download_urls)
-
- if self._is_newer_version(remote_version):
- logging.info(f"Update available: {remote_version}")
-
- #Store the available version
- self.settings.set_value(
- Settings.Key.UPDATE_AVAILABLE_VERSION,
- remote_version
- )
-
- update_info = UpdateInfo(
- version=remote_version,
- release_notes=release_notes,
- download_urls=download_url
- )
- self.update_available.emit(update_info)
-
- else:
- logging.info("No update available")
- self.settings.set_value(
- Settings.Key.UPDATE_AVAILABLE_VERSION,
- ""
- )
-
- except (json.JSONDecodeError, KeyError) as e:
- error_msg = f"Failed to parse version info: {e}"
- logging.error(error_msg)
-
- def _get_download_url(self, download_urls: dict) -> list:
- system = platform.system()
- machine = platform.machine().lower()
-
- if system == "Windows":
- urls = download_urls.get("windows_x64", [])
- elif system == "Darwin":
- if machine in ("arm64", "aarch64"):
- urls = download_urls.get("macos_arm", [])
- else:
- urls = download_urls.get("macos_x86", [])
- else:
- urls = []
-
- return urls if isinstance(urls, list) else [urls]
-
- def _is_newer_version(self, remote_version: str) -> bool:
- """Compare remote version with current version"""
- try:
- current_parts = [int(x) for x in VERSION.split(".")]
- remote_parts = [int(x) for x in remote_version.split(".")]
-
- #pad with zeros if needed
- while len(current_parts) < len(remote_parts):
- current_parts.append(0)
- while len(remote_parts) < len(current_parts):
- remote_parts.append(0)
-
- return remote_parts > current_parts
-
- except ValueError:
- logging.error(f"Invalid version format: {VERSION} or {remote_version}")
- return False
\ No newline at end of file
diff --git a/buzz/whisper_audio.py b/buzz/whisper_audio.py
deleted file mode 100644
index ce3850b7..00000000
--- a/buzz/whisper_audio.py
+++ /dev/null
@@ -1,73 +0,0 @@
-import subprocess
-import numpy as np
-import sys
-import os
-import logging
-
-from buzz.assets import APP_BASE_DIR
-
-SAMPLE_RATE = 16000
-
-N_FFT = 400
-HOP_LENGTH = 160
-CHUNK_LENGTH = 30
-N_SAMPLES = CHUNK_LENGTH * SAMPLE_RATE # 480000 samples in a 30-second chunk
-
-app_env = os.environ.copy()
-app_env['PATH'] = os.pathsep.join([os.path.join(APP_BASE_DIR, "_internal")] + [app_env['PATH']])
-
-def load_audio(file: str, sr: int = SAMPLE_RATE):
- """
- Open an audio file and read as mono waveform, resampling as necessary
-
- Parameters
- ----------
- file: str
- The audio file to open
-
- sr: int
- The sample rate to resample the audio if necessary
-
- Returns
- -------
- A NumPy array containing the audio waveform, in float32 dtype.
- """
-
- # This launches a subprocess to decode audio while down-mixing
- # and resampling as necessary. Requires the ffmpeg CLI in PATH.
- # fmt: off
- cmd = [
- "ffmpeg",
- "-nostdin",
- "-threads", "0",
- "-i", file,
- "-f", "s16le",
- "-ac", "1",
- "-acodec", "pcm_s16le",
- "-ar", str(sr),
- "-loglevel", "panic",
- "-"
- ]
- # fmt: on
- 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,
- env=app_env,
- creationflags=subprocess.CREATE_NO_WINDOW
- )
- else:
- result = subprocess.run(cmd, capture_output=True)
-
- if result.returncode != 0:
- logging.warning(f"FFMPEG audio load warning. Process return code was not zero: {result.returncode}")
-
- if len(result.stderr):
- logging.warning(f"FFMPEG audio load error. Error: {result.stderr.decode()}")
- raise RuntimeError(f"FFMPEG Failed to load audio: {result.stderr.decode()}")
-
- return np.frombuffer(result.stdout, np.int16).flatten().astype(np.float32) / 32768.0
diff --git a/buzz/widgets/__init__.py b/buzz/widgets/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/buzz/widgets/about_dialog.py b/buzz/widgets/about_dialog.py
deleted file mode 100644
index 5c6b6757..00000000
--- a/buzz/widgets/about_dialog.py
+++ /dev/null
@@ -1,124 +0,0 @@
-import json
-from typing import Optional
-from platformdirs import user_log_dir
-
-from PyQt6 import QtGui
-from PyQt6.QtCore import Qt, QUrl
-from PyQt6.QtGui import QIcon, QPixmap, QDesktopServices
-from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
-from PyQt6.QtWidgets import (
- QDialog,
- QWidget,
- QVBoxLayout,
- QLabel,
- QPushButton,
- QDialogButtonBox,
- QMessageBox,
-)
-
-from buzz.__version__ import VERSION
-from buzz.widgets.icon import BUZZ_ICON_PATH, BUZZ_LARGE_ICON_PATH
-from buzz.locale import _
-from buzz.settings.settings import APP_NAME
-
-
-class AboutDialog(QDialog):
- GITHUB_API_LATEST_RELEASE_URL = (
- "https://api.github.com/repos/chidiwilliams/buzz/releases/latest"
- )
- GITHUB_LATEST_RELEASE_URL = "https://github.com/chidiwilliams/buzz/releases/latest"
-
- def __init__(
- self,
- network_access_manager: Optional[QNetworkAccessManager] = None,
- parent: Optional[QWidget] = None,
- ) -> None:
- super().__init__(parent)
-
- self.setWindowIcon(QIcon(BUZZ_ICON_PATH))
- self.setWindowTitle(f'{_("About")} {APP_NAME}')
-
- if network_access_manager is None:
- network_access_manager = QNetworkAccessManager()
-
- self.network_access_manager = network_access_manager
- self.network_access_manager.finished.connect(self.on_latest_release_reply)
-
- layout = QVBoxLayout(self)
-
- image_label = QLabel()
- pixmap = QPixmap(BUZZ_LARGE_ICON_PATH).scaled(
- 80,
- 80,
- Qt.AspectRatioMode.KeepAspectRatio,
- Qt.TransformationMode.SmoothTransformation,
- )
- image_label.setPixmap(pixmap)
- image_label.setAlignment(
- Qt.AlignmentFlag(
- Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignHCenter
- )
- )
-
- buzz_label = QLabel(APP_NAME)
- buzz_label.setAlignment(
- Qt.AlignmentFlag(
- Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignHCenter
- )
- )
- buzz_label_font = QtGui.QFont()
- buzz_label_font.setBold(True)
- buzz_label_font.setPointSize(20)
- buzz_label.setFont(buzz_label_font)
-
- version_label = QLabel(f"{_('Version')} {VERSION}")
- version_label.setAlignment(
- Qt.AlignmentFlag(
- Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignHCenter
- )
- )
-
- self.check_updates_button = QPushButton(_("Check for updates"), self)
- self.check_updates_button.clicked.connect(self.on_click_check_for_updates)
-
- self.show_logs_button = QPushButton(_("Show logs"), self)
- self.show_logs_button.clicked.connect(self.on_click_show_logs)
-
- button_box = QDialogButtonBox(
- QDialogButtonBox.StandardButton(QDialogButtonBox.StandardButton.Close), self
- )
- button_box.accepted.connect(self.accept)
- button_box.rejected.connect(self.reject)
-
- layout.addWidget(image_label)
- layout.addWidget(buzz_label)
- layout.addWidget(version_label)
- layout.addWidget(self.check_updates_button)
- layout.addWidget(self.show_logs_button)
- layout.addWidget(button_box)
-
- self.setLayout(layout)
- self.setMinimumWidth(350)
-
- def on_click_check_for_updates(self):
- url = QUrl(self.GITHUB_API_LATEST_RELEASE_URL)
- self.network_access_manager.get(QNetworkRequest(url))
- self.check_updates_button.setDisabled(True)
-
- def on_click_show_logs(self):
- log_dir = user_log_dir(appname="Buzz")
- QDesktopServices.openUrl(QUrl.fromLocalFile(log_dir))
-
- def on_latest_release_reply(self, reply: QNetworkReply):
- if reply.error() == QNetworkReply.NetworkError.NoError:
- response = json.loads(reply.readAll().data())
- tag_name = response.get("name")
- if self.is_version_lower(VERSION, tag_name[1:]):
- QDesktopServices.openUrl(QUrl(self.GITHUB_LATEST_RELEASE_URL))
- else:
- QMessageBox.information(self, "", _("You're up to date!"))
- self.check_updates_button.setEnabled(True)
-
- @staticmethod
- def is_version_lower(version_a: str, version_b: str):
- return version_a.replace(".", "") < version_b.replace(".", "")
diff --git a/buzz/widgets/application.py b/buzz/widgets/application.py
deleted file mode 100755
index 8fad584b..00000000
--- a/buzz/widgets/application.py
+++ /dev/null
@@ -1,106 +0,0 @@
-import logging
-import os
-import sys
-import locale
-import platform
-import darkdetect
-
-from posthog import Posthog
-
-from PyQt6.QtCore import Qt
-from PyQt6.QtGui import QFont
-from PyQt6.QtWidgets import QApplication, QStyleFactory
-
-from buzz.__version__ import VERSION
-from buzz.db.dao.transcription_dao import TranscriptionDAO
-from buzz.db.dao.transcription_segment_dao import TranscriptionSegmentDAO
-from buzz.db.db import setup_app_db
-from buzz.db.service.transcription_service import TranscriptionService
-from buzz.settings.settings import APP_NAME, Settings
-
-from buzz.transcriber.transcriber import FileTranscriptionTask
-from buzz.widgets.main_window import MainWindow
-
-
-class Application(QApplication):
- window: MainWindow
-
- def __init__(self, argv: list) -> None:
- super().__init__(argv)
-
- self.setApplicationName(APP_NAME)
- self.setApplicationVersion(VERSION)
- self.hide_main_window = False
-
- if darkdetect.isDark():
- self.styleHints().setColorScheme(Qt.ColorScheme.Dark)
- self.setStyleSheet("QCheckBox::indicator:unchecked { border: 1px solid white; }")
-
- if sys.platform.startswith("win"):
- self.setStyle(QStyleFactory.create("Fusion"))
-
- self.settings = Settings()
- logging.debug(f"Settings filename: {self.settings.settings.fileName()}")
-
- # Set BUZZ_FORCE_CPU environment variable if Force CPU setting is enabled
- force_cpu_enabled = self.settings.value(
- key=Settings.Key.FORCE_CPU, default_value=False
- )
- if force_cpu_enabled:
- os.environ["BUZZ_FORCE_CPU"] = "true"
-
- # Set BUZZ_REDUCE_GPU_MEMORY environment variable if Reduce GPU RAM setting is enabled
- reduce_gpu_memory_enabled = self.settings.value(
- key=Settings.Key.REDUCE_GPU_MEMORY, default_value=False
- )
- if reduce_gpu_memory_enabled:
- os.environ["BUZZ_REDUCE_GPU_MEMORY"] = "true"
-
- font_size = self.settings.value(
- key=Settings.Key.FONT_SIZE, default_value=self.font().pointSize()
- )
-
- if sys.platform == "darwin":
- self.setFont(QFont("SF Pro", font_size))
- else:
- self.setFont(QFont(self.font().family(), font_size))
-
- self.db = setup_app_db()
- transcription_service = TranscriptionService(
- TranscriptionDAO(self.db), TranscriptionSegmentDAO(self.db)
- )
-
- self.window = MainWindow(transcription_service)
-
- disable_telemetry = os.getenv("BUZZ_DISABLE_TELEMETRY", None)
-
- if not disable_telemetry:
- posthog = Posthog(project_api_key='phc_NqZQUw8NcxfSXsbtk5eCFylmCQpp4FuNnd6ocPAzg2f',
- host='https://us.i.posthog.com')
- posthog.capture(distinct_id=self.settings.get_user_identifier(), event="app_launched", properties={
- "app": VERSION,
- "locale": locale.getlocale(),
- "system": platform.system(),
- "release": platform.release(),
- "machine": platform.machine(),
- "version": platform.version(),
- })
-
- logging.debug(f"Launching Buzz: {VERSION}, "
- f"locale: {locale.getlocale()}, "
- f"system: {platform.system()}, "
- f"release: {platform.release()}, "
- f"machine: {platform.machine()}, "
- f"version: {platform.version()}, ")
-
- def show_main_window(self):
- if not self.hide_main_window:
- self.window.show()
-
- def add_task(self, task: FileTranscriptionTask, quit_on_complete: bool = False):
- self.window.quit_on_complete = quit_on_complete
- self.window.add_task(task)
-
- def close_database(self):
- from buzz.db.db import close_app_db
- close_app_db()
diff --git a/buzz/widgets/audio_devices_combo_box.py b/buzz/widgets/audio_devices_combo_box.py
deleted file mode 100644
index d796ce2f..00000000
--- a/buzz/widgets/audio_devices_combo_box.py
+++ /dev/null
@@ -1,55 +0,0 @@
-from typing import List, Tuple, Optional
-
-import sounddevice
-from PyQt6.QtCore import pyqtSignal
-from PyQt6.QtWidgets import QComboBox, QWidget, QMessageBox
-
-
-class AudioDevicesComboBox(QComboBox):
- """AudioDevicesComboBox displays a list of available audio input devices"""
-
- device_changed = pyqtSignal(int)
- audio_devices: List[Tuple[int, str]]
-
- def __init__(self, parent: Optional[QWidget] = None) -> None:
- super().__init__(parent)
- self.audio_devices = self.get_audio_devices()
- self.addItems([device[1] for device in self.audio_devices])
- self.currentIndexChanged.connect(self.on_index_changed)
-
- default_device_id = self.get_default_device_id()
- if default_device_id != -1:
- for i, device in enumerate(self.audio_devices):
- if device[0] == default_device_id:
- self.setCurrentIndex(i)
-
- def get_audio_devices(self) -> List[Tuple[int, str]]:
- try:
- devices: sounddevice.DeviceList = sounddevice.query_devices()
- return [
- (device.get("index"), device.get("name"))
- for device in devices
- if device.get("max_input_channels") > 0
- ]
- except UnicodeDecodeError:
- QMessageBox.critical(
- self,
- "",
- "An error occurred while loading your audio devices. Please check the application logs for more "
- "information.",
- )
- return []
-
- def on_index_changed(self, index: int):
- self.device_changed.emit(self.audio_devices[index][0])
-
- def get_default_device_id(self) -> Optional[int]:
- default_system_device = sounddevice.default.device[0]
- if default_system_device != -1:
- return default_system_device
-
- audio_devices = self.get_audio_devices()
- if len(audio_devices) > 0:
- return audio_devices[0][0]
-
- return -1
diff --git a/buzz/widgets/audio_meter_widget.py b/buzz/widgets/audio_meter_widget.py
deleted file mode 100644
index e9664740..00000000
--- a/buzz/widgets/audio_meter_widget.py
+++ /dev/null
@@ -1,102 +0,0 @@
-from typing import Optional
-
-from PyQt6 import QtGui
-from PyQt6.QtCore import Qt, QRect
-from PyQt6.QtGui import QColor, QPainter
-from PyQt6.QtWidgets import QWidget
-
-from buzz.locale import _
-
-
-class AudioMeterWidget(QWidget):
- current_amplitude: float
- BAR_WIDTH = 2
- BAR_MARGIN = 1
- BAR_INACTIVE_COLOR: QColor
- BAR_ACTIVE_COLOR: QColor
-
- # Factor by which the amplitude is scaled to make the changes more visible
- DIFF_MULTIPLIER_FACTOR = 10
- SMOOTHING_FACTOR = 0.95
-
- def __init__(self, parent: Optional[QWidget] = None):
- super().__init__(parent)
- self.setMinimumWidth(10)
- self.setFixedHeight(56)
-
- self.BARS_HEIGHT = 28
- # Extra padding to fix layout
- self.PADDING_TOP = 14
-
- self.current_amplitude = 0.0
-
- self.average_amplitude = 0.0
- self.queue_size = 0
-
- self.MINIMUM_AMPLITUDE = 0.00005 # minimum amplitude to show the first bar
- self.AMPLITUDE_SCALE_FACTOR = 10 # scale the amplitudes such that 1/AMPLITUDE_SCALE_FACTOR will show all bars
-
- if self.palette().window().color().black() > 127:
- self.BAR_INACTIVE_COLOR = QColor("#555")
- self.BAR_ACTIVE_COLOR = QColor("#999")
- else:
- self.BAR_INACTIVE_COLOR = QColor("#BBB")
- self.BAR_ACTIVE_COLOR = QColor("#555")
-
- def paintEvent(self, event: QtGui.QPaintEvent) -> None:
- painter = QPainter(self)
- painter.setPen(Qt.PenStyle.NoPen)
-
- rect = self.rect()
- center_x = rect.center().x()
- num_bars_in_half = int((rect.width() / 2) / (self.BAR_MARGIN + self.BAR_WIDTH))
- for i in range(num_bars_in_half):
- is_bar_active = (
- (self.current_amplitude - self.MINIMUM_AMPLITUDE)
- * self.AMPLITUDE_SCALE_FACTOR
- ) > (i / num_bars_in_half)
- painter.setBrush(
- self.BAR_ACTIVE_COLOR if is_bar_active else self.BAR_INACTIVE_COLOR
- )
-
- # draw to left
- painter.drawRect(
- center_x - ((i + 1) * (self.BAR_MARGIN + self.BAR_WIDTH)),
- rect.top() + self.PADDING_TOP,
- self.BAR_WIDTH,
- self.BARS_HEIGHT - self.PADDING_TOP,
- )
- # draw to right
- painter.drawRect(
- center_x + (self.BAR_MARGIN + (i * (self.BAR_MARGIN + self.BAR_WIDTH))),
- rect.top() + self.PADDING_TOP,
- self.BAR_WIDTH,
- self.BARS_HEIGHT - self.PADDING_TOP,
- )
-
- text_rect = QRect(rect.left(), self.BARS_HEIGHT, rect.width(), rect.height() - self.BARS_HEIGHT)
- painter.setPen(self.BAR_ACTIVE_COLOR)
- average_volume_label = _("Average volume")
- queue_label = _("Queue")
- painter.drawText(text_rect, Qt.AlignmentFlag.AlignCenter,
- f"{average_volume_label}: {self.average_amplitude:.4f} {queue_label}: {self.queue_size}")
-
- def reset_amplitude(self):
- self.current_amplitude = 0.0
- self.average_amplitude = 0.0
- self.queue_size = 0
- self.repaint()
-
- def update_amplitude(self, amplitude: float):
- self.current_amplitude = max(
- amplitude, self.current_amplitude * self.SMOOTHING_FACTOR
- )
- self.update()
-
- def update_average_amplitude(self, amplitude: float):
- self.average_amplitude = amplitude
- self.update()
-
- def update_queue_size(self, size: int):
- self.queue_size = size
- self.update()
diff --git a/buzz/widgets/audio_player.py b/buzz/widgets/audio_player.py
deleted file mode 100644
index bb8d15ae..00000000
--- a/buzz/widgets/audio_player.py
+++ /dev/null
@@ -1,229 +0,0 @@
-import logging
-from typing import Tuple, Optional
-
-from PyQt6 import QtGui
-from PyQt6.QtCore import QTime, QUrl, Qt, pyqtSignal
-from PyQt6.QtMultimedia import QAudioOutput, QMediaPlayer, QMediaDevices
-from PyQt6.QtWidgets import QWidget, QSlider, QPushButton, QLabel, QHBoxLayout, QVBoxLayout
-
-from buzz.widgets.icon import PlayIcon, PauseIcon
-from buzz.settings.settings import Settings
-from buzz.transcriber.file_transcriber import is_video_file
-
-
-class AudioPlayer(QWidget):
- position_ms_changed = pyqtSignal(int)
-
- def __init__(self, file_path: str):
- super().__init__()
-
- self.range_ms: Optional[Tuple[int, int]] = None
- self.position_ms = 0
- self.duration_ms = 0
- self.invalid_media = None
- self.is_looping = False # Flag to prevent recursive position changes
- self.is_slider_dragging = False # Flag to track if use is dragging slider
-
- # Initialize settings
- self.settings = Settings()
-
- self.is_video = is_video_file(file_path)
-
- self.audio_output = QAudioOutput()
- self.audio_output.setVolume(100)
-
- # Log audio device info for debugging
- default_device = QMediaDevices.defaultAudioOutput()
- if default_device.isNull():
- logging.warning("No default audio output device found!")
- else:
- logging.info(f"Audio output device: {default_device.description()}")
-
- audio_outputs = QMediaDevices.audioOutputs()
- logging.info(f"Available audio outputs: {[d.description() for d in audio_outputs]}")
-
- self.media_player = QMediaPlayer()
- self.media_player.setSource(QUrl.fromLocalFile(file_path))
- self.media_player.setAudioOutput(self.audio_output)
-
- if self.is_video:
- from PyQt6.QtMultimediaWidgets import QVideoWidget
- self.video_widget = QVideoWidget(self)
- self.media_player.setVideoOutput(self.video_widget)
- else:
- self.video_widget = None
-
- # Speed control moved to transcription viewer - just set default rate
- saved_rate = self.settings.value(Settings.Key.AUDIO_PLAYBACK_RATE, 1.0, float)
- saved_rate = max(0.1, min(5.0, saved_rate)) # Ensure valid range
- self.media_player.setPlaybackRate(saved_rate)
-
- self.scrubber = QSlider(Qt.Orientation.Horizontal)
- self.scrubber.setRange(0, 0)
- self.scrubber.sliderMoved.connect(self.on_slider_moved)
- self.scrubber.sliderPressed.connect(self.on_slider_pressed)
- self.scrubber.sliderReleased.connect(self.on_slider_released)
-
- # Track if user is dragging the slider
- self.is_slider_dragging = False
-
- self.play_icon = PlayIcon(self)
- self.pause_icon = PauseIcon(self)
-
- self.play_button = QPushButton("")
- self.play_button.setIcon(self.play_icon)
- self.play_button.clicked.connect(self.toggle_play)
- self.play_button.setMaximumWidth(40) # Match other button widths
- self.play_button.setMinimumHeight(30) # Match other button heights
-
- self.time_label = QLabel()
- self.time_label.setAlignment(Qt.AlignmentFlag.AlignRight)
-
- # Create main layout - simplified without speed controls
- if self.is_video:
- #Vertical layout for video
- main_layout = QVBoxLayout()
- main_layout.addWidget(self.video_widget, stretch=1) # As video takes more space
-
- controls_layout = QHBoxLayout()
- controls_layout.addWidget(self.play_button, alignment=Qt.AlignmentFlag.AlignVCenter)
- controls_layout.addWidget(self.scrubber, alignment=Qt.AlignmentFlag.AlignVCenter)
- controls_layout.addWidget(self.time_label, alignment=Qt.AlignmentFlag.AlignVCenter)
-
- main_layout.addLayout(controls_layout)
- else:
- # Horizontal layout for audio only
- main_layout = QHBoxLayout()
- main_layout.addWidget(self.play_button, alignment=Qt.AlignmentFlag.AlignVCenter)
- main_layout.addWidget(self.scrubber, alignment=Qt.AlignmentFlag.AlignVCenter)
- main_layout.addWidget(self.time_label, alignment=Qt.AlignmentFlag.AlignVCenter)
-
- self.setLayout(main_layout)
-
- # Connect media player signals to the corresponding slots
- self.media_player.durationChanged.connect(self.on_duration_changed)
- self.media_player.positionChanged.connect(self.on_position_changed)
- self.media_player.playbackStateChanged.connect(self.on_playback_state_changed)
- self.media_player.mediaStatusChanged.connect(self.on_media_status_changed)
- self.media_player.errorOccurred.connect(self.on_error_occurred)
-
- self.on_duration_changed(self.media_player.duration())
-
- def on_duration_changed(self, duration_ms: int):
- self.scrubber.setRange(0, duration_ms)
- self.duration_ms = duration_ms
- self.update_time_label()
-
- def on_position_changed(self, position_ms: int):
- # Don't update slider if user is currently dragging it
- if not self.is_slider_dragging:
- self.scrubber.blockSignals(True)
- self.scrubber.setValue(position_ms)
- self.scrubber.blockSignals(False)
-
- self.position_ms = position_ms
- self.position_ms_changed.emit(self.position_ms)
- self.update_time_label()
-
- # If a range has been selected as we've reached the end of the range,
- # loop back to the start of the range
- if self.range_ms is not None and not self.is_looping:
- start_range_ms, end_range_ms = self.range_ms
- # Check if we're at or past the end of the range (with small buffer for precision)
- if position_ms >= (end_range_ms - 50): # Within 50ms of end
- logging.debug(f"🔄 LOOP: Reached end {end_range_ms}ms, jumping to start {start_range_ms}ms")
- self.is_looping = True # Set flag to prevent recursion
- self.set_position(start_range_ms)
- # Reset flag immediately after setting position
- self.is_looping = False
-
- def on_playback_state_changed(self, state: QMediaPlayer.PlaybackState):
- if state == QMediaPlayer.PlaybackState.PlayingState:
- self.play_button.setIcon(self.pause_icon)
- else:
- self.play_button.setIcon(self.play_icon)
-
- def on_media_status_changed(self, status: QMediaPlayer.MediaStatus):
- logging.debug(f"Media status changed: {status}")
- match status:
- case QMediaPlayer.MediaStatus.InvalidMedia:
- self.set_invalid_media(True)
- case QMediaPlayer.MediaStatus.LoadedMedia:
- self.set_invalid_media(False)
-
- def on_error_occurred(self, error: QMediaPlayer.Error, error_string: str):
- logging.error(f"Media player error: {error} - {error_string}")
-
- def set_invalid_media(self, invalid_media: bool):
- self.invalid_media = invalid_media
- if self.invalid_media:
- self.play_button.setDisabled(True)
- self.scrubber.setRange(0, 1)
- self.scrubber.setDisabled(True)
- self.time_label.setDisabled(True)
- else:
- self.play_button.setEnabled(True)
- self.scrubber.setEnabled(True)
- self.time_label.setEnabled(True)
-
- def toggle_play(self):
- if self.media_player.playbackState() == QMediaPlayer.PlaybackState.PlayingState:
- self.media_player.pause()
- else:
- self.media_player.play()
-
- def set_range(self, range_ms: Tuple[int, int]):
- """Set a loop range. Only jump to start if current position is outside the range."""
- self.range_ms = range_ms
- start_range_ms, end_range_ms = range_ms
-
- # Only jump to start if current position is outside the range
- if self.position_ms < start_range_ms or self.position_ms > end_range_ms:
- logging.debug(f"🔄 LOOP: Position {self.position_ms}ms outside range, jumping to {start_range_ms}ms")
- self.set_position(start_range_ms)
-
- def clear_range(self):
- """Clear the current loop range"""
- self.range_ms = None
-
- def _reset_looping_flag(self):
- """Reset the looping flag"""
- self.is_looping = False
-
- def on_slider_moved(self, position_ms: int):
- self.set_position(position_ms)
- # Only clear range if scrubbed significantly outside the current range
- if self.range_ms is not None:
- start_range_ms, end_range_ms = self.range_ms
- # Clear range if scrubbed more than 2 seconds outside the range
- if position_ms < (start_range_ms - 2000) or position_ms > (end_range_ms + 2000):
- self.range_ms = None
-
- def on_slider_pressed(self):
- """Called when the user starts dragging the slider"""
- self.is_slider_dragging = True
-
- def on_slider_released(self):
- """Called when user releases the slider"""
- self.is_slider_dragging = False
- # Update the position where user released
- self.set_position(self.scrubber.value())
-
- def set_position(self, position_ms: int):
- self.media_player.setPosition(position_ms)
-
- def update_time_label(self):
- position_time = QTime(0, 0).addMSecs(self.position_ms).toString()
- duration_time = QTime(0, 0).addMSecs(self.duration_ms).toString()
- self.time_label.setText(f"{position_time} / {duration_time}")
-
- def stop(self):
- self.media_player.stop()
-
- def closeEvent(self, a0: QtGui.QCloseEvent) -> None:
- self.stop()
- super().closeEvent(a0)
-
- def hideEvent(self, a0: QtGui.QHideEvent) -> None:
- self.stop()
- super().hideEvent(a0)
diff --git a/buzz/widgets/form_label.py b/buzz/widgets/form_label.py
deleted file mode 100644
index f05b51ce..00000000
--- a/buzz/widgets/form_label.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from typing import Optional
-
-from PyQt6.QtCore import Qt
-from PyQt6.QtWidgets import QLabel, QWidget
-
-
-class FormLabel(QLabel):
- def __init__(self, name: str, parent: Optional[QWidget], *args) -> None:
- super().__init__(name, parent, *args)
- self.setStyleSheet("QLabel { text-align: right; }")
- self.setAlignment(
- Qt.AlignmentFlag(
- Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignRight
- )
- )
diff --git a/buzz/widgets/icon.py b/buzz/widgets/icon.py
deleted file mode 100644
index 12718725..00000000
--- a/buzz/widgets/icon.py
+++ /dev/null
@@ -1,132 +0,0 @@
-from PyQt6.QtGui import QIcon, QPixmap, QPainter, QColor
-from PyQt6.QtWidgets import QWidget
-
-from buzz.assets import get_path
-
-
-class Icon(QIcon):
- LIGHT_THEME_COLOR = "#555"
- DARK_THEME_COLOR = "#EEE"
-
- def __init__(self, path: str, parent: QWidget):
- super().__init__()
- self.path = path
- self.parent = parent
-
- self.color = self.get_color()
- normal_pixmap = self.create_default_pixmap(self.path, self.color)
- disabled_pixmap = self.create_disabled_pixmap(normal_pixmap, self.color)
- self.addPixmap(normal_pixmap, QIcon.Mode.Normal)
- self.addPixmap(disabled_pixmap, QIcon.Mode.Disabled)
-
- # https://stackoverflow.com/questions/15123544/change-the-color-of-an-svg-in-qt
- def create_default_pixmap(self, path, color):
- pixmap = QPixmap(path)
- painter = QPainter(pixmap)
- painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceIn)
- painter.fillRect(pixmap.rect(), color)
- painter.end()
- return pixmap
-
- def create_disabled_pixmap(self, pixmap, color):
- disabled_pixmap = QPixmap(pixmap.size())
- disabled_pixmap.fill(QColor(0, 0, 0, 0))
-
- painter = QPainter(disabled_pixmap)
- painter.setOpacity(0.4)
- painter.drawPixmap(0, 0, pixmap)
- painter.setCompositionMode(
- QPainter.CompositionMode.CompositionMode_DestinationIn
- )
- painter.fillRect(disabled_pixmap.rect(), color)
- painter.end()
- return disabled_pixmap
-
- def get_color(self) -> QColor:
- is_dark_theme = self.parent.palette().window().color().black() > 127
- return QColor(
- self.DARK_THEME_COLOR if is_dark_theme else self.LIGHT_THEME_COLOR
- )
-
-
-class PlayIcon(Icon):
- def __init__(self, parent: QWidget):
- super().__init__(get_path("assets/play_arrow_black_24dp.svg"), parent)
-
-
-class PauseIcon(Icon):
- def __init__(self, parent: QWidget):
- super().__init__(get_path("assets/pause_black_24dp.svg"), parent)
-
-
-class UndoIcon(Icon):
- def __init__(self, parent: QWidget):
- super().__init__(get_path("assets/undo_FILL0_wght700_GRAD0_opsz48.svg"), parent)
-
-
-class RedoIcon(Icon):
- def __init__(self, parent: QWidget):
- super().__init__(get_path("assets/redo_FILL0_wght700_GRAD0_opsz48.svg"), parent)
-
-
-class FileDownloadIcon(Icon):
- def __init__(self, parent: QWidget):
- super().__init__(get_path("assets/file_download_black_24dp.svg"), parent)
-
-
-class TranslateIcon(Icon):
- def __init__(self, parent: QWidget):
- super().__init__(get_path("assets/translate_black.svg"), parent)
-
-class ResizeIcon(Icon):
- def __init__(self, parent: QWidget):
- super().__init__(get_path("assets/resize_black.svg"), parent)
-
-class SpeakerIdentificationIcon(Icon):
- def __init__(self, parent: QWidget):
- super().__init__(get_path("assets/speaker-identification.svg"), parent)
-
-class VisibilityIcon(Icon):
- def __init__(self, parent: QWidget):
- super().__init__(
- get_path("assets/visibility_FILL0_wght700_GRAD0_opsz48.svg"), parent
- )
-
-
-class ScrollToCurrentIcon(Icon):
- def __init__(self, parent: QWidget):
- super().__init__(
- get_path("assets/visibility_FILL0_wght700_GRAD0_opsz48.svg"), parent
- )
-
-class NewWindowIcon(Icon):
- def __init__(self, parent: QWidget):
- super().__init__(get_path("assets/icons/new-window.svg"), parent)
-
-
-class FullscreenIcon(Icon):
- def __init__(self, parent: QWidget):
- super().__init__(get_path("assets/icons/fullscreen.svg"), parent)
-
-
-class ColorBackgroundIcon(Icon):
- def __init__(self, parent: QWidget):
- super().__init__(get_path("assets/icons/color-background.svg"), parent)
-
-
-class TextColorIcon(Icon):
- def __init__(self, parent: QWidget):
- super().__init__(get_path("assets/icons/gui-text-color.svg"), parent)
-
-
-BUZZ_ICON_PATH = get_path("assets/buzz.ico")
-BUZZ_LARGE_ICON_PATH = get_path("assets/buzz-icon-1024.png")
-
-INFO_ICON_PATH = get_path("assets/info-circle.svg")
-RECORD_ICON_PATH = get_path("assets/mic_FILL0_wght700_GRAD0_opsz48.svg")
-EXPAND_ICON_PATH = get_path("assets/open_in_full_FILL0_wght700_GRAD0_opsz48.svg")
-ADD_ICON_PATH = get_path("assets/add_FILL0_wght700_GRAD0_opsz48.svg")
-URL_ICON_PATH = get_path("assets/url.svg")
-TRASH_ICON_PATH = get_path("assets/delete_FILL0_wght700_GRAD0_opsz48.svg")
-CANCEL_ICON_PATH = get_path("assets/cancel_FILL0_wght700_GRAD0_opsz48.svg")
-UPDATE_ICON_PATH = get_path("assets/update_FILL0_wght700_GRAD0_opsz48.svg")
\ No newline at end of file
diff --git a/buzz/widgets/icon_presentation.py b/buzz/widgets/icon_presentation.py
deleted file mode 100644
index 6f230971..00000000
--- a/buzz/widgets/icon_presentation.py
+++ /dev/null
@@ -1,60 +0,0 @@
-from PyQt6.QtGui import QIcon, QPixmap, QPainter, QPalette
-from PyQt6.QtCore import QSize
-from PyQt6.QtSvg import QSvgRenderer
-import os
-from buzz.assets import APP_BASE_DIR
-
-class PresentationIcon:
- "Icons for presentation window controls"
- def __init__(self, parent, svg_path: str, color: str = None):
- self.parent = parent
- self.svg_path = svg_path
- self.color = color or self.get_default_color()
-
-
- def get_default_color(self) -> str:
- """Get default icon color based on theme"""
- palette = self.parent.palette()
- is_dark = palette.window().color().black() > 127
-
- return "#EEE" if is_dark else "#555"
-
- def get_icon(self) -> QIcon:
- """Load SVG icon and return as QIcon"""
- #Load from asset first
- full_path = os.path.join(APP_BASE_DIR, "assets", "icons", os.path.basename(self.svg_path))
-
- if not os.path.exists(full_path):
- pixmap = QPixmap(24, 24)
- pixmap.fill(self.color)
-
- return QIcon(pixmap)
-
- #Load SVG
- renderer = QSvgRenderer(full_path)
- pixmap = QPixmap(24, 24)
- pixmap.fill(Qt.GlobalColor.transparent)
- painter = QPainter(pixmap)
- renderer.render(painter)
- painter.end()
-
- return QIcon(pixmap)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/buzz/widgets/import_url_dialog.py b/buzz/widgets/import_url_dialog.py
deleted file mode 100644
index 7f19269c..00000000
--- a/buzz/widgets/import_url_dialog.py
+++ /dev/null
@@ -1,53 +0,0 @@
-from typing import Optional
-
-from PyQt6.QtCore import Qt, QRegularExpression
-from PyQt6.QtWidgets import QDialog, QWidget, QDialogButtonBox, QMessageBox, QFormLayout
-
-from buzz.locale import _
-from buzz.widgets.line_edit import LineEdit
-
-
-class ImportURLDialog(QDialog):
- url: Optional[str] = None
- url_regex = QRegularExpression(
- "^((http|https)://)[-a-zA-Z0-9@:%._\\+~#?&//=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%._\\+~#?&//=]*)$"
- )
-
- def __init__(self, parent: Optional[QWidget] = None):
- super().__init__(parent=parent, flags=Qt.WindowType.Window)
-
- self.setWindowTitle(_("Import URL"))
-
- self.line_edit = LineEdit()
- self.line_edit.setPlaceholderText(_("https://example.com/audio.mp3"))
- self.line_edit.setMinimumWidth(350)
-
- self.button_box = QDialogButtonBox(
- QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
- )
- self.button_box.button(QDialogButtonBox.StandardButton.Ok).setText(_("Ok"))
- self.button_box.button(QDialogButtonBox.StandardButton.Cancel).setText(_("Cancel"))
- self.button_box.accepted.connect(self.accept)
- self.button_box.rejected.connect(self.reject)
-
- self.layout = QFormLayout()
- self.layout.addRow(_("URL:"), self.line_edit)
- self.layout.addWidget(self.button_box)
- self.setLayout(self.layout)
-
- def accept(self):
- if self.url_regex.match(self.line_edit.text()).hasMatch():
- self.url = self.line_edit.text()
- super().accept()
- else:
- QMessageBox.critical(
- self, _("Invalid URL"), _("The URL you entered is invalid.")
- )
-
- @classmethod
- def prompt(cls, parent: Optional[QWidget] = None) -> Optional[str]:
- dialog = cls(parent=parent)
- if dialog.exec() == QDialog.DialogCode.Accepted:
- return dialog.url
- else:
- return None
diff --git a/buzz/widgets/line_edit.py b/buzz/widgets/line_edit.py
deleted file mode 100644
index ba8985ab..00000000
--- a/buzz/widgets/line_edit.py
+++ /dev/null
@@ -1,12 +0,0 @@
-import platform
-from typing import Optional
-
-from PyQt6.QtWidgets import QLineEdit, QWidget, QSizePolicy
-
-
-class LineEdit(QLineEdit):
- def __init__(self, default_text: str = "", parent: Optional[QWidget] = None):
- super().__init__(default_text, parent)
- if platform.system() == "Darwin":
- self.setStyleSheet("QLineEdit { padding: 4px }")
- self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
diff --git a/buzz/widgets/main_window.py b/buzz/widgets/main_window.py
deleted file mode 100644
index f877321a..00000000
--- a/buzz/widgets/main_window.py
+++ /dev/null
@@ -1,528 +0,0 @@
-import os
-import logging
-from typing import Tuple, List, Optional
-from uuid import UUID
-
-from PyQt6 import QtGui
-from PyQt6.QtCore import (
- Qt,
- QThread,
- QModelIndex,
- pyqtSignal
-)
-
-from PyQt6.QtGui import QIcon
-from PyQt6.QtWidgets import (
- QApplication,
- QMainWindow,
- QMessageBox,
- QFileDialog,
-)
-
-from buzz.db.entity.transcription import Transcription
-from buzz.db.service.transcription_service import TranscriptionService
-from buzz.file_transcriber_queue_worker import FileTranscriberQueueWorker
-from buzz.locale import _
-from buzz.settings.settings import APP_NAME, Settings
-from buzz.update_checker import UpdateChecker, UpdateInfo
-from buzz.widgets.update_dialog import UpdateDialog
-from buzz.settings.shortcuts import Shortcuts
-from buzz.store.keyring_store import set_password, Key
-from buzz.transcriber.transcriber import (
- FileTranscriptionTask,
- TranscriptionOptions,
- FileTranscriptionOptions,
- SUPPORTED_AUDIO_FORMATS,
- Segment,
-)
-from buzz.widgets.icon import BUZZ_ICON_PATH
-from buzz.widgets.import_url_dialog import ImportURLDialog
-from buzz.widgets.main_window_toolbar import MainWindowToolbar
-from buzz.widgets.menu_bar import MenuBar
-from buzz.widgets.preferences_dialog.models.preferences import Preferences
-from buzz.widgets.transcriber.file_transcriber_widget import FileTranscriberWidget
-from buzz.widgets.transcription_task_folder_watcher import (
- TranscriptionTaskFolderWatcher,
- SUPPORTED_EXTENSIONS,
-)
-from buzz.widgets.transcription_tasks_table_widget import (
- TranscriptionTasksTableWidget,
-)
-from buzz.widgets.transcription_viewer.transcription_viewer_widget import (
- TranscriptionViewerWidget,
-)
-
-
-class MainWindow(QMainWindow):
- table_widget: TranscriptionTasksTableWidget
- transcriptions_updated = pyqtSignal(UUID)
-
- def __init__(self, transcription_service: TranscriptionService):
- super().__init__(flags=Qt.WindowType.Window)
-
- self.setWindowTitle(APP_NAME)
- self.setWindowIcon(QIcon(BUZZ_ICON_PATH))
-
- self.setAcceptDrops(True)
-
- self.settings = Settings()
-
- self.shortcuts = Shortcuts(settings=self.settings)
-
- self.quit_on_complete = False
- self.transcription_service = transcription_service
-
- #update checker
- self._update_info: Optional[UpdateInfo] = None
-
- self.toolbar = MainWindowToolbar(shortcuts=self.shortcuts, parent=self)
- self.toolbar.new_transcription_action_triggered.connect(
- self.on_new_transcription_action_triggered
- )
- self.toolbar.new_url_transcription_action_triggered.connect(
- self.on_new_url_transcription_action_triggered
- )
- self.toolbar.open_transcript_action_triggered.connect(
- self.open_transcript_viewer
- )
- self.toolbar.clear_history_action_triggered.connect(
- self.on_clear_history_action_triggered
- )
- self.toolbar.stop_transcription_action_triggered.connect(
- self.on_stop_transcription_action_triggered
- )
- self.addToolBar(self.toolbar)
- self.toolbar.update_action_triggered.connect(self.on_update_action_triggered)
- self.setUnifiedTitleAndToolBarOnMac(True)
-
- self.preferences = self.load_preferences(settings=self.settings)
- self.menu_bar = MenuBar(
- shortcuts=self.shortcuts,
- preferences=self.preferences,
- parent=self,
- )
- self.menu_bar.import_action_triggered.connect(
- self.on_new_transcription_action_triggered
- )
- self.menu_bar.import_url_action_triggered.connect(
- self.on_new_url_transcription_action_triggered
- )
- self.menu_bar.import_folder_action_triggered.connect(
- self.on_import_folder_action_triggered
- )
- self.menu_bar.shortcuts_changed.connect(self.on_shortcuts_changed)
- self.menu_bar.openai_api_key_changed.connect(
- self.on_openai_access_token_changed
- )
- self.menu_bar.preferences_changed.connect(self.on_preferences_changed)
- self.setMenuBar(self.menu_bar)
-
- self.table_widget = TranscriptionTasksTableWidget(self)
- self.table_widget.transcription_service = self.transcription_service
- self.table_widget.doubleClicked.connect(self.on_table_double_clicked)
- self.table_widget.return_clicked.connect(self.open_transcript_viewer)
- self.table_widget.delete_requested.connect(self.on_clear_history_action_triggered)
- self.table_widget.selectionModel().selectionChanged.connect(
- self.on_table_selection_changed
- )
- self.transcriptions_updated.connect(
- self.on_transcriptions_updated
- )
-
- self.setCentralWidget(self.table_widget)
-
- # Start transcriber thread
- self.transcriber_thread = QThread()
-
- self.transcriber_worker = FileTranscriberQueueWorker()
- self.transcriber_worker.moveToThread(self.transcriber_thread)
-
- self.transcriber_worker.task_started.connect(self.on_task_started)
- self.transcriber_worker.task_progress.connect(self.on_task_progress)
- self.transcriber_worker.task_download_progress.connect(
- self.on_task_download_progress
- )
- self.transcriber_worker.task_error.connect(self.on_task_error)
- self.transcriber_worker.task_completed.connect(self.on_task_completed)
-
- self.transcriber_worker.completed.connect(self.transcriber_thread.quit)
-
- self.transcriber_thread.started.connect(self.transcriber_worker.run)
-
- self.transcriber_thread.start()
-
- self.load_geometry()
-
- self.folder_watcher = TranscriptionTaskFolderWatcher(
- tasks={},
- preferences=self.preferences.folder_watch,
- )
- self.folder_watcher.task_found.connect(self.add_task)
- self.folder_watcher.find_tasks()
-
- self.transcription_viewer_widget = None
-
- #Initialize and run update checker
- self._init_update_checker()
-
- def on_preferences_changed(self, preferences: Preferences):
- self.preferences = preferences
- self.save_preferences(preferences)
- self.folder_watcher.set_preferences(preferences.folder_watch)
- self.folder_watcher.find_tasks()
-
- def save_preferences(self, preferences: Preferences):
- self.settings.settings.beginGroup("preferences")
- preferences.save(self.settings.settings)
- self.settings.settings.endGroup()
-
- def load_preferences(self, settings: Settings):
- settings.settings.beginGroup("preferences")
- preferences = Preferences.load(settings.settings)
- settings.settings.endGroup()
- return preferences
-
- def dragEnterEvent(self, event):
- # Accept file drag events
- if event.mimeData().hasUrls():
- event.accept()
- else:
- event.ignore()
-
- def dropEvent(self, event):
- file_paths = [url.toLocalFile() for url in event.mimeData().urls()]
- self.open_file_transcriber_widget(file_paths=file_paths)
-
- def on_file_transcriber_triggered(
- self, options: Tuple[TranscriptionOptions, FileTranscriptionOptions, str]
- ):
- transcription_options, file_transcription_options, model_path = options
-
- if file_transcription_options.file_paths is not None:
- for file_path in file_transcription_options.file_paths:
- task = FileTranscriptionTask(
- transcription_options=transcription_options,
- file_transcription_options=file_transcription_options,
- model_path=model_path,
- file_path=file_path,
- source=FileTranscriptionTask.Source.FILE_IMPORT,
- )
- self.add_task(task)
- else:
- task = FileTranscriptionTask(
- transcription_options=transcription_options,
- file_transcription_options=file_transcription_options,
- model_path=model_path,
- url=file_transcription_options.url,
- source=FileTranscriptionTask.Source.URL_IMPORT,
- )
- self.add_task(task)
-
- def on_clear_history_action_triggered(self):
- selected_rows = self.table_widget.selectionModel().selectedRows()
- if len(selected_rows) == 0:
- return
-
- question_box = QMessageBox()
- question_box.setWindowTitle(_("Clear History"))
- question_box.setIcon(QMessageBox.Icon.Question)
- question_box.setText(
- _(
- "Are you sure you want to delete the selected transcription(s)? "
- "This action cannot be undone."
- ),
- )
- question_box.setStandardButtons(
- QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
- )
- question_box.button(QMessageBox.StandardButton.Yes).setText(_("Ok"))
- question_box.button(QMessageBox.StandardButton.No).setText(_("Cancel"))
-
- reply = question_box.exec()
-
- if reply == QMessageBox.StandardButton.Yes:
- self.table_widget.delete_transcriptions(selected_rows)
-
- def on_stop_transcription_action_triggered(self):
- selected_transcriptions = self.table_widget.selected_transcriptions()
- for transcription in selected_transcriptions:
- transcription_id = transcription.id_as_uuid
- self.transcriber_worker.cancel_task(transcription_id)
- self.transcription_service.update_transcription_as_canceled(
- transcription_id
- )
- self.table_widget.refresh_row(transcription_id)
- self.on_table_selection_changed()
-
- def on_new_transcription_action_triggered(self):
- (file_paths, __) = QFileDialog.getOpenFileNames(
- self, _("Select audio file"), "", SUPPORTED_AUDIO_FORMATS
- )
- if len(file_paths) == 0:
- return
-
- self.open_file_transcriber_widget(file_paths)
-
- def on_new_url_transcription_action_triggered(self):
- url = ImportURLDialog.prompt(parent=self)
- if url is not None:
- self.open_file_transcriber_widget(url=url)
-
- def on_import_folder_action_triggered(self):
- folder = QFileDialog.getExistingDirectory(self, _("Select folder"))
- if not folder:
- return
- file_paths = []
- for dirpath, _dirs, filenames in os.walk(folder):
- for filename in filenames:
- ext = os.path.splitext(filename)[1].lower()
- if ext in SUPPORTED_EXTENSIONS:
- file_paths.append(os.path.join(dirpath, filename))
- if not file_paths:
- return
- self.open_file_transcriber_widget(file_paths)
-
- def open_file_transcriber_widget(
- self, file_paths: Optional[List[str]] = None, url: Optional[str] = None
- ):
- file_transcriber_window = FileTranscriberWidget(
- file_paths=file_paths,
- url=url,
- parent=self,
- flags=Qt.WindowType.Window,
- )
- file_transcriber_window.triggered.connect(self.on_file_transcriber_triggered)
- file_transcriber_window.openai_access_token_changed.connect(
- self.on_openai_access_token_changed
- )
- file_transcriber_window.show()
- file_transcriber_window.raise_()
- file_transcriber_window.activateWindow()
-
- @staticmethod
- def on_openai_access_token_changed(access_token: str):
- try:
- set_password(Key.OPENAI_API_KEY, access_token)
- except Exception as exc:
- logging.error("Unable to write to keyring: %s", exc)
- QMessageBox.critical(
- None, _("Error"), _("Unable to save OpenAI API key to keyring")
- )
-
- def open_transcript_viewer(self):
- selected_rows = self.table_widget.selectionModel().selectedRows()
- for selected_row in selected_rows:
- transcription = self.table_widget.transcription(selected_row)
- self.open_transcription_viewer(transcription)
-
- def on_table_selection_changed(self):
- self.toolbar.set_open_transcript_action_enabled(
- self.should_enable_open_transcript_action()
- )
- self.toolbar.set_stop_transcription_action_enabled(
- self.should_enable_stop_transcription_action()
- )
- self.toolbar.set_clear_history_action_enabled(
- self.should_enable_clear_history_action()
- )
-
- def should_enable_open_transcript_action(self):
- selected_transcriptions = self.table_widget.selected_transcriptions()
- if len(selected_transcriptions) == 0:
- return False
- return all(
- MainWindow.can_open_transcript(transcription)
- for transcription in selected_transcriptions
- )
-
- @staticmethod
- def can_open_transcript(transcription: Transcription) -> bool:
- return (
- FileTranscriptionTask.Status(transcription.status)
- == FileTranscriptionTask.Status.COMPLETED
- )
-
- def should_enable_stop_transcription_action(self):
- return self.selected_tasks_have_status(
- [
- FileTranscriptionTask.Status.IN_PROGRESS,
- FileTranscriptionTask.Status.QUEUED,
- ]
- )
-
- def should_enable_clear_history_action(self):
- return self.selected_tasks_have_status(
- [
- FileTranscriptionTask.Status.COMPLETED,
- FileTranscriptionTask.Status.FAILED,
- FileTranscriptionTask.Status.CANCELED,
- ]
- )
-
- def selected_tasks_have_status(self, statuses: List[FileTranscriptionTask.Status]):
- transcriptions = self.table_widget.selected_transcriptions()
- if len(transcriptions) == 0:
- return False
-
- return all(
- [
- transcription.status_as_status in statuses
- for transcription in transcriptions
- ]
- )
-
- def on_table_double_clicked(self, index: QModelIndex):
- transcription = self.table_widget.transcription(index)
- if not MainWindow.can_open_transcript(transcription):
- return
- self.open_transcription_viewer(transcription)
-
- def open_transcription_viewer(self, transcription: Transcription):
- self.transcription_viewer_widget = TranscriptionViewerWidget(
- transcription=transcription,
- transcription_service=self.transcription_service,
- shortcuts=self.shortcuts,
- parent=self,
- flags=Qt.WindowType.Window,
- transcriptions_updated_signal=self.transcriptions_updated,
- )
- self.transcription_viewer_widget.show()
-
- def add_task(self, task: FileTranscriptionTask):
- self.transcription_service.create_transcription(task)
- self.table_widget.refresh_all()
- self.transcriber_worker.add_task(task)
-
- def on_transcriptions_updated(self):
- self.table_widget.refresh_all()
-
- def on_task_started(self, task: FileTranscriptionTask):
- self.transcription_service.update_transcription_as_started(task.uid)
- self.table_widget.refresh_row(task.uid)
-
- def on_task_progress(self, task: FileTranscriptionTask, progress: float):
- self.transcription_service.update_transcription_progress(task.uid, progress)
- self.table_widget.refresh_row(task.uid)
-
- def on_task_download_progress(
- self, task: FileTranscriptionTask, fraction_downloaded: float
- ):
- # TODO: Save download progress in the database
- pass
-
- def on_task_completed(self, task: FileTranscriptionTask, segments: List[Segment]):
- # Update file path in database only for URL imports where file is downloaded
- if task.source == FileTranscriptionTask.Source.URL_IMPORT and task.file_path:
- logging.debug(f"Updating transcription file path: {task.file_path}")
- # Use the file basename (video title) as the display name
- basename = os.path.basename(task.file_path)
- name = os.path.splitext(basename)[0] # Remove .wav extension
- self.transcription_service.update_transcription_file_and_name(task.uid, task.file_path, name)
-
- self.transcription_service.update_transcription_as_completed(task.uid, segments)
- self.table_widget.refresh_row(task.uid)
-
- if self.quit_on_complete:
- self.close()
- QApplication.quit()
-
-
- def on_task_error(self, task: FileTranscriptionTask, error: str):
- self.transcription_service.update_transcription_as_failed(task.uid, error)
- self.table_widget.refresh_row(task.uid)
-
- if self.quit_on_complete:
- self.close()
- QApplication.quit()
-
- def on_shortcuts_changed(self):
- self.menu_bar.reset_shortcuts()
- self.toolbar.reset_shortcuts()
-
- def resizeEvent(self, event):
- self.save_geometry()
-
- def closeEvent(self, event: QtGui.QCloseEvent) -> None:
- self.save_geometry()
- self.settings.settings.sync()
-
- if self.folder_watcher:
- try:
- self.folder_watcher.task_found.disconnect()
- if len(self.folder_watcher.directories()) > 0:
- self.folder_watcher.removePaths(self.folder_watcher.directories())
- except Exception as e:
- logging.warning(f"Error cleaning up folder watcher: {e}")
-
- try:
- self.transcriber_worker.task_started.disconnect()
- self.transcriber_worker.task_progress.disconnect()
- self.transcriber_worker.task_download_progress.disconnect()
- self.transcriber_worker.task_error.disconnect()
- self.transcriber_worker.task_completed.disconnect()
- except Exception as e:
- logging.warning(f"Error disconnecting signals: {e}")
-
- self.transcriber_worker.stop()
- self.transcriber_thread.quit()
-
- if self.transcriber_thread.isRunning():
- if not self.transcriber_thread.wait(10000):
- logging.warning("Transcriber thread did not finish within 10s timeout, terminating")
- self.transcriber_thread.terminate()
- if not self.transcriber_thread.wait(2000):
- logging.error("Transcriber thread could not be terminated")
-
- if self.transcription_viewer_widget is not None:
- self.transcription_viewer_widget.close()
-
- try:
- from buzz.widgets.application import Application
- app = Application.instance()
- if app and hasattr(app, 'close_database'):
- app.close_database()
- except Exception as e:
- logging.warning(f"Error closing database: {e}")
-
- logging.debug("MainWindow closeEvent completed")
-
- super().closeEvent(event)
-
- def save_geometry(self):
- self.settings.begin_group(Settings.Key.MAIN_WINDOW)
- self.settings.settings.setValue("geometry", self.saveGeometry())
- self.settings.end_group()
-
- def load_geometry(self):
- self.settings.begin_group(Settings.Key.MAIN_WINDOW)
- geometry = self.settings.settings.value("geometry")
- if geometry is not None:
- self.restoreGeometry(geometry)
- else:
- self.setBaseSize(1240, 600)
- self.resize(1240, 600)
- self.settings.end_group()
-
- def _init_update_checker(self):
- """Initializes and runs the update checker."""
- self.update_checker = UpdateChecker(settings=self.settings, parent=self)
- self.update_checker.update_available.connect(self._on_update_available)
-
- # Check for updates on startup
- self.update_checker.check_for_updates()
-
- def _on_update_available(self, update_info: UpdateInfo):
- """Called when an update is available."""
- self._update_info = update_info
- self.toolbar.set_update_available(True)
-
- def on_update_action_triggered(self):
- """Called when user clicks the update action in toolbar."""
- if self._update_info is None:
- return
-
- dialog = UpdateDialog(
- update_info=self._update_info,
- parent=self
- )
- dialog.exec()
\ No newline at end of file
diff --git a/buzz/widgets/main_window_toolbar.py b/buzz/widgets/main_window_toolbar.py
deleted file mode 100644
index fdbc8a2e..00000000
--- a/buzz/widgets/main_window_toolbar.py
+++ /dev/null
@@ -1,133 +0,0 @@
-from typing import Optional
-
-from PyQt6.QtCore import pyqtSignal, Qt
-from PyQt6.QtGui import QKeySequence
-from PyQt6.QtWidgets import QWidget
-
-from buzz.action import Action
-from buzz.locale import _
-from buzz.settings.shortcut import Shortcut
-from buzz.settings.shortcuts import Shortcuts
-from buzz.widgets.icon import Icon
-from buzz.widgets.icon import (
- RECORD_ICON_PATH,
- ADD_ICON_PATH,
- URL_ICON_PATH,
- EXPAND_ICON_PATH,
- CANCEL_ICON_PATH,
- TRASH_ICON_PATH,
- UPDATE_ICON_PATH,
-)
-from buzz.widgets.recording_transcriber_widget import RecordingTranscriberWidget
-from buzz.widgets.toolbar import ToolBar
-
-
-class MainWindowToolbar(ToolBar):
- new_transcription_action_triggered: pyqtSignal
- new_url_transcription_action_triggered: pyqtSignal
- open_transcript_action_triggered: pyqtSignal
- clear_history_action_triggered: pyqtSignal
- update_action_triggered: pyqtSignal
- ICON_LIGHT_THEME_BACKGROUND = "#555"
- ICON_DARK_THEME_BACKGROUND = "#AAA"
-
- def __init__(self, shortcuts: Shortcuts, parent: Optional[QWidget]):
- super().__init__(parent)
-
- self.shortcuts = shortcuts
-
- self.record_action = Action(Icon(RECORD_ICON_PATH, self), _("Record"), self)
- self.record_action.triggered.connect(self.on_record_action_triggered)
-
- # Note: Changes to "New File Transcription" need to be reflected
- # also in tests/widgets/main_window_test.py
- self.new_transcription_action = Action(
- Icon(ADD_ICON_PATH, self), _("New File Transcription"), self
- )
- self.new_transcription_action_triggered = (
- self.new_transcription_action.triggered
- )
-
- self.new_url_transcription_action = Action(
- Icon(URL_ICON_PATH, self), _("New URL Transcription"), self
- )
- self.new_url_transcription_action_triggered = (
- self.new_url_transcription_action.triggered
- )
-
- self.open_transcript_action = Action(
- Icon(EXPAND_ICON_PATH, self), _("Open Transcript"), self
- )
- self.open_transcript_action_triggered = self.open_transcript_action.triggered
- self.open_transcript_action.setDisabled(True)
-
- self.stop_transcription_action = Action(
- Icon(CANCEL_ICON_PATH, self), _("Cancel Transcription"), self
- )
- self.stop_transcription_action_triggered = (
- self.stop_transcription_action.triggered
- )
- self.stop_transcription_action.setDisabled(True)
-
- self.clear_history_action = Action(
- Icon(TRASH_ICON_PATH, self), _("Clear History"), self
- )
-
- self.update_action = Action(
- Icon(UPDATE_ICON_PATH, self), _("Update Available"), self
- )
- self.update_action_triggered = self.update_action.triggered
- self.update_action.setVisible(False)
-
- self.clear_history_action_triggered = self.clear_history_action.triggered
- self.clear_history_action.setDisabled(True)
-
- self.reset_shortcuts()
-
- self.addAction(self.record_action)
- self.addSeparator()
- self.addActions(
- [
- self.new_transcription_action,
- self.new_url_transcription_action,
- self.open_transcript_action,
- self.stop_transcription_action,
- self.clear_history_action,
- ]
- )
-
- self.addSeparator()
- self.addAction(self.update_action)
-
- self.setMovable(False)
- self.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
-
- def reset_shortcuts(self):
- self.record_action.setShortcut(
- QKeySequence.fromString(self.shortcuts.get(Shortcut.OPEN_RECORD_WINDOW))
- )
- self.stop_transcription_action.setShortcut(
- QKeySequence.fromString(self.shortcuts.get(Shortcut.STOP_TRANSCRIPTION))
- )
- self.clear_history_action.setShortcut(
- QKeySequence.fromString(self.shortcuts.get(Shortcut.CLEAR_HISTORY))
- )
-
- def on_record_action_triggered(self):
- recording_transcriber_window = RecordingTranscriberWidget(
- self, flags=Qt.WindowType.Window
- )
- recording_transcriber_window.show()
-
- def set_stop_transcription_action_enabled(self, enabled: bool):
- self.stop_transcription_action.setEnabled(enabled)
-
- def set_open_transcript_action_enabled(self, enabled: bool):
- self.open_transcript_action.setEnabled(enabled)
-
- def set_clear_history_action_enabled(self, enabled: bool):
- self.clear_history_action.setEnabled(enabled)
-
- def set_update_available(self, available: bool):
- """Shows or hides the update action in the toolbar."""
- self.update_action.setVisible(available)
diff --git a/buzz/widgets/menu_bar.py b/buzz/widgets/menu_bar.py
deleted file mode 100644
index f30857c0..00000000
--- a/buzz/widgets/menu_bar.py
+++ /dev/null
@@ -1,112 +0,0 @@
-import platform
-import webbrowser
-from typing import Optional
-
-from PyQt6.QtCore import pyqtSignal
-from PyQt6.QtGui import QAction, QKeySequence
-from PyQt6.QtWidgets import QMenuBar, QWidget
-
-from buzz.locale import _
-from buzz.settings.settings import APP_NAME
-from buzz.settings.shortcut import Shortcut
-from buzz.settings.shortcuts import Shortcuts
-from buzz.widgets.about_dialog import AboutDialog
-from buzz.widgets.preferences_dialog.models.preferences import Preferences
-from buzz.widgets.preferences_dialog.preferences_dialog import (
- PreferencesDialog,
-)
-
-
-class MenuBar(QMenuBar):
- import_action_triggered = pyqtSignal()
- import_url_action_triggered = pyqtSignal()
- import_folder_action_triggered = pyqtSignal()
- shortcuts_changed = pyqtSignal()
- openai_api_key_changed = pyqtSignal(str)
- preferences_changed = pyqtSignal(Preferences)
- preferences_dialog: Optional[PreferencesDialog] = None
-
- def __init__(
- self,
- shortcuts: Shortcuts,
- preferences: Preferences,
- parent: Optional[QWidget] = None,
- ):
- super().__init__(parent)
-
- self.shortcuts = shortcuts
- self.preferences = preferences
-
- self.import_action = QAction(_("Import File..."), self)
- self.import_action.triggered.connect(self.import_action_triggered)
-
- self.import_url_action = QAction(_("Import URL..."), self)
- self.import_url_action.triggered.connect(self.import_url_action_triggered)
-
- self.import_folder_action = QAction(_("Import Folder..."), self)
- self.import_folder_action.triggered.connect(self.import_folder_action_triggered)
-
- about_label = _("About")
- about_action = QAction(f'{about_label} {APP_NAME}', self)
- about_action.triggered.connect(self.on_about_action_triggered)
- about_action.setMenuRole(QAction.MenuRole.AboutRole)
-
- self.preferences_action = QAction(_("Preferences..."), self)
- self.preferences_action.triggered.connect(self.on_preferences_action_triggered)
- self.preferences_action.setMenuRole(QAction.MenuRole.PreferencesRole)
-
- help_label = _("Help")
- help_action = QAction(f'{help_label}', self)
- help_action.triggered.connect(self.on_help_action_triggered)
-
- self.reset_shortcuts()
-
- file_menu = self.addMenu(_("File"))
- file_menu.addAction(self.import_action)
- file_menu.addAction(self.import_url_action)
- file_menu.addAction(self.import_folder_action)
-
- help_menu_title = _("Help") + ("\u200B" if platform.system() == "Darwin" else "")
- help_menu = self.addMenu(help_menu_title)
- help_menu.addAction(about_action)
- help_menu.addAction(help_action)
- help_menu.addAction(self.preferences_action)
-
- def on_about_action_triggered(self):
- about_dialog = AboutDialog(parent=self)
- about_dialog.open()
-
- def on_preferences_action_triggered(self):
- preferences_dialog = PreferencesDialog(
- shortcuts=self.shortcuts,
- preferences=self.preferences,
- parent=self,
- )
- preferences_dialog.shortcuts_changed.connect(self.shortcuts_changed)
- preferences_dialog.openai_api_key_changed.connect(self.openai_api_key_changed)
- preferences_dialog.finished.connect(self.on_preferences_dialog_finished)
- preferences_dialog.open()
-
- self.preferences_dialog = preferences_dialog
-
- def on_preferences_dialog_finished(self, result):
- if result == self.preferences_dialog.DialogCode.Accepted:
- updated_preferences = self.preferences_dialog.updated_preferences
- self.preferences = updated_preferences
- self.preferences_changed.emit(updated_preferences)
-
- def on_help_action_triggered(self):
- webbrowser.open("https://chidiwilliams.github.io/buzz/docs")
-
- def reset_shortcuts(self):
- self.import_action.setShortcut(
- QKeySequence.fromString(self.shortcuts.get(Shortcut.OPEN_IMPORT_WINDOW))
- )
- self.import_url_action.setShortcut(
- QKeySequence.fromString(self.shortcuts.get(Shortcut.OPEN_IMPORT_URL_WINDOW))
- )
- self.preferences_action.setShortcut(
- QKeySequence.fromString(
- self.shortcuts.get(Shortcut.OPEN_PREFERENCES_WINDOW)
- )
- )
diff --git a/buzz/widgets/model_download_progress_dialog.py b/buzz/widgets/model_download_progress_dialog.py
deleted file mode 100644
index 3c2bba80..00000000
--- a/buzz/widgets/model_download_progress_dialog.py
+++ /dev/null
@@ -1,56 +0,0 @@
-from datetime import datetime
-from typing import Optional
-
-import humanize
-from PyQt6.QtCore import Qt
-from PyQt6.QtWidgets import QProgressDialog, QWidget, QPushButton
-
-from buzz.locale import _
-from buzz.model_loader import ModelType
-
-
-class ModelDownloadProgressDialog(QProgressDialog):
- def __init__(
- self,
- model_type: ModelType,
- parent: Optional[QWidget] = None,
- modality=Qt.WindowModality.WindowModal,
- ):
- super().__init__(parent)
-
- self.setMinimumWidth(350)
- self.cancelable = (
- model_type == ModelType.WHISPER
- )
- self.start_time = datetime.now()
- self.setRange(0, 100)
- self.setMinimumDuration(0)
- self.setWindowModality(modality)
- self.update_label_text(0)
- cancel_button = QPushButton(_("Cancel"), self)
- self.setCancelButton(cancel_button)
-
- if not self.cancelable:
- cancel_button.setEnabled(False)
-
- def update_label_text(self, fraction_completed: float):
- downloading_text = _("Downloading model")
- remaining_text = _("remaining")
- label_text = f"{downloading_text} ("
- if fraction_completed > 0:
- time_spent = (datetime.now() - self.start_time).total_seconds()
- time_left = (time_spent / fraction_completed) - time_spent
- label_text += f"{humanize.naturaldelta(time_left)} {remaining_text}"
- label_text += ")"
-
- self.setLabelText(label_text)
-
- def set_value(self, fraction_completed: float):
- if self.wasCanceled():
- return
- self.setValue(int(fraction_completed * self.maximum()))
- self.update_label_text(fraction_completed)
-
- def cancel(self) -> None:
- if self.cancelable:
- super().cancel()
diff --git a/buzz/widgets/model_type_combo_box.py b/buzz/widgets/model_type_combo_box.py
deleted file mode 100644
index 31273f60..00000000
--- a/buzz/widgets/model_type_combo_box.py
+++ /dev/null
@@ -1,33 +0,0 @@
-from typing import Optional, List
-
-from PyQt6.QtCore import pyqtSignal
-from PyQt6.QtWidgets import QComboBox, QWidget
-
-from buzz.model_loader import ModelType
-
-
-class ModelTypeComboBox(QComboBox):
- changed = pyqtSignal(ModelType)
-
- def __init__(
- self,
- model_types: Optional[List[ModelType]] = None,
- default_model: Optional[ModelType] = None,
- parent: Optional[QWidget] = None,
- ):
- super().__init__(parent)
-
- if model_types is None:
- model_types = [
- model_type for model_type in ModelType if model_type.is_available()
- ]
-
- for model_type in model_types:
- self.addItem(model_type.value)
-
- self.currentTextChanged.connect(self.on_text_changed)
- if default_model is not None:
- self.setCurrentText(default_model.value)
-
- def on_text_changed(self, text: str):
- self.changed.emit(ModelType(text))
diff --git a/buzz/widgets/openai_api_key_line_edit.py b/buzz/widgets/openai_api_key_line_edit.py
deleted file mode 100644
index c994ce8a..00000000
--- a/buzz/widgets/openai_api_key_line_edit.py
+++ /dev/null
@@ -1,50 +0,0 @@
-import logging
-from typing import Optional
-
-from PyQt6.QtCore import pyqtSignal
-from PyQt6.QtWidgets import QWidget, QLineEdit
-
-from buzz.assets import get_path
-from buzz.widgets.icon import Icon, VisibilityIcon
-from buzz.widgets.line_edit import LineEdit
-
-
-class OpenAIAPIKeyLineEdit(LineEdit):
- key_changed = pyqtSignal(str)
- focus_out = pyqtSignal()
-
- def __init__(self, key: str, parent: Optional[QWidget] = None):
- super().__init__(key, parent)
-
- self.key = key
-
- self.visible_on_icon = VisibilityIcon(self)
- self.visible_off_icon = Icon(
- get_path("assets/visibility_off_FILL0_wght700_GRAD0_opsz48.svg"), self
- )
-
- self.setPlaceholderText("sk-...")
- self.setEchoMode(QLineEdit.EchoMode.Password)
- self.textChanged.connect(self.on_openai_api_key_changed)
- self.toggle_show_openai_api_key_action = self.addAction(
- self.visible_on_icon, QLineEdit.ActionPosition.TrailingPosition
- )
- self.toggle_show_openai_api_key_action.triggered.connect(
- self.on_toggle_show_action_triggered
- )
-
- def focusOutEvent(self, event):
- super().focusOutEvent(event)
- self.focus_out.emit()
-
- def on_toggle_show_action_triggered(self):
- if self.echoMode() == QLineEdit.EchoMode.Password:
- self.setEchoMode(QLineEdit.EchoMode.Normal)
- self.toggle_show_openai_api_key_action.setIcon(self.visible_off_icon)
- else:
- self.setEchoMode(QLineEdit.EchoMode.Password)
- self.toggle_show_openai_api_key_action.setIcon(self.visible_on_icon)
-
- def on_openai_api_key_changed(self, key: str):
- self.key = key
- self.key_changed.emit(key)
diff --git a/buzz/widgets/preferences_dialog/__init__.py b/buzz/widgets/preferences_dialog/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py b/buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
deleted file mode 100644
index e7a3c0c9..00000000
--- a/buzz/widgets/preferences_dialog/folder_watch_preferences_widget.py
+++ /dev/null
@@ -1,160 +0,0 @@
-from typing import Tuple, Optional
-
-from PyQt6.QtCore import pyqtSignal
-from PyQt6.QtWidgets import (
- QWidget,
- QPushButton,
- QFormLayout,
- QHBoxLayout,
- QFileDialog,
- QCheckBox,
- QVBoxLayout,
-)
-
-from buzz.locale import _
-from buzz.store.keyring_store import Key, get_password
-from buzz.transcriber.transcriber import (
- TranscriptionOptions,
- FileTranscriptionOptions,
-)
-from buzz.widgets.line_edit import LineEdit
-from buzz.widgets.preferences_dialog.models.file_transcription_preferences import (
- FileTranscriptionPreferences,
-)
-from buzz.widgets.preferences_dialog.models.folder_watch_preferences import (
- FolderWatchPreferences,
-)
-from buzz.widgets.transcriber.file_transcription_form_widget import (
- FileTranscriptionFormWidget,
-)
-
-
-class FolderWatchPreferencesWidget(QWidget):
- config_changed = pyqtSignal(FolderWatchPreferences)
-
- def __init__(
- self, config: FolderWatchPreferences, parent: Optional[QWidget] = None
- ):
- super().__init__(parent)
-
- self.config = config
-
- checkbox = QCheckBox(_("Enable folder watch"))
- checkbox.setChecked(config.enabled)
- checkbox.setObjectName("EnableFolderWatchCheckbox")
- checkbox.stateChanged.connect(self.on_enable_changed)
-
- delete_checkbox = QCheckBox(_("Delete processed files"))
- delete_checkbox.setChecked(config.delete_processed_files)
- delete_checkbox.setObjectName("DeleteProcessedFilesCheckbox")
- delete_checkbox.stateChanged.connect(self.on_delete_processed_files_changed)
-
- self.input_folder_browse_button = QPushButton(_("Browse"))
- self.input_folder_browse_button.clicked.connect(self.on_click_browse_input_folder)
-
- self.output_folder_browse_button = QPushButton(_("Browse"))
- self.output_folder_browse_button.clicked.connect(self.on_click_browse_output_folder)
-
- input_folder_row = QHBoxLayout()
- self.input_folder_line_edit = LineEdit(config.input_directory, self)
- self.input_folder_line_edit.setPlaceholderText("/path/to/input/folder")
- self.input_folder_line_edit.textChanged.connect(self.on_input_folder_changed)
- self.input_folder_line_edit.setObjectName("InputFolderLineEdit")
-
- input_folder_row.addWidget(self.input_folder_line_edit)
- input_folder_row.addWidget(self.input_folder_browse_button)
-
- output_folder_row = QHBoxLayout()
- self.output_folder_line_edit = LineEdit(config.output_directory, self)
- self.output_folder_line_edit.setPlaceholderText("/path/to/output/folder")
- self.output_folder_line_edit.textChanged.connect(self.on_output_folder_changed)
- self.output_folder_line_edit.setObjectName("OutputFolderLineEdit")
-
- output_folder_row.addWidget(self.output_folder_line_edit)
- output_folder_row.addWidget(self.output_folder_browse_button)
-
- openai_access_token = get_password(Key.OPENAI_API_KEY)
- (
- transcription_options,
- file_transcription_options,
- ) = config.file_transcription_options.to_transcription_options(
- openai_access_token=openai_access_token,
- file_paths=[],
- )
-
- self.transcription_form_widget = FileTranscriptionFormWidget(
- transcription_options=transcription_options,
- file_transcription_options=file_transcription_options,
- parent=self,
- )
- self.transcription_form_widget.transcription_options_changed.connect(
- self.on_transcription_options_changed
- )
-
- self.delete_checkbox = delete_checkbox
-
- layout = QVBoxLayout(self)
-
- folders_form_layout = QFormLayout()
-
- folders_form_layout.addRow("", checkbox)
- folders_form_layout.addRow(_("Input folder"), input_folder_row)
- folders_form_layout.addRow(_("Output folder"), output_folder_row)
- folders_form_layout.addRow("", delete_checkbox)
- folders_form_layout.addWidget(self.transcription_form_widget)
-
- layout.addLayout(folders_form_layout)
- layout.addWidget(self.transcription_form_widget)
- layout.addStretch()
-
- self.setLayout(layout)
-
- self._set_settings_enabled(config.enabled)
-
- def on_click_browse_input_folder(self):
- folder = QFileDialog.getExistingDirectory(self, _("Select Input Folder"))
- self.input_folder_line_edit.setText(folder)
- self.on_input_folder_changed(folder)
-
- def on_input_folder_changed(self, folder):
- self.config.input_directory = folder
- self.config_changed.emit(self.config)
-
- def on_click_browse_output_folder(self):
- folder = QFileDialog.getExistingDirectory(self, _("Select Output Folder"))
- self.output_folder_line_edit.setText(folder)
- self.on_output_folder_changed(folder)
-
- def on_output_folder_changed(self, folder):
- self.config.output_directory = folder
- self.config_changed.emit(self.config)
-
- def _set_settings_enabled(self, enabled: bool):
- self.input_folder_line_edit.setEnabled(enabled)
- self.input_folder_browse_button.setEnabled(enabled)
- self.output_folder_line_edit.setEnabled(enabled)
- self.output_folder_browse_button.setEnabled(enabled)
- self.delete_checkbox.setEnabled(enabled)
- self.transcription_form_widget.setEnabled(enabled)
-
- def on_enable_changed(self, state: int):
- enabled = state == 2
- self.config.enabled = enabled
- self._set_settings_enabled(enabled)
- self.config_changed.emit(self.config)
-
- def on_delete_processed_files_changed(self, state: int):
- self.config.delete_processed_files = state == 2
- self.config_changed.emit(self.config)
-
- def on_transcription_options_changed(
- self, options: Tuple[TranscriptionOptions, FileTranscriptionOptions]
- ):
- transcription_options, file_transcription_options = options
- self.config.file_transcription_options = (
- FileTranscriptionPreferences.from_transcription_options(
- transcription_options=transcription_options,
- file_transcription_options=file_transcription_options,
- )
- )
- self.config_changed.emit(self.config)
diff --git a/buzz/widgets/preferences_dialog/general_preferences_widget.py b/buzz/widgets/preferences_dialog/general_preferences_widget.py
deleted file mode 100644
index ff49f63c..00000000
--- a/buzz/widgets/preferences_dialog/general_preferences_widget.py
+++ /dev/null
@@ -1,384 +0,0 @@
-import re
-import logging
-import requests
-from typing import Optional
-from platformdirs import user_documents_dir
-
-from PyQt6.QtCore import QRunnable, QObject, pyqtSignal, QThreadPool, QLocale
-from PyQt6.QtWidgets import (
- QWidget,
- QFormLayout,
- QPushButton,
- QMessageBox,
- QCheckBox,
- QHBoxLayout,
- QFileDialog,
- QSpinBox,
- QComboBox,
- QLabel,
- QSizePolicy,
-)
-from PyQt6.QtGui import QIcon
-from openai import AuthenticationError, OpenAI
-
-from buzz.settings.settings import Settings
-from buzz.store.keyring_store import get_password, Key
-from buzz.widgets.line_edit import LineEdit
-from buzz.widgets.openai_api_key_line_edit import OpenAIAPIKeyLineEdit
-from buzz.locale import _
-from buzz.widgets.icon import INFO_ICON_PATH
-from buzz.settings.recording_transcriber_mode import RecordingTranscriberMode
-
-BASE64_PATTERN = re.compile(r'^[A-Za-z0-9+/=_-]*$')
-
-ui_locales = {
- "en_US": _("English"),
- "ca_ES": _("Catalan"),
- "da_DK": _("Danish"),
- "nl": _("Dutch"),
- "de_DE": _("German"),
- "es_ES": _("Spanish"),
- "it_IT": _("Italian"),
- "ja_JP": _("Japanese"),
- "lv_LV": _("Latvian"),
- "pl_PL": _("Polish"),
- "pt_BR": _("Portuguese (Brazil)"),
- "uk_UA": _("Ukrainian"),
- "zh_CN": _("Chinese (Simplified)"),
- "zh_TW": _("Chinese (Traditional)")
-}
-
-
-class GeneralPreferencesWidget(QWidget):
- openai_api_key_changed = pyqtSignal(str)
-
- def __init__(
- self,
- parent: Optional[QWidget] = None,
- ):
- super().__init__(parent)
-
- self.settings = Settings()
-
- self.openai_api_key = get_password(Key.OPENAI_API_KEY)
-
- layout = QFormLayout(self)
-
- self.ui_language_combo_box = QComboBox(self)
- self.ui_language_combo_box.addItems(ui_locales.values())
- system_locale = self.settings.value(Settings.Key.UI_LOCALE, QLocale().name())
- locale_index = 0
- for i, (code, language) in enumerate(ui_locales.items()):
- if code == system_locale:
- locale_index = i
- break
- self.ui_language_combo_box.setCurrentIndex(locale_index)
- self.ui_language_combo_box.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
- self.ui_language_combo_box.currentIndexChanged.connect(self.on_language_changed)
-
- self.ui_locale_layout = QHBoxLayout()
- self.ui_locale_layout.setContentsMargins(0, 0, 0, 0)
- self.ui_locale_layout.setSpacing(0)
- self.ui_locale_layout.addWidget(self.ui_language_combo_box)
-
- self.load_note_tooltip_icon = QLabel()
- self.load_note_tooltip_icon.setPixmap(QIcon(INFO_ICON_PATH).pixmap(23, 23))
- self.load_note_tooltip_icon.setToolTip(_("Restart required!"))
- self.load_note_tooltip_icon.setVisible(False)
- self.ui_locale_layout.addWidget(self.load_note_tooltip_icon)
-
- layout.addRow(_("Ui Language"), self.ui_locale_layout)
-
- self.font_size_spin_box = QSpinBox(self)
- self.font_size_spin_box.setMinimum(8)
- self.font_size_spin_box.setMaximum(32)
- self.font_size_spin_box.setValue(self.font().pointSize())
- self.font_size_spin_box.valueChanged.connect(self.on_font_size_changed)
-
- layout.addRow(_("Font Size"), self.font_size_spin_box)
-
- self.openai_api_key_line_edit = OpenAIAPIKeyLineEdit(self.openai_api_key, self)
- self.openai_api_key_line_edit.key_changed.connect(
- self.on_openai_api_key_changed
- )
- self.openai_api_key_line_edit.focus_out.connect(self.on_openai_api_key_focus_out)
- self.openai_api_key_line_edit.setMinimumWidth(200)
-
- self.test_openai_api_key_button = QPushButton(_("Test"))
- self.test_openai_api_key_button.clicked.connect(
- self.on_click_test_openai_api_key_button
- )
- self.update_test_openai_api_key_button()
-
- layout.addRow(_("OpenAI API key"), self.openai_api_key_line_edit)
- layout.addRow("", self.test_openai_api_key_button)
-
- self.custom_openai_base_url = self.settings.value(
- key=Settings.Key.CUSTOM_OPENAI_BASE_URL, default_value=""
- )
-
- self.custom_openai_base_url_line_edit = LineEdit(self.custom_openai_base_url, self)
- self.custom_openai_base_url_line_edit.textChanged.connect(
- self.on_custom_openai_base_url_changed
- )
- self.custom_openai_base_url_line_edit.setMinimumWidth(200)
- self.custom_openai_base_url_line_edit.setPlaceholderText("https://api.openai.com/v1")
- layout.addRow(_("OpenAI base url"), self.custom_openai_base_url_line_edit)
-
- self.openai_api_model = self.settings.value(
- key=Settings.Key.OPENAI_API_MODEL, default_value="whisper-1"
- )
-
- self.openai_api_model_line_edit = LineEdit(self.openai_api_model, self)
- self.openai_api_model_line_edit.textChanged.connect(
- self.on_openai_api_model_changed
- )
- self.openai_api_model_line_edit.setMinimumWidth(200)
- self.openai_api_model_line_edit.setPlaceholderText("whisper-1")
- layout.addRow(_("OpenAI API model"), self.openai_api_model_line_edit)
-
- default_export_file_name = self.settings.get_default_export_file_template()
-
- default_export_file_name_line_edit = LineEdit(default_export_file_name, self)
- default_export_file_name_line_edit.textChanged.connect(
- self.on_default_export_file_name_changed
- )
- default_export_file_name_line_edit.setMinimumWidth(200)
- layout.addRow(_("Default export file name"), default_export_file_name_line_edit)
-
- self.recording_export_enabled = self.settings.value(
- key=Settings.Key.RECORDING_TRANSCRIBER_EXPORT_ENABLED, default_value=False
- )
-
- self.export_enabled_checkbox = QCheckBox(_("Enable live recording transcription export"))
- self.export_enabled_checkbox.setChecked(self.recording_export_enabled)
- self.export_enabled_checkbox.setObjectName("EnableRecordingExportCheckbox")
- self.export_enabled_checkbox.stateChanged.connect(self.on_recording_export_enable_changed)
- layout.addRow("", self.export_enabled_checkbox)
-
- self.recording_export_folder_browse_button = QPushButton(_("Browse"))
- self.recording_export_folder_browse_button.clicked.connect(self.on_click_browse_export_folder)
- self.recording_export_folder_browse_button.setObjectName("RecordingExportFolderBrowseButton")
-
- recording_export_folder = self.settings.value(
- key=Settings.Key.RECORDING_TRANSCRIBER_EXPORT_FOLDER, default_value=user_documents_dir()
- )
-
- recording_export_folder_row = QHBoxLayout()
- self.recording_export_folder_line_edit = LineEdit(recording_export_folder, self)
- self.recording_export_folder_line_edit.textChanged.connect(self.on_recording_export_folder_changed)
- self.recording_export_folder_line_edit.setObjectName("RecordingExportFolderLineEdit")
-
- self.recording_export_folder_line_edit.setEnabled(self.recording_export_enabled)
- self.recording_export_folder_browse_button.setEnabled(self.recording_export_enabled)
-
- recording_export_folder_row.addWidget(self.recording_export_folder_line_edit)
- recording_export_folder_row.addWidget(self.recording_export_folder_browse_button)
-
- layout.addRow(_("Export folder"), recording_export_folder_row)
-
- self.recording_transcriber_mode = QComboBox(self)
- for mode in RecordingTranscriberMode:
- self.recording_transcriber_mode.addItem(mode.value)
-
- self.recording_transcriber_mode.setCurrentIndex(
- self.settings.value(Settings.Key.RECORDING_TRANSCRIBER_MODE, 0)
- )
- self.recording_transcriber_mode.currentIndexChanged.connect(self.on_recording_transcriber_mode_changed)
-
- layout.addRow(_("Live recording mode"), self.recording_transcriber_mode)
-
- export_note_label = QLabel(
- _("Note: Live recording export settings will be moved to the Advanced Settings in the Live Recording screen in a future version."),
- self,
- )
- export_note_label.setWordWrap(True)
- export_note_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
- layout.addRow("", export_note_label)
-
- self.reduce_gpu_memory_enabled = self.settings.value(
- key=Settings.Key.REDUCE_GPU_MEMORY, default_value=False
- )
-
- self.reduce_gpu_memory_checkbox = QCheckBox(_("Use 8-bit quantization to reduce memory usage"))
- self.reduce_gpu_memory_checkbox.setChecked(self.reduce_gpu_memory_enabled)
- self.reduce_gpu_memory_checkbox.setObjectName("ReduceGPUMemoryCheckbox")
- self.reduce_gpu_memory_checkbox.setToolTip(
- _("Applies to Huggingface and Faster Whisper models. "
- "Reduces GPU memory usage but may slightly decrease transcription quality.")
- )
- self.reduce_gpu_memory_checkbox.stateChanged.connect(self.on_reduce_gpu_memory_changed)
- layout.addRow(_("Reduce GPU RAM"), self.reduce_gpu_memory_checkbox)
-
- self.force_cpu_enabled = self.settings.value(
- key=Settings.Key.FORCE_CPU, default_value=False
- )
-
- self.force_cpu_checkbox = QCheckBox(_("Use only CPU and disable GPU acceleration"))
- self.force_cpu_checkbox.setChecked(self.force_cpu_enabled)
- self.force_cpu_checkbox.setObjectName("ForceCPUCheckbox")
- self.force_cpu_checkbox.setToolTip(_("Set this if larger models do not fit your GPU memory and Buzz crashes"))
- self.force_cpu_checkbox.stateChanged.connect(self.on_force_cpu_changed)
- layout.addRow(_("Disable GPU"), self.force_cpu_checkbox)
-
- self.setLayout(layout)
-
- def on_default_export_file_name_changed(self, text: str):
- self.settings.set_value(Settings.Key.DEFAULT_EXPORT_FILE_NAME, text)
-
- def update_test_openai_api_key_button(self):
- self.test_openai_api_key_button.setEnabled(len(self.openai_api_key) > 0)
-
- def on_click_test_openai_api_key_button(self):
- self.test_openai_api_key_button.setEnabled(False)
-
- 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)
-
- thread_pool = QThreadPool.globalInstance()
- thread_pool.start(job)
-
- def on_test_openai_api_key_success(self):
- self.test_openai_api_key_button.setEnabled(True)
- QMessageBox.information(
- self,
- _("OpenAI API Key Test"),
- _("Your API key is valid. Buzz will use this key to perform Whisper API transcriptions and AI translations."),
- )
-
- def on_test_openai_api_key_failure(self, error: str):
- self.test_openai_api_key_button.setEnabled(True)
- QMessageBox.warning(self, _("OpenAI API Key Test"), error)
-
- def on_openai_api_key_changed(self, key: str):
- self.openai_api_key = key
- self.update_test_openai_api_key_button()
- self.openai_api_key_changed.emit(key)
-
- def on_openai_api_key_focus_out(self):
- if not BASE64_PATTERN.match(self.openai_api_key):
- QMessageBox.warning(
- self,
- _("Invalid API key"),
- _("API supports only base64 characters (A-Za-z0-9+/=_-). Other characters in API key may cause errors."),
- )
-
- def on_custom_openai_base_url_changed(self, text: str):
- self.settings.set_value(Settings.Key.CUSTOM_OPENAI_BASE_URL, text)
-
- def on_openai_api_model_changed(self, text: str):
- self.settings.set_value(Settings.Key.OPENAI_API_MODEL, text)
-
- def on_recording_export_enable_changed(self, state: int):
- self.recording_export_enabled = state == 2
-
- self.recording_export_folder_line_edit.setEnabled(self.recording_export_enabled)
- self.recording_export_folder_browse_button.setEnabled(self.recording_export_enabled)
-
- self.settings.set_value(
- Settings.Key.RECORDING_TRANSCRIBER_EXPORT_ENABLED,
- self.recording_export_enabled,
- )
-
- def on_click_browse_export_folder(self):
- folder = QFileDialog.getExistingDirectory(self, _("Select Export Folder"))
- self.recording_export_folder_line_edit.setText(folder)
- self.on_recording_export_folder_changed(folder)
-
- def on_recording_export_folder_changed(self, folder):
- self.settings.set_value(
- Settings.Key.RECORDING_TRANSCRIBER_EXPORT_FOLDER,
- folder,
- )
-
- def on_language_changed(self, index):
- selected_language = self.ui_language_combo_box.itemText(index)
- locale_code = next((code for code, lang in ui_locales.items() if lang == selected_language), "en_US")
-
- self.load_note_tooltip_icon.setVisible(True)
-
- self.settings.set_value(Settings.Key.UI_LOCALE, locale_code)
-
- def on_font_size_changed(self, value):
- from buzz.widgets.application import Application
- font = self.font()
- font.setPointSize(value)
- self.setFont(font)
- Application.instance().setFont(font)
-
- self.settings.set_value(Settings.Key.FONT_SIZE, value)
-
- def on_recording_transcriber_mode_changed(self, value):
- self.settings.set_value(Settings.Key.RECORDING_TRANSCRIBER_MODE, value)
-
- def on_force_cpu_changed(self, state: int):
- import os
- self.force_cpu_enabled = state == 2
- self.settings.set_value(Settings.Key.FORCE_CPU, self.force_cpu_enabled)
-
- if self.force_cpu_enabled:
- os.environ["BUZZ_FORCE_CPU"] = "true"
- else:
- os.environ.pop("BUZZ_FORCE_CPU", None)
-
- def on_reduce_gpu_memory_changed(self, state: int):
- import os
- self.reduce_gpu_memory_enabled = state == 2
- self.settings.set_value(Settings.Key.REDUCE_GPU_MEMORY, self.reduce_gpu_memory_enabled)
-
- if self.reduce_gpu_memory_enabled:
- os.environ["BUZZ_REDUCE_GPU_MEMORY"] = "true"
- else:
- os.environ.pop("BUZZ_REDUCE_GPU_MEMORY", None)
-
-
-class ValidateOpenAIApiKeyJob(QRunnable):
- class Signals(QObject):
- success = pyqtSignal()
- failed = pyqtSignal(str)
-
- def __init__(self, api_key: str):
- super().__init__()
- self.api_key = api_key
- self.signals = self.Signals()
-
- def run(self):
- settings = Settings()
- custom_openai_base_url = settings.value(
- key=Settings.Key.CUSTOM_OPENAI_BASE_URL, default_value=""
- )
-
- if custom_openai_base_url:
- try:
- if not custom_openai_base_url.endswith("/"):
- custom_openai_base_url += "/"
-
- headers = {
- "Authorization": f"Bearer {self.api_key}",
- "Content-Type": "application/json"
- }
-
- response = requests.get(custom_openai_base_url + "models", headers=headers, timeout=5)
-
- if response.status_code != 200:
- self.signals.failed.emit(
- _("OpenAI API returned invalid response. Please check the API url or your key. "
- "Transcription and translation may still work if the API does not support key validation.")
- )
- return
- except requests.exceptions.RequestException as exc:
- self.signals.failed.emit(str(exc))
- return
-
- try:
- client = OpenAI(
- api_key=self.api_key,
- base_url=custom_openai_base_url if custom_openai_base_url else None,
- timeout=15,
- )
- client.models.list()
- self.signals.success.emit()
- except AuthenticationError as exc:
- self.signals.failed.emit(exc.body["message"])
diff --git a/buzz/widgets/preferences_dialog/models/__init__.py b/buzz/widgets/preferences_dialog/models/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/buzz/widgets/preferences_dialog/models/file_transcription_preferences.py b/buzz/widgets/preferences_dialog/models/file_transcription_preferences.py
deleted file mode 100644
index 91e31494..00000000
--- a/buzz/widgets/preferences_dialog/models/file_transcription_preferences.py
+++ /dev/null
@@ -1,126 +0,0 @@
-from dataclasses import dataclass
-from typing import Optional, Tuple, Set, List
-
-from PyQt6.QtCore import QSettings
-
-from buzz.model_loader import TranscriptionModel
-from buzz.transcriber.transcriber import (
- Task,
- OutputFormat,
- TranscriptionOptions,
- FileTranscriptionOptions,
-)
-
-
-@dataclass()
-class FileTranscriptionPreferences:
- language: Optional[str]
- task: Task
- model: TranscriptionModel
- word_level_timings: bool
- extract_speech: bool
- initial_prompt: str
- enable_llm_translation: bool
- llm_prompt: str
- llm_model: str
- output_formats: Set["OutputFormat"]
-
- def save(self, settings: QSettings) -> None:
- settings.setValue("language", self.language)
- settings.setValue("task", self.task)
- settings.setValue("model", self.model)
- settings.setValue("word_level_timings", self.word_level_timings)
- settings.setValue("extract_speech", self.extract_speech)
- settings.setValue("initial_prompt", self.initial_prompt)
- settings.setValue("enable_llm_translation", self.enable_llm_translation)
- settings.setValue("llm_model", self.llm_model)
- settings.setValue("llm_prompt", self.llm_prompt)
- settings.setValue(
- "output_formats",
- [output_format.value for output_format in self.output_formats],
- )
-
- @classmethod
- def load(cls, settings: QSettings) -> "FileTranscriptionPreferences":
- language = settings.value("language", None)
- task = settings.value("task", Task.TRANSCRIBE)
- model: TranscriptionModel = settings.value(
- "model", TranscriptionModel.default()
- )
-
- word_level_timings_value = settings.value("word_level_timings", False)
- word_level_timings = False if word_level_timings_value == "false" \
- else bool(word_level_timings_value)
-
- extract_speech_value = settings.value("extract_speech", False)
- extract_speech = False if extract_speech_value == "false" \
- else bool(extract_speech_value)
-
- initial_prompt = settings.value("initial_prompt", "")
- enable_llm_translation_value = settings.value("enable_llm_translation", False)
- enable_llm_translation = False if enable_llm_translation_value == "false" \
- else bool(enable_llm_translation_value)
- llm_model = settings.value("llm_model", "")
- llm_prompt = settings.value("llm_prompt", "")
- output_formats = settings.value("output_formats", []) or []
- return FileTranscriptionPreferences(
- language=language,
- task=task,
- model=model
- if model.model_type.is_available()
- else TranscriptionModel.default(),
- word_level_timings=word_level_timings,
- extract_speech=extract_speech,
- initial_prompt=initial_prompt,
- enable_llm_translation=enable_llm_translation,
- llm_model=llm_model,
- llm_prompt=llm_prompt,
- output_formats=set(
- [OutputFormat(output_format) for output_format in output_formats]
- ),
- )
-
- @classmethod
- def from_transcription_options(
- cls,
- transcription_options: TranscriptionOptions,
- file_transcription_options: FileTranscriptionOptions,
- ) -> "FileTranscriptionPreferences":
- return FileTranscriptionPreferences(
- task=transcription_options.task,
- language=transcription_options.language,
- initial_prompt=transcription_options.initial_prompt,
- enable_llm_translation=transcription_options.enable_llm_translation,
- llm_model=transcription_options.llm_model,
- llm_prompt=transcription_options.llm_prompt,
- word_level_timings=transcription_options.word_level_timings,
- extract_speech=transcription_options.extract_speech,
- model=transcription_options.model,
- output_formats=file_transcription_options.output_formats,
- )
-
- def to_transcription_options(
- self,
- openai_access_token: Optional[str],
- file_paths: Optional[List[str]] = None,
- url: Optional[str] = None,
- ) -> Tuple[TranscriptionOptions, FileTranscriptionOptions]:
- return (
- TranscriptionOptions(
- task=self.task,
- language=self.language,
- initial_prompt=self.initial_prompt,
- enable_llm_translation=self.enable_llm_translation,
- llm_model=self.llm_model,
- llm_prompt=self.llm_prompt,
- word_level_timings=self.word_level_timings,
- extract_speech=self.extract_speech,
- model=self.model,
- openai_access_token=openai_access_token,
- ),
- FileTranscriptionOptions(
- output_formats=self.output_formats,
- file_paths=file_paths,
- url=url,
- ),
- )
diff --git a/buzz/widgets/preferences_dialog/models/folder_watch_preferences.py b/buzz/widgets/preferences_dialog/models/folder_watch_preferences.py
deleted file mode 100644
index 6dcea82a..00000000
--- a/buzz/widgets/preferences_dialog/models/folder_watch_preferences.py
+++ /dev/null
@@ -1,45 +0,0 @@
-from dataclasses import dataclass
-
-from PyQt6.QtCore import QSettings
-
-from buzz.widgets.preferences_dialog.models.file_transcription_preferences import (
- FileTranscriptionPreferences,
-)
-
-
-@dataclass
-class FolderWatchPreferences:
- enabled: bool
- input_directory: str
- output_directory: str
- file_transcription_options: FileTranscriptionPreferences
- delete_processed_files: bool = False
-
- def save(self, settings: QSettings):
- settings.setValue("enabled", self.enabled)
- settings.setValue("input_folder", self.input_directory)
- settings.setValue("output_directory", self.output_directory)
- settings.setValue("delete_processed_files", self.delete_processed_files)
- settings.beginGroup("file_transcription_options")
- self.file_transcription_options.save(settings)
- settings.endGroup()
-
- @classmethod
- def load(cls, settings: QSettings) -> "FolderWatchPreferences":
- enabled_value = settings.value("enabled", False)
- enabled = False if enabled_value == "false" else bool(enabled_value)
-
- input_folder = settings.value("input_folder", defaultValue="", type=str)
- output_folder = settings.value("output_directory", defaultValue="", type=str)
- delete_value = settings.value("delete_processed_files", False)
- delete_processed_files = False if delete_value == "false" else bool(delete_value)
- settings.beginGroup("file_transcription_options")
- file_transcription_options = FileTranscriptionPreferences.load(settings)
- settings.endGroup()
- return FolderWatchPreferences(
- enabled=enabled,
- input_directory=input_folder,
- output_directory=output_folder,
- file_transcription_options=file_transcription_options,
- delete_processed_files=delete_processed_files,
- )
diff --git a/buzz/widgets/preferences_dialog/models/preferences.py b/buzz/widgets/preferences_dialog/models/preferences.py
deleted file mode 100644
index b3d8d0fa..00000000
--- a/buzz/widgets/preferences_dialog/models/preferences.py
+++ /dev/null
@@ -1,24 +0,0 @@
-from dataclasses import dataclass
-
-from PyQt6.QtCore import QSettings
-
-from buzz.widgets.preferences_dialog.models.folder_watch_preferences import (
- FolderWatchPreferences,
-)
-
-
-@dataclass
-class Preferences:
- folder_watch: FolderWatchPreferences
-
- def save(self, settings: QSettings):
- settings.beginGroup("folder_watch")
- self.folder_watch.save(settings)
- settings.endGroup()
-
- @classmethod
- def load(cls, settings: QSettings) -> "Preferences":
- settings.beginGroup("folder_watch")
- folder_watch = FolderWatchPreferences.load(settings)
- settings.endGroup()
- return Preferences(folder_watch=folder_watch)
diff --git a/buzz/widgets/preferences_dialog/models_preferences_widget.py b/buzz/widgets/preferences_dialog/models_preferences_widget.py
deleted file mode 100644
index 1835453f..00000000
--- a/buzz/widgets/preferences_dialog/models_preferences_widget.py
+++ /dev/null
@@ -1,283 +0,0 @@
-import logging
-from typing import Optional
-
-from PyQt6.QtCore import Qt, QThreadPool, QLocale
-from PyQt6.QtWidgets import (
- QWidget,
- QFormLayout,
- QTreeWidget,
- QTreeWidgetItem,
- QPushButton,
- QMessageBox,
- QHBoxLayout,
- QLayout
-)
-
-from buzz.locale import _
-from buzz.model_loader import (
- ModelType,
- WhisperModelSize,
- TranscriptionModel,
- ModelDownloader,
-)
-from buzz.settings.settings import Settings
-from buzz.widgets.model_download_progress_dialog import ModelDownloadProgressDialog
-from buzz.widgets.model_type_combo_box import ModelTypeComboBox
-from buzz.widgets.line_edit import LineEdit
-from buzz.widgets.transcriber.hugging_face_search_line_edit import (
- HuggingFaceSearchLineEdit,
-)
-
-
-class ModelsPreferencesWidget(QWidget):
- model: Optional[TranscriptionModel]
-
- def __init__(
- self,
- progress_dialog_modality=Qt.WindowModality.WindowModal,
- parent: Optional[QWidget] = None,
- ):
- super().__init__(parent)
-
- self.settings = Settings()
- self.ui_locale = self.settings.value(Settings.Key.UI_LOCALE, QLocale().name())
- self.model_downloader: Optional[ModelDownloader] = None
-
- model_types = [
- model_type
- for model_type in ModelType
- if model_type.is_available() and model_type.is_manually_downloadable()
- ]
-
- self.model = (
- TranscriptionModel(
- model_type=model_types[0], whisper_model_size=WhisperModelSize.TINY
- )
- if model_types[0] is not None
- else None
- )
- self.progress_dialog_modality = progress_dialog_modality
-
- self.progress_dialog: Optional[ModelDownloadProgressDialog] = None
-
- layout = QFormLayout()
- layout.setSizeConstraint(QLayout.SizeConstraint.SetNoConstraint)
- model_type_combo_box = ModelTypeComboBox(
- model_types=model_types,
- default_model=self.model.model_type if self.model is not None else None,
- parent=self,
- )
- model_type_combo_box.changed.connect(self.on_model_type_changed)
- layout.addRow(_("Group"), model_type_combo_box)
-
- self.model_list_widget = QTreeWidget()
- self.model_list_widget.setColumnCount(1)
- self.model_list_widget.currentItemChanged.connect(self.on_model_size_changed)
- layout.addWidget(self.model_list_widget)
-
- buttons_layout = QHBoxLayout()
-
- self.custom_model_id_input = HuggingFaceSearchLineEdit()
- self.custom_model_id_input.setObjectName("ModelIdInput")
-
- self.custom_model_id_input.setPlaceholderText(_("Huggingface ID of a Faster whisper model"))
- self.custom_model_id_input.textChanged.connect(self.on_custom_model_id_input_changed)
- layout.addRow("", self.custom_model_id_input)
- self.custom_model_id_input.hide()
-
- self.custom_model_link_input = LineEdit()
- self.custom_model_link_input.setMinimumWidth(255)
- self.custom_model_link_input.setObjectName("ModelLinkInput")
- self.custom_model_link_input.textChanged.connect(self.on_custom_model_link_input_changed)
- layout.addRow("", self.custom_model_link_input)
- self.custom_model_link_input.hide()
-
- self.download_button = QPushButton(_("Download"))
- self.download_button.setObjectName("DownloadButton")
- self.download_button.clicked.connect(self.on_download_button_clicked)
- buttons_layout.addWidget(self.download_button)
-
- self.show_file_location_button = QPushButton(_("Show file location"))
- self.show_file_location_button.setObjectName("ShowFileLocationButton")
- self.show_file_location_button.clicked.connect(
- self.on_show_file_location_button_clicked
- )
- buttons_layout.addWidget(self.show_file_location_button)
- buttons_layout.addStretch(1)
-
- self.delete_button = QPushButton(_("Delete"))
- self.delete_button.setObjectName("DeleteButton")
- self.delete_button.clicked.connect(self.on_delete_button_clicked)
- buttons_layout.addWidget(self.delete_button)
-
- layout.addRow("", buttons_layout)
-
- self.reset()
-
- self.setLayout(layout)
-
- def on_model_size_changed(self, current: QTreeWidgetItem, _: QTreeWidgetItem):
- if current is None:
- return
- item_data = current.data(0, Qt.ItemDataRole.UserRole)
- if item_data is None:
- return
- self.model.whisper_model_size = item_data
- self.reset()
-
- def reset(self):
- # reset buttons
- path = self.model.get_local_model_path()
- self.download_button.setVisible(path is None)
- self.download_button.setEnabled(self.model.whisper_model_size != WhisperModelSize.CUSTOM)
- self.delete_button.setVisible(self.model.is_deletable())
- self.show_file_location_button.setVisible(self.model.is_deletable())
-
- # reset model list
- self.model_list_widget.clear()
- downloaded_item = QTreeWidgetItem(self.model_list_widget)
- downloaded_item.setText(0, _("Downloaded"))
- downloaded_item.setFlags(
- downloaded_item.flags() & ~Qt.ItemFlag.ItemIsSelectable
- )
- available_item = QTreeWidgetItem(self.model_list_widget)
- available_item.setText(0, _("Available for Download"))
- available_item.setFlags(available_item.flags() & ~Qt.ItemFlag.ItemIsSelectable)
- self.model_list_widget.addTopLevelItems([downloaded_item, available_item])
- self.model_list_widget.expandToDepth(2)
- self.model_list_widget.setHeaderHidden(True)
- self.model_list_widget.setAlternatingRowColors(True)
-
- self.model.hugging_face_model_id = self.settings.load_custom_model_id(self.model)
- self.custom_model_id_input.setText(self.model.hugging_face_model_id)
-
- if (self.model.whisper_model_size == WhisperModelSize.CUSTOM
- and self.model.model_type == ModelType.FASTER_WHISPER):
- self.custom_model_id_input.show()
- self.download_button.setEnabled(
- self.model.hugging_face_model_id != ""
- )
- else:
- self.custom_model_id_input.hide()
-
- if self.model.model_type == ModelType.WHISPER_CPP:
- self.custom_model_link_input.setPlaceholderText(
- _("Download link to Whisper.cpp ggml model file")
- )
-
- if (self.model.whisper_model_size == WhisperModelSize.CUSTOM
- and self.model.model_type == ModelType.WHISPER_CPP
- and path is None):
- self.custom_model_link_input.show()
- self.download_button.setEnabled(
- self.custom_model_link_input.text() != "")
- else:
- self.custom_model_link_input.hide()
-
- if self.model is None:
- return
-
- for model_size in WhisperModelSize:
- # Skip custom size for OpenAI Whisper
- if (self.model.model_type == ModelType.WHISPER and
- model_size == WhisperModelSize.CUSTOM):
- continue
-
- # Skip LUMII size for all non Latvians
- if (model_size == WhisperModelSize.LUMII and
- (self.model.model_type != ModelType.WHISPER_CPP or self.ui_locale != "lv_LV")):
- continue
-
- model = TranscriptionModel(
- model_type=self.model.model_type,
- whisper_model_size=WhisperModelSize(model_size),
- hugging_face_model_id=self.model.hugging_face_model_id,
- )
- model_path = model.get_local_model_path()
- parent = downloaded_item if model_path is not None else available_item
- item = QTreeWidgetItem(parent)
- item.setText(0, model_size.value.title())
- item.setData(0, Qt.ItemDataRole.UserRole, model_size)
- if self.model.whisper_model_size == model_size:
- item.setSelected(True)
- parent.addChild(item)
-
- def on_model_type_changed(self, model_type: ModelType):
- self.model.model_type = model_type
- self.reset()
-
- def on_custom_model_id_input_changed(self, text):
- self.model.hugging_face_model_id = text
- self.settings.save_custom_model_id(self.model)
- self.download_button.setEnabled(
- self.model.hugging_face_model_id != ""
- )
-
- def on_custom_model_link_input_changed(self, text):
- self.download_button.setEnabled(text != "")
-
- def on_download_button_clicked(self):
- self.progress_dialog = ModelDownloadProgressDialog(
- model_type=self.model.model_type,
- modality=self.progress_dialog_modality,
- parent=self,
- )
- self.progress_dialog.canceled.connect(self.on_progress_dialog_canceled)
-
- self.download_button.setEnabled(False)
-
- if (self.model.whisper_model_size == WhisperModelSize.CUSTOM and
- self.model.model_type == ModelType.WHISPER_CPP):
- self.model_downloader = ModelDownloader(
- model=self.model,
- custom_model_url=self.custom_model_link_input.text()
- )
- else:
- self.model_downloader = ModelDownloader(model=self.model)
-
- self.model_downloader.signals.finished.connect(self.on_download_completed)
- self.model_downloader.signals.progress.connect(self.on_download_progress)
- self.model_downloader.signals.error.connect(self.on_download_error)
- QThreadPool().globalInstance().start(self.model_downloader)
-
- def on_delete_button_clicked(self):
- reply = QMessageBox(self)
- reply.setWindowTitle(_("Delete Model"))
- reply.setText(_("Are you sure you want to delete the selected model?"))
- reply.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
-
- ok_button = reply.button(QMessageBox.StandardButton.Yes)
- cancel_button = reply.button(QMessageBox.StandardButton.No)
- ok_button.setText(_("Ok"))
- cancel_button.setText(_("Cancel"))
-
- user_choice = reply.exec()
-
- if user_choice == QMessageBox.StandardButton.Yes:
- self.model.delete_local_file()
- self.reset()
-
- def on_show_file_location_button_clicked(self):
- self.model.open_file_location()
-
- def on_download_completed(self, _: str):
- self.progress_dialog = None
- self.download_button.setEnabled(True)
- self.reset()
-
- def on_download_error(self, error: str):
- self.progress_dialog.cancel()
- self.progress_dialog.close()
- self.progress_dialog = None
- self.download_button.setEnabled(True)
- self.reset()
- download_failed_label = _('Download failed')
- QMessageBox.warning(self, _("Error"), f"{download_failed_label}: {error}")
-
- def on_download_progress(self, progress: tuple):
- if progress[1] != 0:
- self.progress_dialog.set_value(float(progress[0]) / progress[1])
-
- def on_progress_dialog_canceled(self):
- self.model_downloader.cancel()
- self.reset()
diff --git a/buzz/widgets/preferences_dialog/preferences_dialog.py b/buzz/widgets/preferences_dialog/preferences_dialog.py
deleted file mode 100644
index 211e345d..00000000
--- a/buzz/widgets/preferences_dialog/preferences_dialog.py
+++ /dev/null
@@ -1,80 +0,0 @@
-import copy
-from typing import Optional
-
-from PyQt6.QtCore import pyqtSignal
-from PyQt6.QtWidgets import QDialog, QWidget, QVBoxLayout, QTabWidget, QDialogButtonBox
-
-from buzz.locale import _
-from buzz.settings.shortcuts import Shortcuts
-from buzz.widgets.preferences_dialog.folder_watch_preferences_widget import (
- FolderWatchPreferencesWidget,
-)
-from buzz.widgets.preferences_dialog.general_preferences_widget import (
- GeneralPreferencesWidget,
-)
-from buzz.widgets.preferences_dialog.models.folder_watch_preferences import (
- FolderWatchPreferences,
-)
-from buzz.widgets.preferences_dialog.models.preferences import Preferences
-from buzz.widgets.preferences_dialog.models_preferences_widget import (
- ModelsPreferencesWidget,
-)
-from buzz.widgets.preferences_dialog.shortcuts_editor_preferences_widget import (
- ShortcutsEditorPreferencesWidget,
-)
-
-
-class PreferencesDialog(QDialog):
- shortcuts_changed = pyqtSignal()
- openai_api_key_changed = pyqtSignal(str)
- folder_watch_config_changed = pyqtSignal(FolderWatchPreferences)
- preferences_changed = pyqtSignal(Preferences)
-
- def __init__(
- self,
- shortcuts: Shortcuts,
- preferences: Preferences,
- parent: Optional[QWidget] = None,
- ) -> None:
- super().__init__(parent)
-
- self.updated_preferences = copy.deepcopy(preferences)
-
- self.setWindowTitle(_("Preferences"))
-
- layout = QVBoxLayout(self)
- tab_widget = QTabWidget(self)
-
- general_tab_widget = GeneralPreferencesWidget(parent=self)
- general_tab_widget.openai_api_key_changed.connect(self.openai_api_key_changed)
- tab_widget.addTab(general_tab_widget, _("General"))
-
- models_tab_widget = ModelsPreferencesWidget(parent=self)
- tab_widget.addTab(models_tab_widget, _("Models"))
-
- shortcuts_table_widget = ShortcutsEditorPreferencesWidget(shortcuts, self)
- shortcuts_table_widget.shortcuts_changed.connect(self.shortcuts_changed)
- tab_widget.addTab(shortcuts_table_widget, _("Shortcuts"))
-
- folder_watch_widget = FolderWatchPreferencesWidget(
- config=self.updated_preferences.folder_watch, parent=self
- )
- folder_watch_widget.config_changed.connect(self.folder_watch_config_changed)
- tab_widget.addTab(folder_watch_widget, _("Folder Watch"))
-
- button_box = QDialogButtonBox(
- QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel,
- self,
- )
- button_box.button(QDialogButtonBox.StandardButton.Ok).setText(_("Ok"))
- button_box.button(QDialogButtonBox.StandardButton.Cancel).setText(_("Cancel"))
- button_box.accepted.connect(self.accept)
- button_box.rejected.connect(self.reject)
-
- layout.addWidget(tab_widget)
- layout.addWidget(button_box)
-
- self.setLayout(layout)
-
- self.setMinimumHeight(500)
- self.setMinimumWidth(650)
diff --git a/buzz/widgets/preferences_dialog/shortcuts_editor_preferences_widget.py b/buzz/widgets/preferences_dialog/shortcuts_editor_preferences_widget.py
deleted file mode 100644
index 66395a64..00000000
--- a/buzz/widgets/preferences_dialog/shortcuts_editor_preferences_widget.py
+++ /dev/null
@@ -1,56 +0,0 @@
-from typing import Optional
-
-from PyQt6.QtCore import pyqtSignal
-from PyQt6.QtGui import QKeySequence
-from PyQt6.QtWidgets import QWidget, QFormLayout, QPushButton
-
-from buzz.locale import _
-from buzz.settings.shortcut import Shortcut
-from buzz.settings.shortcuts import Shortcuts
-from buzz.widgets.line_edit import LineEdit
-from buzz.widgets.sequence_edit import SequenceEdit
-
-
-class ShortcutsEditorPreferencesWidget(QWidget):
- shortcuts_changed = pyqtSignal()
-
- def __init__(self, shortcuts: Shortcuts, parent: Optional[QWidget] = None):
- super().__init__(parent)
-
- self.shortcuts = shortcuts
-
- self.layout = QFormLayout(self)
- _field_height = LineEdit().sizeHint().height()
- for shortcut in Shortcut:
- sequence_edit = SequenceEdit(shortcuts.get(shortcut), self)
- sequence_edit.setFixedHeight(_field_height)
- sequence_edit.keySequenceChanged.connect(
- self.get_key_sequence_changed(shortcut)
- )
- self.layout.addRow(shortcut.description, sequence_edit)
-
- reset_to_defaults_button = QPushButton(_("Reset to Defaults"), self)
- reset_to_defaults_button.setDefault(False)
- reset_to_defaults_button.setAutoDefault(False)
- reset_to_defaults_button.clicked.connect(self.reset_to_defaults)
-
- self.layout.addWidget(reset_to_defaults_button)
-
- def get_key_sequence_changed(self, shortcut: Shortcut):
- def key_sequence_changed(sequence: QKeySequence):
- self.shortcuts.set(shortcut, sequence.toString())
- self.shortcuts_changed.emit()
-
- return key_sequence_changed
-
- def reset_to_defaults(self):
- self.shortcuts.clear()
-
- for i, shortcut in enumerate(Shortcut):
- sequence_edit = self.layout.itemAt(
- i, QFormLayout.ItemRole.FieldRole
- ).widget()
- assert isinstance(sequence_edit, SequenceEdit)
- sequence_edit.setKeySequence(QKeySequence(self.shortcuts.get(shortcut)))
-
- self.shortcuts_changed.emit()
diff --git a/buzz/widgets/presentation_window.py b/buzz/widgets/presentation_window.py
deleted file mode 100644
index 8aad5ee4..00000000
--- a/buzz/widgets/presentation_window.py
+++ /dev/null
@@ -1,189 +0,0 @@
-import logging
-from typing import Optional
-from PyQt6.QtCore import Qt
-from PyQt6.QtGui import QTextCursor
-from PyQt6.QtWidgets import QWidget, QVBoxLayout, QTextBrowser
-from platformdirs import user_cache_dir
-
-from buzz.locale import _
-from buzz.settings.settings import Settings
-
-import os
-
-class PresentationWindow(QWidget):
- """Window for displaying live transcripts in presentation mode"""
-
- def __init__(self, parent: Optional[QWidget] = None):
- super().__init__(parent)
-
- self.settings = Settings()
- self._current_transcript = ""
- self._current_translation = ""
- self.window_style = ""
- self.setWindowTitle(_("Live Transcript Presentation"))
- self.setWindowFlag(Qt.WindowType.Window)
-
- # Window size
- self.resize(800, 600)
-
- # Create layout
- layout = QVBoxLayout(self)
- layout.setContentsMargins(0, 0, 0, 0)
- layout.setSpacing(0)
-
- # Text display widget
- self.transcript_display = QTextBrowser(self)
- self.transcript_display.setReadOnly(True)
-
- # Translation display (hidden first)
- self.translation_display = QTextBrowser(self)
- self.translation_display.setReadOnly(True)
- self.translation_display.hide()
-
- # Add to layout
- layout.addWidget(self.transcript_display)
- layout.addWidget(self.translation_display)
-
- self.load_settings()
-
- def load_settings(self):
- """Load and apply saved presentation settings"""
- theme = self.settings.value(
- Settings.Key.PRESENTATION_WINDOW_THEME,
- "light"
- )
-
- # Load text size
- text_size = self.settings.value(
- Settings.Key.PRESENTATION_WINDOW_TEXT_SIZE,
- 24,
- int
- )
-
- # Load colors based on theme
- if theme == "light":
- text_color = "#000000"
- bg_color = "#FFFFFF"
- elif theme == "dark":
- text_color = "#FFFFFF"
- bg_color = "#000000"
- else:
- text_color = self.settings.value(
- Settings.Key.PRESENTATION_WINDOW_TEXT_COLOR,
- "#000000"
- )
-
- bg_color = self.settings.value(
- Settings.Key.PRESENTATION_WINDOW_BACKGROUND_COLOR,
- "#FFFFFF"
- )
-
- self.apply_styling(text_color, bg_color, text_size)
-
- # Refresh content with new styling
- if self._current_transcript:
- self.update_transcripts(self._current_transcript)
- if self._current_translation:
- self.update_translations(self._current_translation)
-
- def apply_styling(self, text_color: str, bg_color: str, text_size: int):
- """Apply text color, background color and font size"""
-
- # Load custom CSS if it exists
- css_file_path = self.get_css_file_path()
-
- if os.path.exists(css_file_path):
- try:
- with open(css_file_path, "r", encoding="utf-8") as f:
- self.window_style = f.read()
- except Exception as e:
- logging.warning(f"Failed to load custom CSS: {e}")
- else:
- self.window_style = f"""
- body {{
- color: {text_color};
- background-color: {bg_color};
- font-size: {text_size}pt;
- font-family: Arial, sans-serif;
- padding: 0;
- margin: 20px;
- }}
- """
-
- def update_transcripts(self, text: str):
- """Update the transcript display with new text"""
- if not text:
- return
-
- self._current_transcript = text
- escaped_text = text.replace("&", "&").replace("<", "<").replace(">", ">")
- html_text = escaped_text.replace("\n", " ")
-
- html_content = f"""
-
-
-
-
-
- {html_text}
-
-
- """
-
- self.transcript_display.setHtml(html_content)
- self.transcript_display.moveCursor(QTextCursor.MoveOperation.End)
-
- def update_translations(self, text: str):
- """Update the translation display with new text"""
- if not text:
- return
-
- self._current_translation = text
- self.translation_display.show()
-
- escaped_text = text.replace("&", "&").replace("<", "<").replace(">", ">")
- html_text = escaped_text.replace("\n", " ")
-
- html_content = f"""
-
-
-
-
-
- {html_text}
-
-
- """
-
- self.translation_display.setHtml(html_content)
- self.translation_display.moveCursor(QTextCursor.MoveOperation.End)
-
- def toggle_fullscreen(self):
- """Toggle fullscreen mode"""
- if self.isFullScreen():
- self.showNormal()
- else:
- self.showFullScreen()
-
- def keyPressEvent(self, event):
- """Handle keyboard events"""
- # ESC Key exits fullscreen
- if event.key() == Qt.Key.Key_Escape and self.isFullScreen():
- self.showNormal()
- event.accept()
- else:
- super().keyPressEvent(event)
-
-
- def get_css_file_path(self) -> str:
- """Get path to custom CSS file"""
- cache_dir = user_cache_dir("Buzz")
- os.makedirs(cache_dir, exist_ok=True)
-
- return os.path.join(cache_dir, "presentation_window_style.css")
-
-
diff --git a/buzz/widgets/record_button.py b/buzz/widgets/record_button.py
deleted file mode 100644
index a1b153ea..00000000
--- a/buzz/widgets/record_button.py
+++ /dev/null
@@ -1,22 +0,0 @@
-from typing import Optional
-
-from PyQt6.QtWidgets import QPushButton, QWidget, QSizePolicy
-
-from buzz.locale import _
-
-
-class RecordButton(QPushButton):
- def __init__(self, parent: Optional[QWidget]) -> None:
- super().__init__(_("Record"), parent)
- self.setDefault(True)
- self.setSizePolicy(
- QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
- )
-
- def set_stopped(self):
- self.setText(_("Record"))
- self.setDefault(True)
-
- def set_recording(self):
- self.setText(_("Stop"))
- self.setDefault(False)
diff --git a/buzz/widgets/record_delegate.py b/buzz/widgets/record_delegate.py
deleted file mode 100644
index 4a021dd5..00000000
--- a/buzz/widgets/record_delegate.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from typing import Callable
-
-from PyQt6.QtSql import QSqlRecord, QSqlTableModel
-from PyQt6.QtWidgets import QStyledItemDelegate
-
-
-class RecordDelegate(QStyledItemDelegate):
- def __init__(self, text_getter: Callable[[QSqlRecord], str]):
- super().__init__()
- self.callback = text_getter
-
- def initStyleOption(self, option, index):
- super().initStyleOption(option, index)
- model: QSqlTableModel = index.model()
- option.text = self.callback(model.record(index.row()))
diff --git a/buzz/widgets/recording_transcriber_widget.py b/buzz/widgets/recording_transcriber_widget.py
deleted file mode 100644
index d0433631..00000000
--- a/buzz/widgets/recording_transcriber_widget.py
+++ /dev/null
@@ -1,1210 +0,0 @@
-import csv
-import io
-import os
-import re
-import enum
-import time
-import requests
-import logging
-import datetime
-import sounddevice
-from enum import auto
-from typing import Optional, Tuple, Any
-
-from PyQt6.QtCore import QThread, Qt, QThreadPool, QTimer, pyqtSignal
-from PyQt6.QtGui import QTextCursor, QCloseEvent, QColor
-from PyQt6.QtWidgets import (
- QWidget,
- QVBoxLayout,
- QFormLayout,
- QHBoxLayout,
- QMessageBox,
- QApplication,
- QPushButton,
- QComboBox,
- QLabel,
- QSpinBox,
- QColorDialog
-)
-
-from buzz.dialogs import show_model_download_error_dialog
-from buzz.locale import _
-from buzz.model_loader import (
- ModelDownloader,
- TranscriptionModel,
- ModelType,
- WhisperModelSize
-)
-from buzz.store.keyring_store import get_password, Key
-from buzz.recording import RecordingAmplitudeListener
-from buzz.settings.settings import Settings
-from buzz.settings.recording_transcriber_mode import RecordingTranscriberMode
-from buzz.transcriber.recording_transcriber import RecordingTranscriber
-from buzz.transcriber.transcriber import (
- TranscriptionOptions,
- Task,
-)
-from buzz.translator import Translator
-from buzz.widgets.audio_devices_combo_box import AudioDevicesComboBox
-from buzz.widgets.audio_meter_widget import AudioMeterWidget
-from buzz.widgets.model_download_progress_dialog import ModelDownloadProgressDialog
-from buzz.widgets.record_button import RecordButton
-from buzz.widgets.text_display_box import TextDisplayBox
-from buzz.widgets.transcriber.transcription_options_group_box import (
- TranscriptionOptionsGroupBox,
-)
-from buzz.widgets.presentation_window import PresentationWindow
-from buzz.widgets.icon import NewWindowIcon, FullscreenIcon, ColorBackgroundIcon, TextColorIcon
-
-REAL_CHARS_REGEX = re.compile(r'\w')
-NO_SPACE_BETWEEN_SENTENCES = re.compile(r'([.!?。!?])([A-Z])')
-
-
-class RecordingTranscriberWidget(QWidget):
- current_status: "RecordingStatus"
- transcription_options: TranscriptionOptions
- selected_device_id: Optional[int]
- model_download_progress_dialog: Optional[ModelDownloadProgressDialog] = None
- transcriber: Optional[RecordingTranscriber] = None
- model_loader: Optional[ModelDownloader] = None
- transcription_thread: Optional[QThread] = None
- recording_amplitude_listener: Optional[RecordingAmplitudeListener] = None
- device_sample_rate: Optional[int] = None
-
- transcription_stopped = pyqtSignal()
-
- class RecordingStatus(enum.Enum):
- STOPPED = auto()
- RECORDING = auto()
-
- def __init__(
- self,
- parent: Optional[QWidget] = None,
- flags: Optional[Qt.WindowType] = None,
- custom_sounddevice: Optional[Any] = None,
- ) -> None:
- super().__init__(parent)
- self.sounddevice = custom_sounddevice or sounddevice
-
- self.upload_url = os.getenv("BUZZ_UPLOAD_URL", "")
-
- if flags is not None:
- self.setWindowFlags(flags)
-
- layout = QVBoxLayout(self)
-
- self.translation_thread = None
- self.translator = None
- self.transcripts = []
- self.translations = []
- self.current_status = self.RecordingStatus.STOPPED
- self.setWindowTitle(_("Live Recording"))
-
- self.settings = Settings()
- self.transcriber_mode = list(RecordingTranscriberMode)[
- self.settings.value(key=Settings.Key.RECORDING_TRANSCRIBER_MODE, default_value=0)]
-
- default_language = self.settings.value(
- key=Settings.Key.RECORDING_TRANSCRIBER_LANGUAGE, default_value=""
- )
-
- model_types = [
- model_type
- for model_type in ModelType
- if model_type.is_available()
- ]
- default_model: Optional[TranscriptionModel] = None
- if len(model_types) > 0:
- default_model = TranscriptionModel(model_type=model_types[0])
-
- selected_model = self.settings.value(
- key=Settings.Key.RECORDING_TRANSCRIBER_MODEL,
- default_value=default_model,
- )
-
- if selected_model is None or selected_model.model_type not in model_types:
- selected_model = default_model
-
- openai_access_token = get_password(key=Key.OPENAI_API_KEY)
-
- self.transcription_options = TranscriptionOptions(
- model=selected_model,
- task=self.settings.value(
- key=Settings.Key.RECORDING_TRANSCRIBER_TASK,
- default_value=Task.TRANSCRIBE,
- ),
- language=default_language if default_language != "" else None,
- openai_access_token=openai_access_token,
- initial_prompt=self.settings.value(
- key=Settings.Key.RECORDING_TRANSCRIBER_INITIAL_PROMPT, default_value=""
- ),
- word_level_timings=False,
- enable_llm_translation=self.settings.value(
- key=Settings.Key.RECORDING_TRANSCRIBER_ENABLE_LLM_TRANSLATION,
- default_value=False,
- ),
- llm_model=self.settings.value(
- key=Settings.Key.RECORDING_TRANSCRIBER_LLM_MODEL, default_value=""
- ),
- llm_prompt=self.settings.value(
- key=Settings.Key.RECORDING_TRANSCRIBER_LLM_PROMPT, default_value=""
- ),
- silence_threshold=self.settings.value(
- key=Settings.Key.RECORDING_TRANSCRIBER_SILENCE_THRESHOLD,
- default_value=0.0025,
- ),
- line_separator=self.settings.value(
- key=Settings.Key.RECORDING_TRANSCRIBER_LINE_SEPARATOR,
- default_value="\n\n",
- ),
- transcription_step=self.settings.value(
- key=Settings.Key.RECORDING_TRANSCRIBER_TRANSCRIPTION_STEP,
- default_value=3.5,
- ),
- )
-
- self.audio_devices_combo_box = AudioDevicesComboBox(self)
- self.audio_devices_combo_box.device_changed.connect(self.on_device_changed)
- self.selected_device_id = self.audio_devices_combo_box.get_default_device_id()
-
- self.record_button = RecordButton(self)
- self.record_button.clicked.connect(self.on_record_button_clicked)
- self.reset_transcriber_controls()
-
- self.transcription_text_box = TextDisplayBox(self)
- self.transcription_text_box.setPlaceholderText(_("Click Record to begin..."))
-
- self.translation_text_box = TextDisplayBox(self)
- self.translation_text_box.setPlaceholderText(_("Waiting for AI translation..."))
-
- self.transcription_options_group_box = TranscriptionOptionsGroupBox(
- default_transcription_options=self.transcription_options,
- model_types=model_types,
- parent=self,
- show_recording_settings=True,
- )
- self.transcription_options_group_box.transcription_options_changed.connect(
- self.on_transcription_options_changed
- )
- self.transcription_options_group_box.advanced_settings_dialog.recording_mode_changed.connect(
- self.on_recording_mode_changed
- )
- self.transcription_options_group_box.advanced_settings_dialog.hide_unconfirmed_changed.connect(
- self.on_hide_unconfirmed_changed
- )
-
- recording_options_layout = QFormLayout()
- self.microphone_label = QLabel(_("Microphone:"))
- recording_options_layout.addRow(self.microphone_label, self.audio_devices_combo_box)
-
- self.audio_meter_widget = AudioMeterWidget(self)
-
- record_button_layout = QHBoxLayout()
- record_button_layout.setContentsMargins(0, 4, 0, 8)
- record_button_layout.addWidget(self.audio_meter_widget, alignment=Qt.AlignmentFlag.AlignVCenter)
- record_button_layout.addWidget(self.record_button)
-
- layout.addWidget(self.transcription_options_group_box)
- layout.addLayout(recording_options_layout)
- layout.addLayout(record_button_layout)
- layout.addWidget(self.transcription_text_box)
- layout.addWidget(self.translation_text_box)
-
- if not self.transcription_options.enable_llm_translation:
- self.translation_text_box.hide()
-
- self.setLayout(layout)
- self.resize(700, 600)
-
- self.reset_recording_amplitude_listener()
-
- self._closing = False
- self.transcript_export_file = None
- self.translation_export_file = None
- self.export_file_type = "txt"
- self.export_max_entries = 0
- self.hide_unconfirmed = self.settings.value(
- Settings.Key.RECORDING_TRANSCRIBER_HIDE_UNCONFIRMED, True
- )
- self.export_enabled = self.settings.value(
- key=Settings.Key.RECORDING_TRANSCRIBER_EXPORT_ENABLED,
- default_value=False,
- )
-
- #Presentation window
- self.presentation_window: Optional[PresentationWindow] = None
-
- self.presentation_options_bar = self.create_presentation_options_bar()
- layout.insertWidget(3, self.presentation_options_bar)
- self.presentation_options_bar.hide()
- self.copy_actions_bar = self.create_copy_actions_bar()
- layout.addWidget(self.copy_actions_bar) # Add at the bottom
- self.copy_actions_bar.hide()
-
- def create_presentation_options_bar(self) -> QWidget:
- """Crete the presentation options bar widget"""
-
- bar = QWidget(self)
- layout = QHBoxLayout(bar)
- layout.setContentsMargins(5, 5, 5, 5)
- layout.setSpacing(10)
-
- self.show_presentation_button = QPushButton(bar)
- self.show_presentation_button.setIcon(NewWindowIcon(bar))
- self.show_presentation_button.setToolTip(_("Show in new window"))
- self.show_presentation_button.clicked.connect(self.on_show_presentation_clicked)
- layout.addWidget(self.show_presentation_button)
-
- layout.addStretch() #Push other controls to the right
-
- text_size_label = QLabel(_("Text Size:"), bar)
- layout.addWidget(text_size_label)
-
- self.text_size_spinbox = QSpinBox(bar)
- self.text_size_spinbox.setRange(10, 100) #10pt to 100pt
-
- saved_text_size = self.settings.value(
- Settings.Key.PRESENTATION_WINDOW_TEXT_SIZE,
- 24,
- int
- )
- self.text_size_spinbox.setValue(saved_text_size)
- self.text_size_spinbox.valueChanged.connect(self.on_text_size_changed)
- layout.addWidget(self.text_size_spinbox)
-
- #Theme selector
- theme_label = QLabel(_("Theme"), bar)
- layout.addWidget(theme_label)
-
- self.theme_combo = QComboBox(bar)
- self.theme_combo.addItems([_("Light"), _("Dark"), _("Custom")])
- #Load saved theme
- saved_theme = self.settings.value(
- Settings.Key.PRESENTATION_WINDOW_THEME,
- "light"
- )
- theme_index = {"light": 0, "dark": 1, "custom": 2}.get(saved_theme, 0)
- self.theme_combo.setCurrentIndex(theme_index)
- self.theme_combo.currentIndexChanged.connect(self.on_theme_changed)
- layout.addWidget(self.theme_combo)
-
- #Color buttons hidden first, show when custom is selected
- self.text_color_button = QPushButton(bar)
- self.text_color_button.setIcon(TextColorIcon(bar))
- self.text_color_button.setToolTip(_("Text Color"))
- self.text_color_button.clicked.connect(self.on_text_color_clicked)
- self.text_color_button.hide()
-
- if saved_theme == "custom":
- self.text_color_button.show()
- layout.addWidget(self.text_color_button)
-
- self.bg_color_button = QPushButton(bar)
- self.bg_color_button.setIcon(ColorBackgroundIcon(bar))
- self.bg_color_button.setToolTip(_("Background Color"))
- self.bg_color_button.clicked.connect(self.on_bg_color_clicked)
- self.bg_color_button.hide()
- if saved_theme == "custom":
- self.bg_color_button.show()
- layout.addWidget(self.bg_color_button)
-
- self.fullscreen_button = QPushButton(bar)
- self.fullscreen_button.setIcon(FullscreenIcon(bar))
- self.fullscreen_button.setToolTip(_("Fullscreen"))
- self.fullscreen_button.clicked.connect(self.on_fullscreen_clicked)
- self.fullscreen_button.setEnabled(False)
- layout.addWidget(self.fullscreen_button)
-
- return bar
-
- def create_copy_actions_bar(self) -> QWidget:
- """Create the copy actions bar widget"""
- bar = QWidget(self)
- layout = QHBoxLayout(bar)
- layout.setContentsMargins(5, 5, 5, 5)
- layout.setSpacing(10)
-
- layout.addStretch() # Push button to the right
-
- self.copy_transcript_button = QPushButton(_("Copy"), bar)
- self.copy_transcript_button.setToolTip(_("Copy transcription to clipboard"))
- self.copy_transcript_button.clicked.connect(self.on_copy_transcript_clicked)
- layout.addWidget(self.copy_transcript_button)
-
- return bar
-
- def on_copy_transcript_clicked(self):
- """Handle copy transcript button click"""
- transcript_text = self.transcription_text_box.toPlainText().strip()
-
- if not transcript_text:
- self.copy_transcript_button.setText(_("Nothing to copy!"))
- QTimer.singleShot(1500, lambda: self.copy_transcript_button.setText(_("Copy")))
- return
-
- app = QApplication.instance()
- if app is None:
- logging.warning("QApplication instance not available; clipboard disabled")
- self.copy_transcript_button.setText(_("Copy failed"))
- QTimer.singleShot(1500, lambda: self.copy_transcript_button.setText(_("Copy")))
- return
-
- clipboard = app.clipboard()
- if clipboard is None:
- logging.warning("Clipboard not available")
- self.copy_transcript_button.setText(_("Copy failed"))
- QTimer.singleShot(1500, lambda: self.copy_transcript_button.setText(_("Copy")))
- return
-
- try:
- clipboard.setText(transcript_text)
- except Exception as e:
- logging.warning("Clipboard error: %s", e)
- self.copy_transcript_button.setText(_("Copy failed"))
- QTimer.singleShot(1500, lambda: self.copy_transcript_button.setText(_("Copy")))
- return
-
- self.copy_transcript_button.setText(_("Copied!"))
- QTimer.singleShot(2000, lambda: self.copy_transcript_button.setText(_("Copy")))
-
- def on_show_presentation_clicked(self):
- """Handle click on 'Show in new window' button"""
- if self.presentation_window is None or not self.presentation_window.isVisible():
- #Create new presentation window
- self.presentation_window = PresentationWindow(self)
- self.presentation_window.show()
-
- #Enable fullscreen button
- self.fullscreen_button.setEnabled(True)
-
- #Sync current content to presentation window
- transcript_text = self.transcription_text_box.toPlainText()
- if transcript_text:
- self.presentation_window.update_transcripts(transcript_text)
-
- if self.transcription_options.enable_llm_translation:
- translation_text = self.translation_text_box.toPlainText()
- if translation_text:
- self.presentation_window.update_translations(translation_text)
- else:
- #Window already open, bring to front
- self.presentation_window.raise_()
- self.presentation_window.activateWindow()
-
- def on_text_size_changed(self, value: int):
- """Handle text size change"""
- def save_settings():
- self.settings.set_value(Settings.Key.PRESENTATION_WINDOW_TEXT_SIZE, value)
- if self.presentation_window:
- # reload setting to apply new size
- self.presentation_window.load_settings()
- #Incase user drags slider, Debounce by waiting 100ms before saving
- QTimer.singleShot(100, save_settings)
-
- def on_theme_changed(self, index: int):
- """Handle theme selection change"""
- theme = ["light", "dark", "custom"]
- selected_theme = theme[index]
- self.settings.set_value(Settings.Key.PRESENTATION_WINDOW_THEME, selected_theme)
-
- #Show/hide color buttons based on selection
- if selected_theme == "custom":
- self.text_color_button.show()
- self.bg_color_button.show()
- else:
- self.text_color_button.hide()
- self.bg_color_button.hide()
-
- # Apply theme to presentation window
- if self.presentation_window:
- self.presentation_window.load_settings()
-
- def on_text_color_clicked(self):
- """Handle text color button click"""
-
- current_color = QColor(
- self.settings.value(
- Settings.Key.PRESENTATION_WINDOW_TEXT_COLOR,
- "#000000"
- )
- )
-
- color = QColorDialog.getColor(current_color, self, _("Select Text Color"))
- if color.isValid():
- color_hex = color.name()
- self.settings.set_value(Settings.Key.PRESENTATION_WINDOW_TEXT_COLOR, color_hex)
- if self.presentation_window:
- self.presentation_window.load_settings()
-
- def on_bg_color_clicked(self):
- """Handle background color button click"""
-
- current_color = QColor(
- self.settings.value(
- Settings.Key.PRESENTATION_WINDOW_BACKGROUND_COLOR,
- "#FFFFFF"
- )
- )
-
- color = QColorDialog.getColor(current_color, self, _("Select Background Color"))
- if color.isValid():
- color_hex = color.name()
- self.settings.set_value(Settings.Key.PRESENTATION_WINDOW_BACKGROUND_COLOR, color_hex)
- if self.presentation_window:
- self.presentation_window.load_settings()
-
- def on_fullscreen_clicked(self):
- """Handle fullscreen button click"""
- if self.presentation_window:
- self.presentation_window.toggle_fullscreen()
-
- def setup_for_export(self):
- export_folder = self.settings.value(
- key=Settings.Key.RECORDING_TRANSCRIBER_EXPORT_FOLDER,
- default_value="",
- )
-
- date_time_now = datetime.datetime.now().strftime("%d-%b-%Y %H-%M-%S")
-
- custom_template = self.settings.value(
- key=Settings.Key.RECORDING_TRANSCRIBER_EXPORT_FILE_NAME,
- default_value="",
- )
- export_file_name_template = custom_template if custom_template else Settings().get_default_export_file_template()
-
- self.export_file_type = self.settings.value(
- key=Settings.Key.RECORDING_TRANSCRIBER_EXPORT_FILE_TYPE,
- default_value="txt",
- )
- self.export_max_entries = self.settings.value(
- Settings.Key.RECORDING_TRANSCRIBER_EXPORT_MAX_ENTRIES, 0, int
- )
- self.hide_unconfirmed = self.settings.value(
- Settings.Key.RECORDING_TRANSCRIBER_HIDE_UNCONFIRMED, True
- )
- ext = ".csv" if self.export_file_type == "csv" else ".txt"
-
- export_file_name = (
- export_file_name_template.replace("{{ input_file_name }}", "live recording")
- .replace("{{ task }}", self.transcription_options.task.value)
- .replace("{{ language }}", self.transcription_options.language or "")
- .replace("{{ model_type }}", self.transcription_options.model.model_type.value)
- .replace("{{ model_size }}", self.transcription_options.model.whisper_model_size or "")
- .replace("{{ date_time }}", date_time_now)
- + ext
- )
-
- translated_ext = ".translated" + ext
-
- if not os.path.isdir(export_folder):
- self.export_enabled = False
-
- self.transcript_export_file = os.path.join(export_folder, export_file_name)
- self.translation_export_file = self.transcript_export_file.replace(ext, translated_ext)
-
- # Clear export files at the start of each recording session
- for path in (self.transcript_export_file, self.translation_export_file):
- if os.path.isfile(path):
- self.write_to_export_file(path, "", mode="w")
-
- def on_recording_mode_changed(self, mode: RecordingTranscriberMode):
- self.transcriber_mode = mode
-
- def on_hide_unconfirmed_changed(self, value: bool):
- self.hide_unconfirmed = value
-
- def on_transcription_options_changed(
- self, transcription_options: TranscriptionOptions
- ):
- self.transcription_options = transcription_options
-
- if self.transcription_options.enable_llm_translation:
- self.translation_text_box.show()
- else:
- self.translation_text_box.hide()
-
- self.reset_transcriber_controls()
-
- def reset_transcriber_controls(self):
- button_enabled = True
- if (self.transcription_options.model.model_type == ModelType.FASTER_WHISPER
- and self.transcription_options.model.whisper_model_size == WhisperModelSize.CUSTOM
- and self.transcription_options.model.hugging_face_model_id == ""):
- button_enabled = False
-
- if (self.transcription_options.model.model_type == ModelType.HUGGING_FACE
- and self.transcription_options.model.hugging_face_model_id == ""):
- button_enabled = False
-
- self.record_button.setEnabled(button_enabled)
-
- def on_device_changed(self, device_id: int):
- self.selected_device_id = device_id
- self.reset_recording_amplitude_listener()
-
- def reset_recording_amplitude_listener(self):
- if self.recording_amplitude_listener is not None:
- self.recording_amplitude_listener.stop_recording()
-
- # Listening to audio will fail if there are no input devices
- if self.selected_device_id is None or self.selected_device_id == -1:
- return
-
- # Get the device sample rate before starting the listener as the PortAudio
- # function # fails if you try to get the device's settings while recording
- # is in progress.
- self.device_sample_rate = RecordingTranscriber.get_device_sample_rate(
- self.selected_device_id
- )
- logging.debug(f"Device sample rate: {self.device_sample_rate}")
-
- self.recording_amplitude_listener = RecordingAmplitudeListener(
- input_device_index=self.selected_device_id, parent=self
- )
- self.recording_amplitude_listener.amplitude_changed.connect(
- self.on_recording_amplitude_changed, Qt.ConnectionType.QueuedConnection
- )
- self.recording_amplitude_listener.average_amplitude_changed.connect(
- self.audio_meter_widget.update_average_amplitude, Qt.ConnectionType.QueuedConnection
- )
- self.recording_amplitude_listener.start_recording()
-
- def on_record_button_clicked(self):
- if self.current_status == self.RecordingStatus.STOPPED:
- # Stop amplitude listener and disconnect its signal before resetting
- # to prevent queued amplitude events from overriding the reset
- if self.recording_amplitude_listener is not None:
- self.recording_amplitude_listener.amplitude_changed.disconnect(
- self.on_recording_amplitude_changed
- )
- self.recording_amplitude_listener.average_amplitude_changed.disconnect(
- self.audio_meter_widget.update_average_amplitude
- )
- self.recording_amplitude_listener.stop_recording()
- self.recording_amplitude_listener = None
- self.audio_meter_widget.reset_amplitude()
- self.start_recording()
- self.current_status = self.RecordingStatus.RECORDING
- self.record_button.set_recording()
- self.transcription_options_group_box.setEnabled(False)
- self.audio_devices_combo_box.setEnabled(False)
- self.microphone_label.setEnabled(False)
- self.presentation_options_bar.show()
- self.copy_actions_bar.hide()
-
- else: # RecordingStatus.RECORDING
- self.stop_recording()
- self.set_recording_status_stopped()
- self.presentation_options_bar.hide()
-
- def start_recording(self):
- self.record_button.setDisabled(True)
- self.transcripts = []
- self.translations = []
-
- self.transcription_text_box.clear()
- self.translation_text_box.clear()
-
- if self.export_enabled:
- self.setup_for_export()
-
- model_path = self.transcription_options.model.get_local_model_path()
- if model_path is not None:
- self.on_model_loaded(model_path)
- return
-
- self.model_loader = ModelDownloader(model=self.transcription_options.model)
- self.model_loader.signals.progress.connect(self.on_download_model_progress)
- self.model_loader.signals.error.connect(self.on_download_model_error)
- self.model_loader.signals.finished.connect(self.on_model_loaded)
- QThreadPool().globalInstance().start(self.model_loader)
-
- def on_model_loaded(self, model_path: str):
- self.reset_recording_controls()
- self.model_loader = None
-
- 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()
-
- self.transcriber = RecordingTranscriber(
- input_device_index=self.selected_device_id,
- sample_rate=self.device_sample_rate,
- transcription_options=self.transcription_options,
- model_path=model_path,
- sounddevice=self.sounddevice,
- )
-
- self.transcriber.moveToThread(self.transcription_thread)
-
- self.transcription_thread.started.connect(self.transcriber.start)
- self.transcription_thread.finished.connect(
- self.transcription_thread.deleteLater
- )
-
- self.transcriber.transcription.connect(self.on_next_transcription)
- self.transcriber.amplitude_changed.connect(
- self.on_recording_amplitude_changed, Qt.ConnectionType.QueuedConnection
- )
- self.transcriber.average_amplitude_changed.connect(
- self.audio_meter_widget.update_average_amplitude, Qt.ConnectionType.QueuedConnection
- )
- self.transcriber.queue_size_changed.connect(
- self.audio_meter_widget.update_queue_size, Qt.ConnectionType.QueuedConnection
- )
-
- # Stop the separate amplitude listener to avoid two streams on the same device
- if self.recording_amplitude_listener is not None:
- self.recording_amplitude_listener.stop_recording()
-
- self.transcriber.finished.connect(self.on_transcriber_finished)
- self.transcriber.finished.connect(self.transcription_thread.quit)
- self.transcriber.finished.connect(self.transcriber.deleteLater)
-
- self.transcriber.error.connect(self.on_transcriber_error)
- self.transcriber.error.connect(self.transcription_thread.quit)
- self.transcriber.error.connect(self.transcriber.deleteLater)
-
- if self.transcription_options.enable_llm_translation:
- self.translation_thread = QThread()
-
- self.translator = Translator(
- self.transcription_options,
- self.transcription_options_group_box.advanced_settings_dialog,
- )
-
- self.translator.moveToThread(self.translation_thread)
-
- self.translation_thread.started.connect(self.translator.start)
- self.translation_thread.finished.connect(
- self.translation_thread.deleteLater
- )
- self.translation_thread.finished.connect(
- lambda: setattr(self, "translation_thread", None)
- )
-
- self.translator.finished.connect(self.translation_thread.quit)
- self.translator.finished.connect(self.translator.deleteLater)
- self.translator.finished.connect(
- lambda: setattr(self, "translator", None)
- )
-
- self.translator.translation.connect(self.on_next_translation)
-
- self.translation_thread.start()
-
- self.transcription_thread.start()
-
- def on_download_model_progress(self, progress: Tuple[float, float]):
- (current_size, total_size) = progress
-
- if self.model_download_progress_dialog is None:
- self.model_download_progress_dialog = ModelDownloadProgressDialog(
- model_type=self.transcription_options.model.model_type, parent=self
- )
- self.model_download_progress_dialog.canceled.connect(
- self.on_cancel_model_progress_dialog
- )
-
- if self.model_download_progress_dialog is not None and total_size > 0:
- self.model_download_progress_dialog.set_value(
- fraction_completed=current_size / total_size
- )
-
- def set_recording_status_stopped(self):
- self.record_button.set_stopped()
- self.current_status = self.RecordingStatus.STOPPED
- self.transcription_options_group_box.setEnabled(True)
- self.audio_devices_combo_box.setEnabled(True)
- self.microphone_label.setEnabled(True)
- self.presentation_options_bar.hide()
- self.copy_actions_bar.show() #added this here
-
- def on_download_model_error(self, error: str):
- self.reset_model_download()
- show_model_download_error_dialog(self, error)
- self.stop_recording()
- self.set_recording_status_stopped()
- self.reset_recording_amplitude_listener()
- self.record_button.setDisabled(False)
-
- @staticmethod
- def strip_newlines(text):
- return text.replace('\r\n', os.linesep).replace('\n', os.linesep)
-
- @staticmethod
- def filter_text(text: str):
- text = text.strip()
-
- if not REAL_CHARS_REGEX.search(text):
- return ""
-
- return text
-
- @staticmethod
- def write_to_export_file(file_path: str, content: str, mode: str = "a", retries: int = 5, delay: float = 0.2):
- """Write to an export file with retry logic for Windows file locking."""
- for attempt in range(retries):
- try:
- with open(file_path, mode, encoding='utf-8') as f:
- f.write(content)
- return
- except PermissionError:
- if attempt < retries - 1:
- time.sleep(delay)
- else:
- logging.warning("Export write failed after %d retries: %s", retries, file_path)
- except OSError as e:
- logging.warning("Export write failed: %s", e)
- return
-
- @staticmethod
- def write_csv_export(file_path: str, text: str, max_entries: int):
- """Append a new column to a single-row CSV export file, applying max_entries limit."""
- existing_columns = []
- if os.path.isfile(file_path):
- try:
- with open(file_path, "r", encoding="utf-8-sig") as f:
- raw = f.read()
- if raw.strip():
- reader = csv.reader(io.StringIO(raw))
- for row in reader:
- existing_columns = row
- break
- except OSError:
- pass
- existing_columns.append(text)
- if max_entries > 0:
- existing_columns = existing_columns[-max_entries:]
- buf = io.StringIO()
- writer = csv.writer(buf)
- writer.writerow(existing_columns)
- for attempt in range(5):
- try:
- with open(file_path, "w", encoding='utf-8-sig') as f:
- f.write(buf.getvalue())
- return
- except PermissionError:
- if attempt < 4:
- time.sleep(0.2)
- else:
- logging.warning("CSV export write failed after retries: %s", file_path)
- except OSError as e:
- logging.warning("CSV export write failed: %s", e)
- return
-
- @staticmethod
- def write_txt_export(file_path: str, text: str, mode: str, max_entries: int, line_separator: str):
- """Write to a TXT export file, applying max_entries limit when needed."""
- if mode == "a":
- RecordingTranscriberWidget.write_to_export_file(file_path, text + line_separator)
- if max_entries > 0 and os.path.isfile(file_path):
- raw = RecordingTranscriberWidget.read_export_file(file_path)
- parts = [p for p in raw.split(line_separator) if p]
- if len(parts) > max_entries:
- parts = parts[-max_entries:]
- RecordingTranscriberWidget.write_to_export_file(
- file_path, line_separator.join(parts) + line_separator, mode="w"
- )
- elif mode == "prepend":
- existing_content = ""
- if os.path.isfile(file_path):
- existing_content = RecordingTranscriberWidget.read_export_file(file_path)
- new_content = text + line_separator + existing_content
- if max_entries > 0:
- parts = [p for p in new_content.split(line_separator) if p]
- if len(parts) > max_entries:
- parts = parts[:max_entries]
- new_content = line_separator.join(parts) + line_separator
- RecordingTranscriberWidget.write_to_export_file(file_path, new_content, mode="w")
- else:
- RecordingTranscriberWidget.write_to_export_file(file_path, text, mode=mode)
-
- @staticmethod
- def read_export_file(file_path: str, retries: int = 5, delay: float = 0.2) -> str:
- """Read an export file with retry logic for Windows file locking."""
- for attempt in range(retries):
- try:
- with open(file_path, "r", encoding='utf-8') as f:
- return f.read()
- except PermissionError:
- if attempt < retries - 1:
- time.sleep(delay)
- else:
- logging.warning("Export read failed after %d retries: %s", retries, file_path)
- except OSError as e:
- logging.warning("Export read failed: %s", e)
- return ""
- return ""
-
- # Copilot magic implementation of a sliding window approach to find the longest common substring between two texts,
- # ignoring the initial differences.
- @staticmethod
- def find_common_part(text1: str, text2: str) -> str:
- len1, len2 = len(text1), len(text2)
- max_len = 0
- end_index = 0
-
- lcsuff = [[0] * (len2 + 1) for _ in range(len1 + 1)]
-
- for i in range(1, len1 + 1):
- for j in range(1, len2 + 1):
- if text1[i - 1] == text2[j - 1]:
- lcsuff[i][j] = lcsuff[i - 1][j - 1] + 1
- if lcsuff[i][j] > max_len:
- max_len = lcsuff[i][j]
- end_index = i
- else:
- lcsuff[i][j] = 0
-
- common_part = text1[end_index - max_len:end_index]
-
- return common_part if len(common_part) >= 5 else ""
-
- @staticmethod
- def merge_text_no_overlap(text1: str, text2: str) -> str:
- overlap_start = 0
- for i in range(1, min(len(text1), len(text2)) + 1):
- if text1[-i:] == text2[:i]:
- overlap_start = i
-
- return text1 + text2[overlap_start:]
-
- def process_transcription_merge(self, text: str, texts, text_box, export_file):
- texts.append(text)
-
- # Possibly in future we want to tie this to some setting, to limit amount of data that needs
- # to be processed and exported. Value should not be less than ~10, so we have enough data to
- # work with.
- # if len(texts) > 20:
- # del texts[:len(texts) - 20]
-
- # Remove possibly errorous parts from overlapping audio chunks
- last_common_length = None
- for i in range(len(texts) - 1):
- common_part = self.find_common_part(texts[i], texts[i + 1])
- if common_part:
- common_length = len(common_part)
- texts[i] = texts[i][:texts[i].rfind(common_part) + common_length]
- texts[i + 1] = texts[i + 1][texts[i + 1].find(common_part):]
- if i == len(texts) - 2:
- last_common_length = common_length
- elif i == len(texts) - 2:
- last_common_length = None
-
- # When hiding unconfirmed: trim the last text to only the part confirmed by overlap
- # with the previous chunk. If no overlap found, drop the last text entirely.
- display_texts = list(texts)
- if self.hide_unconfirmed and len(display_texts) > 1:
- if last_common_length is not None:
- display_texts[-1] = display_texts[-1][:last_common_length]
- else:
- display_texts = display_texts[:-1]
-
- merged_texts = ""
- for text in display_texts:
- merged_texts = self.merge_text_no_overlap(merged_texts, text)
-
- merged_texts = NO_SPACE_BETWEEN_SENTENCES.sub(r'\1 \2', merged_texts)
-
- text_box.setPlainText(merged_texts)
- text_box.moveCursor(QTextCursor.MoveOperation.End)
-
- if self.export_enabled and export_file:
- if self.export_file_type == "csv":
- # For APPEND_AND_CORRECT mode, rewrite the whole CSV with all merged text as a single entry
- self.write_to_export_file(export_file, "", mode="w")
- self.write_csv_export(export_file, merged_texts, 0)
- else:
- self.write_to_export_file(export_file, merged_texts, mode="w")
-
- def on_next_transcription(self, text: str):
- text = self.filter_text(text)
-
- if len(text) == 0:
- return
-
- if self.translator is not None:
- self.translator.enqueue(text)
-
- if self.transcriber_mode == RecordingTranscriberMode.APPEND_BELOW:
- self.transcription_text_box.moveCursor(QTextCursor.MoveOperation.End)
- if len(self.transcription_text_box.toPlainText()) > 0:
- self.transcription_text_box.insertPlainText(self.transcription_options.line_separator)
- self.transcription_text_box.insertPlainText(text)
- self.transcription_text_box.moveCursor(QTextCursor.MoveOperation.End)
-
- if self.export_enabled and self.transcript_export_file:
- if self.export_file_type == "csv":
- self.write_csv_export(self.transcript_export_file, text, self.export_max_entries)
- else:
- self.write_txt_export(self.transcript_export_file, text, "a", self.export_max_entries, self.transcription_options.line_separator)
-
- elif self.transcriber_mode == RecordingTranscriberMode.APPEND_ABOVE:
- self.transcription_text_box.moveCursor(QTextCursor.MoveOperation.Start)
- self.transcription_text_box.insertPlainText(text)
- self.transcription_text_box.insertPlainText(self.transcription_options.line_separator)
- self.transcription_text_box.moveCursor(QTextCursor.MoveOperation.Start)
-
- if self.export_enabled and self.transcript_export_file:
- if self.export_file_type == "csv":
- # For APPEND_ABOVE, prepend in CSV means inserting at beginning of columns
- existing_columns = []
- if os.path.isfile(self.transcript_export_file):
- raw = self.read_export_file(self.transcript_export_file)
- if raw.strip():
- reader = csv.reader(io.StringIO(raw))
- for row in reader:
- existing_columns = row
- break
- new_columns = [text] + existing_columns
- if self.export_max_entries > 0:
- new_columns = new_columns[:self.export_max_entries]
- buf = io.StringIO()
- writer = csv.writer(buf)
- writer.writerow(new_columns)
- self.write_to_export_file(self.transcript_export_file, buf.getvalue(), mode="w")
- else:
- self.write_txt_export(self.transcript_export_file, text, "prepend", self.export_max_entries, self.transcription_options.line_separator)
-
- elif self.transcriber_mode == RecordingTranscriberMode.APPEND_AND_CORRECT:
- self.process_transcription_merge(text, self.transcripts, self.transcription_text_box, self.transcript_export_file)
-
- #Update presentation window if it is open
- if self.presentation_window and self.presentation_window.isVisible():
- #Get current merged text from the translation box
- current_text = self.transcription_text_box.toPlainText()
- self.presentation_window.update_transcripts(current_text)
-
- # Upload to server
- if self.upload_url:
- try:
- requests.post(
- url=self.upload_url,
- json={"kind": "transcript", "text": text},
- headers={'Content-Type': 'application/json'},
- timeout=15
- )
- except Exception as e:
- logging.error(f"Transcript upload failed: {str(e)}")
-
- def on_next_translation(self, text: str, _: Optional[int] = None):
- if len(text) == 0:
- return
-
- if self.transcriber_mode == RecordingTranscriberMode.APPEND_BELOW:
- self.translation_text_box.moveCursor(QTextCursor.MoveOperation.End)
- if len(self.translation_text_box.toPlainText()) > 0:
- self.translation_text_box.insertPlainText(self.transcription_options.line_separator)
- self.translation_text_box.insertPlainText(self.strip_newlines(text))
- self.translation_text_box.moveCursor(QTextCursor.MoveOperation.End)
-
- if self.export_enabled and self.translation_export_file:
- if self.export_file_type == "csv":
- self.write_csv_export(self.translation_export_file, text, self.export_max_entries)
- else:
- self.write_txt_export(self.translation_export_file, text, "a", self.export_max_entries, self.transcription_options.line_separator)
-
- elif self.transcriber_mode == RecordingTranscriberMode.APPEND_ABOVE:
- self.translation_text_box.moveCursor(QTextCursor.MoveOperation.Start)
- self.translation_text_box.insertPlainText(self.strip_newlines(text))
- self.translation_text_box.insertPlainText(self.transcription_options.line_separator)
- self.translation_text_box.moveCursor(QTextCursor.MoveOperation.Start)
-
- if self.export_enabled and self.translation_export_file:
- if self.export_file_type == "csv":
- existing_columns = []
- if os.path.isfile(self.translation_export_file):
- raw = self.read_export_file(self.translation_export_file)
- if raw.strip():
- reader = csv.reader(io.StringIO(raw))
- for row in reader:
- existing_columns = row
- break
- new_columns = [text] + existing_columns
- if self.export_max_entries > 0:
- new_columns = new_columns[:self.export_max_entries]
- buf = io.StringIO()
- writer = csv.writer(buf)
- writer.writerow(new_columns)
- self.write_to_export_file(self.translation_export_file, buf.getvalue(), mode="w")
- else:
- self.write_txt_export(self.translation_export_file, text, "prepend", self.export_max_entries, self.transcription_options.line_separator)
-
- elif self.transcriber_mode == RecordingTranscriberMode.APPEND_AND_CORRECT:
- self.process_transcription_merge(text, self.translations, self.translation_text_box, self.translation_export_file)
-
- if self.presentation_window and self.presentation_window.isVisible():
- current_translation = self.translation_text_box.toPlainText()
- self.presentation_window.update_translations(current_translation)
-
- # Upload to server
- if self.upload_url:
- try:
- requests.post(
- url=self.upload_url,
- json={"kind": "translation", "text": text},
- headers={'Content-Type': 'application/json'},
- timeout=15
- )
- except Exception as e:
- logging.error(f"Translation upload failed: {str(e)}")
-
- def stop_recording(self):
- if self.transcriber is not None:
- self.transcriber.stop_recording()
-
- if self.translator is not None:
- self.translator.stop()
-
- # Disable record button until the transcription is actually stopped in the background
- self.record_button.setDisabled(True)
-
- def on_transcriber_finished(self):
- self.reset_record_button()
- # Restart amplitude listener now that the transcription stream is closed
- self.reset_recording_amplitude_listener()
- self.transcription_stopped.emit()
-
- def on_transcriber_error(self, error: str):
- self.reset_record_button()
- self.set_recording_status_stopped()
- self.reset_recording_amplitude_listener()
- QMessageBox.critical(
- self,
- "",
- _("An error occurred while starting a new recording:")
- + error
- + ". "
- + _(
- "Please check your audio devices or check the application logs for more information."
- ),
- )
-
- def on_cancel_model_progress_dialog(self):
- if self.model_loader is not None:
- self.model_loader.cancel()
- self.reset_model_download()
- self.set_recording_status_stopped()
- self.reset_recording_amplitude_listener()
- self.record_button.setDisabled(False)
-
- def reset_model_download(self):
- if self.model_download_progress_dialog is not None:
- self.model_download_progress_dialog.canceled.disconnect(
- self.on_cancel_model_progress_dialog
- )
- self.model_download_progress_dialog.close()
- self.model_download_progress_dialog = None
-
- def reset_recording_controls(self):
- # Clear text box placeholder because the first chunk takes a while to process
- self.transcription_text_box.setPlaceholderText("")
- self.reset_record_button()
- self.reset_model_download()
-
- def reset_record_button(self):
- self.record_button.setEnabled(True)
-
- def on_recording_amplitude_changed(self, amplitude: float):
- self.audio_meter_widget.update_amplitude(amplitude)
-
- def closeEvent(self, event: QCloseEvent) -> None:
- if self._closing:
- # Second call after deferred close — proceed normally
- self._do_close()
- super().closeEvent(event)
- return
-
- if self.current_status == self.RecordingStatus.RECORDING:
- # Defer the close until the transcription thread finishes to avoid
- # blocking the GUI thread with a synchronous wait.
- event.ignore()
- self._closing = True
-
- if self.model_loader is not None:
- self.model_loader.cancel()
-
- self.stop_recording()
-
- # Connect to QThread.finished — the transcriber C++ object may already
- # be scheduled for deletion via deleteLater() by this point.
- thread = self.transcription_thread
- if thread is not None:
- try:
- if thread.isRunning():
- thread.finished.connect(self._on_close_transcriber_finished)
- else:
- self._on_close_transcriber_finished()
- except RuntimeError:
- self._on_close_transcriber_finished()
- else:
- self._on_close_transcriber_finished()
- return
-
- self._do_close()
- super().closeEvent(event)
-
- def _on_close_transcriber_finished(self):
- self.transcription_thread = None
- self.close()
-
- def _do_close(self):
- #Close presentation window if open
- if self.presentation_window:
- self.presentation_window.close()
- self.presentation_window = None
-
- if self.recording_amplitude_listener is not None:
- self.recording_amplitude_listener.stop_recording()
- self.recording_amplitude_listener.deleteLater()
- self.recording_amplitude_listener = None
-
- if self.translator is not None:
- self.translator.stop()
-
- if self.translation_thread is not None:
- # Just request quit — do not block the GUI thread waiting for it
- self.translation_thread.quit()
-
- self.settings.set_value(
- Settings.Key.RECORDING_TRANSCRIBER_LANGUAGE,
- self.transcription_options.language,
- )
- self.settings.set_value(
- Settings.Key.RECORDING_TRANSCRIBER_TASK, self.transcription_options.task
- )
- self.settings.set_value(
- Settings.Key.RECORDING_TRANSCRIBER_INITIAL_PROMPT,
- self.transcription_options.initial_prompt,
- )
- self.settings.set_value(
- Settings.Key.RECORDING_TRANSCRIBER_MODEL, self.transcription_options.model
- )
- self.settings.set_value(
- Settings.Key.RECORDING_TRANSCRIBER_ENABLE_LLM_TRANSLATION,
- self.transcription_options.enable_llm_translation,
- )
- self.settings.set_value(
- Settings.Key.RECORDING_TRANSCRIBER_LLM_MODEL,
- self.transcription_options.llm_model,
- )
- self.settings.set_value(
- Settings.Key.RECORDING_TRANSCRIBER_LLM_PROMPT,
- self.transcription_options.llm_prompt,
- )
- self.settings.set_value(
- Settings.Key.RECORDING_TRANSCRIBER_SILENCE_THRESHOLD,
- self.transcription_options.silence_threshold,
- )
- self.settings.set_value(
- Settings.Key.RECORDING_TRANSCRIBER_LINE_SEPARATOR,
- self.transcription_options.line_separator,
- )
- self.settings.set_value(
- Settings.Key.RECORDING_TRANSCRIBER_TRANSCRIPTION_STEP,
- self.transcription_options.transcription_step,
- )
diff --git a/buzz/widgets/sequence_edit.py b/buzz/widgets/sequence_edit.py
deleted file mode 100644
index 6af5799b..00000000
--- a/buzz/widgets/sequence_edit.py
+++ /dev/null
@@ -1,34 +0,0 @@
-import platform
-from typing import Optional
-
-from PyQt6 import QtGui
-from PyQt6.QtCore import Qt
-from PyQt6.QtWidgets import QKeySequenceEdit, QWidget
-
-
-class SequenceEdit(QKeySequenceEdit):
- def __init__(self, sequence: str, parent: Optional[QWidget] = None):
- super().__init__(sequence, parent)
- self.setClearButtonEnabled(True)
- if platform.system() == "Darwin":
- self.setStyleSheet("QLineEdit:focus { border: 2px solid #4d90fe; }")
-
- def keyPressEvent(self, event: QtGui.QKeyEvent) -> None:
- key = event.key()
- # The shortcut editor always focuses on the sequence edit widgets, so we need to
- # manually capture Esc key presses to close the dialog. The downside being that
- # the user can't set a shortcut that contains the Esc key.
- if key == Qt.Key.Key_Escape:
- self.parent().keyPressEvent(event)
- return
-
- # Ignore pressing *only* modifier keys
- if (
- key == Qt.Key.Key_Control
- or key == Qt.Key.Key_Shift
- or key == Qt.Key.Key_Alt
- or key == Qt.Key.Key_Meta
- ):
- return
-
- super().keyPressEvent(event)
diff --git a/buzz/widgets/text_display_box.py b/buzz/widgets/text_display_box.py
deleted file mode 100644
index 132e4bb2..00000000
--- a/buzz/widgets/text_display_box.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from typing import Optional
-
-from PyQt6.QtWidgets import QPlainTextEdit, QWidget
-
-
-class TextDisplayBox(QPlainTextEdit):
- """TextDisplayBox is a read-only textbox"""
-
- def __init__(self, parent: Optional[QWidget], *args) -> None:
- super().__init__(parent, *args)
- self.setReadOnly(True)
diff --git a/buzz/widgets/toolbar.py b/buzz/widgets/toolbar.py
deleted file mode 100644
index 1ac595f8..00000000
--- a/buzz/widgets/toolbar.py
+++ /dev/null
@@ -1,30 +0,0 @@
-import platform
-import typing
-
-from PyQt6 import QtGui
-from PyQt6.QtCore import QSize, Qt
-from PyQt6.QtWidgets import QToolBar, QWidget
-
-
-class ToolBar(QToolBar):
- def __init__(self, parent: typing.Optional[QWidget] = None):
- super().__init__(parent)
-
- self.setIconSize(QSize(18, 18))
- self.setStyleSheet("QToolButton{margin: 6px 3px;}")
- self.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
-
- def addAction(self, *args):
- action = super().addAction(*args)
- self.fix_spacing_on_mac()
- return action
-
- def addActions(self, actions: typing.Iterable[QtGui.QAction]) -> None:
- super().addActions(actions)
- self.fix_spacing_on_mac()
-
- def fix_spacing_on_mac(self):
- if platform.system() == "Darwin":
- self.widgetForAction(self.actions()[0]).setStyleSheet(
- "QToolButton { margin-left: 9px; margin-right: 1px; }"
- )
diff --git a/buzz/widgets/transcriber/__init__.py b/buzz/widgets/transcriber/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/buzz/widgets/transcriber/advanced_settings_button.py b/buzz/widgets/transcriber/advanced_settings_button.py
deleted file mode 100644
index 4b74ebb6..00000000
--- a/buzz/widgets/transcriber/advanced_settings_button.py
+++ /dev/null
@@ -1,9 +0,0 @@
-from typing import Optional
-
-from PyQt6.QtWidgets import QPushButton, QWidget
-
-from buzz.locale import _
-
-class AdvancedSettingsButton(QPushButton):
- def __init__(self, parent: Optional[QWidget]) -> None:
- super().__init__(_("Advanced..."), parent)
diff --git a/buzz/widgets/transcriber/advanced_settings_dialog.py b/buzz/widgets/transcriber/advanced_settings_dialog.py
deleted file mode 100644
index a9e1a954..00000000
--- a/buzz/widgets/transcriber/advanced_settings_dialog.py
+++ /dev/null
@@ -1,332 +0,0 @@
-from PyQt6.QtCore import pyqtSignal
-from PyQt6.QtWidgets import (
- QDialog,
- QWidget,
- QDialogButtonBox,
- QCheckBox,
- QPlainTextEdit,
- QFormLayout,
- QLabel,
- QDoubleSpinBox,
- QLineEdit,
- QComboBox,
- QHBoxLayout,
- QPushButton,
- QSpinBox,
- QFileDialog,
-)
-
-from buzz.locale import _
-from buzz.transcriber.transcriber import TranscriptionOptions
-from buzz.settings.settings import Settings
-from buzz.settings.recording_transcriber_mode import RecordingTranscriberMode
-from buzz.widgets.line_edit import LineEdit
-from buzz.widgets.transcriber.initial_prompt_text_edit import InitialPromptTextEdit
-
-
-class AdvancedSettingsDialog(QDialog):
- transcription_options: TranscriptionOptions
- transcription_options_changed = pyqtSignal(TranscriptionOptions)
- recording_mode_changed = pyqtSignal(RecordingTranscriberMode)
- hide_unconfirmed_changed = pyqtSignal(bool)
-
- def __init__(
- self,
- transcription_options: TranscriptionOptions,
- parent: QWidget | None = None,
- show_recording_settings: bool = False,
- ):
- super().__init__(parent)
-
- self.transcription_options = transcription_options
- self.settings = Settings()
-
- self.setWindowTitle(_("Advanced Settings"))
- self.setMinimumWidth(800)
-
- layout = QFormLayout(self)
- layout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
-
- transcription_settings_title= _("Speech recognition settings")
- transcription_settings_title_label = QLabel(f"{transcription_settings_title} ", self)
- layout.addRow("", transcription_settings_title_label)
-
- self.initial_prompt_text_edit = InitialPromptTextEdit(
- transcription_options.initial_prompt,
- transcription_options.model.model_type,
- self,
- )
- self.initial_prompt_text_edit.textChanged.connect(
- self.on_initial_prompt_changed
- )
-
- layout.addRow(_("Initial Prompt:"), self.initial_prompt_text_edit)
-
- translation_settings_title= _("Translation settings")
- translation_settings_title_label = QLabel(f"{translation_settings_title} ", self)
- layout.addRow("", translation_settings_title_label)
-
- self.enable_llm_translation_checkbox = QCheckBox(_("Enable AI translation"))
- self.enable_llm_translation_checkbox.setChecked(self.transcription_options.enable_llm_translation)
- self.enable_llm_translation_checkbox.stateChanged.connect(self.on_enable_llm_translation_changed)
- layout.addRow("", self.enable_llm_translation_checkbox)
-
- llm_model = self.transcription_options.llm_model or "gpt-4.1-mini"
- self.llm_model_line_edit = LineEdit(llm_model, self)
- self.llm_model_line_edit.textChanged.connect(self.on_llm_model_changed)
- self.llm_model_line_edit.setMinimumWidth(170)
- self.llm_model_line_edit.setEnabled(self.transcription_options.enable_llm_translation)
- self.llm_model_label = QLabel(_("AI model:"))
- self.llm_model_label.setEnabled(self.transcription_options.enable_llm_translation)
- layout.addRow(self.llm_model_label, self.llm_model_line_edit)
-
- default_llm_prompt = self.transcription_options.llm_prompt or _(
- "Please translate each text sent to you from English to Spanish. Translation will be used in an automated system, please do not add any comments or notes, just the translation."
- )
- self.llm_prompt_text_edit = QPlainTextEdit(default_llm_prompt)
- self.llm_prompt_text_edit.setEnabled(self.transcription_options.enable_llm_translation)
- self.llm_prompt_text_edit.setMinimumWidth(170)
- self.llm_prompt_text_edit.setFixedHeight(80)
- self.llm_prompt_text_edit.textChanged.connect(self.on_llm_prompt_changed)
- self.llm_prompt_label = QLabel(_("Instructions for AI:"))
- self.llm_prompt_label.setEnabled(self.transcription_options.enable_llm_translation)
- layout.addRow(self.llm_prompt_label, self.llm_prompt_text_edit)
-
- if show_recording_settings:
- recording_settings_title = _("Recording settings")
- recording_settings_title_label = QLabel(f"{recording_settings_title} ", self)
- layout.addRow("", recording_settings_title_label)
-
- self.silence_threshold_spin_box = QDoubleSpinBox(self)
- self.silence_threshold_spin_box.setRange(0.0, 1.0)
- self.silence_threshold_spin_box.setSingleStep(0.0005)
- self.silence_threshold_spin_box.setDecimals(4)
- self.silence_threshold_spin_box.setValue(transcription_options.silence_threshold)
- self.silence_threshold_spin_box.valueChanged.connect(self.on_silence_threshold_changed)
- self.silence_threshold_spin_box.setFixedWidth(90)
- layout.addRow(_("Silence threshold:"), self.silence_threshold_spin_box)
-
- # Live recording mode
- self.recording_mode_combo = QComboBox(self)
- for mode in RecordingTranscriberMode:
- self.recording_mode_combo.addItem(mode.value)
- self.recording_mode_combo.setCurrentIndex(
- self.settings.value(Settings.Key.RECORDING_TRANSCRIBER_MODE, 0)
- )
- self.recording_mode_combo.currentIndexChanged.connect(self.on_recording_mode_changed)
- self.recording_mode_combo.setFixedWidth(250)
- layout.addRow(_("Live recording mode") + ":", self.recording_mode_combo)
-
- self.line_separator_line_edit = QLineEdit(self)
- line_sep_display = repr(transcription_options.line_separator)[1:-1] or r"\n\n"
- self.line_separator_line_edit.setText(line_sep_display)
- self.line_separator_line_edit.textChanged.connect(self.on_line_separator_changed)
- self.line_separator_label = QLabel(_("Line separator:"))
- layout.addRow(self.line_separator_label, self.line_separator_line_edit)
-
- self.transcription_step_spin_box = QDoubleSpinBox(self)
- self.transcription_step_spin_box.setRange(2.0, 5.0)
- self.transcription_step_spin_box.setSingleStep(0.1)
- self.transcription_step_spin_box.setDecimals(1)
- self.transcription_step_spin_box.setValue(transcription_options.transcription_step)
- self.transcription_step_spin_box.valueChanged.connect(self.on_transcription_step_changed)
- self.transcription_step_spin_box.setFixedWidth(80)
- self.transcription_step_label = QLabel(_("Transcription step:"))
- layout.addRow(self.transcription_step_label, self.transcription_step_spin_box)
-
- hide_unconfirmed = self.settings.value(
- Settings.Key.RECORDING_TRANSCRIBER_HIDE_UNCONFIRMED, True
- )
- self.hide_unconfirmed_checkbox = QCheckBox(_("Hide unconfirmed"))
- self.hide_unconfirmed_checkbox.setChecked(hide_unconfirmed)
- self.hide_unconfirmed_checkbox.stateChanged.connect(self.on_hide_unconfirmed_changed)
- self.hide_unconfirmed_label = QLabel("")
- layout.addRow(self.hide_unconfirmed_label, self.hide_unconfirmed_checkbox)
-
- self._update_recording_mode_visibility(
- RecordingTranscriberMode(self.recording_mode_combo.currentText())
- )
-
- # Export enabled checkbox
- self._export_enabled = self.settings.value(
- Settings.Key.RECORDING_TRANSCRIBER_EXPORT_ENABLED, False
- )
- self.export_enabled_checkbox = QCheckBox(_("Enable live recording export"))
- self.export_enabled_checkbox.setChecked(self._export_enabled)
- self.export_enabled_checkbox.stateChanged.connect(self.on_export_enabled_changed)
- layout.addRow("", self.export_enabled_checkbox)
-
- # Export folder
- export_folder = self.settings.value(
- Settings.Key.RECORDING_TRANSCRIBER_EXPORT_FOLDER, ""
- )
- self.export_folder_line_edit = LineEdit(export_folder, self)
- self.export_folder_line_edit.setEnabled(self._export_enabled)
- self.export_folder_line_edit.textChanged.connect(self.on_export_folder_changed)
- self.export_folder_browse_button = QPushButton(_("Browse"), self)
- self.export_folder_browse_button.setEnabled(self._export_enabled)
- self.export_folder_browse_button.clicked.connect(self.on_browse_export_folder)
- export_folder_row = QHBoxLayout()
- export_folder_row.addWidget(self.export_folder_line_edit)
- export_folder_row.addWidget(self.export_folder_browse_button)
- self.export_folder_label = QLabel(_("Export folder:"))
- self.export_folder_label.setEnabled(self._export_enabled)
- layout.addRow(self.export_folder_label, export_folder_row)
-
- # Export file name template
- export_file_name = self.settings.value(
- Settings.Key.RECORDING_TRANSCRIBER_EXPORT_FILE_NAME, ""
- )
- self.export_file_name_line_edit = LineEdit(export_file_name, self)
- self.export_file_name_line_edit.setEnabled(self._export_enabled)
- self.export_file_name_line_edit.textChanged.connect(self.on_export_file_name_changed)
- self.export_file_name_label = QLabel(_("Export file name:"))
- self.export_file_name_label.setEnabled(self._export_enabled)
- layout.addRow(self.export_file_name_label, self.export_file_name_line_edit)
-
- # Export file type
- self.export_file_type_combo = QComboBox(self)
- self.export_file_type_combo.addItem(_("Text file (.txt)"), "txt")
- self.export_file_type_combo.addItem(_("CSV (.csv)"), "csv")
- current_type = self.settings.value(
- Settings.Key.RECORDING_TRANSCRIBER_EXPORT_FILE_TYPE, "txt"
- )
- type_index = self.export_file_type_combo.findData(current_type)
- if type_index >= 0:
- self.export_file_type_combo.setCurrentIndex(type_index)
- self.export_file_type_combo.setEnabled(self._export_enabled)
- self.export_file_type_combo.currentIndexChanged.connect(self.on_export_file_type_changed)
- self.export_file_type_combo.setFixedWidth(200)
- self.export_file_type_label = QLabel(_("Export file type:"))
- self.export_file_type_label.setEnabled(self._export_enabled)
- layout.addRow(self.export_file_type_label, self.export_file_type_combo)
-
- # Max entries
- max_entries = self.settings.value(
- Settings.Key.RECORDING_TRANSCRIBER_EXPORT_MAX_ENTRIES, 0, int
- )
- self.export_max_entries_spin = QSpinBox(self)
- self.export_max_entries_spin.setRange(0, 99)
- self.export_max_entries_spin.setValue(max_entries)
- self.export_max_entries_spin.setEnabled(self._export_enabled)
- self.export_max_entries_spin.valueChanged.connect(self.on_export_max_entries_changed)
- self.export_max_entries_spin.setFixedWidth(90)
- self.export_max_entries_label = QLabel(_("Limit export entries\n(0 = export all):"))
- self.export_max_entries_label.setEnabled(self._export_enabled)
- layout.addRow(self.export_max_entries_label, self.export_max_entries_spin)
-
- _field_height = self.llm_model_line_edit.sizeHint().height()
- for widget in (
- self.line_separator_line_edit,
- self.silence_threshold_spin_box,
- self.recording_mode_combo,
- self.transcription_step_spin_box,
- self.export_file_type_combo,
- self.export_max_entries_spin,
- ):
- widget.setFixedHeight(_field_height)
-
- button_box = QDialogButtonBox(
- QDialogButtonBox.StandardButton(QDialogButtonBox.StandardButton.Ok), self
- )
- button_box.button(QDialogButtonBox.StandardButton.Ok).setText(_("Ok"))
- button_box.accepted.connect(self.accept)
-
- layout.addWidget(button_box)
-
- self.setLayout(layout)
-
- def on_initial_prompt_changed(self):
- self.transcription_options.initial_prompt = (
- self.initial_prompt_text_edit.toPlainText()
- )
- self.transcription_options_changed.emit(self.transcription_options)
-
- def on_enable_llm_translation_changed(self, state):
- self.transcription_options.enable_llm_translation = state == 2
- self.transcription_options_changed.emit(self.transcription_options)
-
- enabled = self.transcription_options.enable_llm_translation
- self.llm_model_label.setEnabled(enabled)
- self.llm_model_line_edit.setEnabled(enabled)
- self.llm_prompt_label.setEnabled(enabled)
- self.llm_prompt_text_edit.setEnabled(enabled)
-
- def on_llm_model_changed(self, text: str):
- self.transcription_options.llm_model = text
- self.transcription_options_changed.emit(self.transcription_options)
-
- def on_llm_prompt_changed(self):
- self.transcription_options.llm_prompt = (
- self.llm_prompt_text_edit.toPlainText()
- )
- self.transcription_options_changed.emit(self.transcription_options)
-
- def on_silence_threshold_changed(self, value: float):
- self.transcription_options.silence_threshold = value
- self.transcription_options_changed.emit(self.transcription_options)
-
- def on_line_separator_changed(self, text: str):
- try:
- self.transcription_options.line_separator = text.encode().decode("unicode_escape")
- except UnicodeDecodeError:
- return
- self.transcription_options_changed.emit(self.transcription_options)
-
- def on_recording_mode_changed(self, index: int):
- self.settings.set_value(Settings.Key.RECORDING_TRANSCRIBER_MODE, index)
- mode = list(RecordingTranscriberMode)[index]
- self._update_recording_mode_visibility(mode)
- self.recording_mode_changed.emit(mode)
-
- def _update_recording_mode_visibility(self, mode: RecordingTranscriberMode):
- is_append_and_correct = mode == RecordingTranscriberMode.APPEND_AND_CORRECT
- self.line_separator_label.setVisible(not is_append_and_correct)
- self.line_separator_line_edit.setVisible(not is_append_and_correct)
- self.transcription_step_label.setVisible(is_append_and_correct)
- self.transcription_step_spin_box.setVisible(is_append_and_correct)
- self.hide_unconfirmed_label.setVisible(is_append_and_correct)
- self.hide_unconfirmed_checkbox.setVisible(is_append_and_correct)
-
- def on_transcription_step_changed(self, value: float):
- self.transcription_options.transcription_step = round(value, 1)
- self.transcription_options_changed.emit(self.transcription_options)
-
- def on_hide_unconfirmed_changed(self, state: int):
- value = state == 2
- self.settings.set_value(Settings.Key.RECORDING_TRANSCRIBER_HIDE_UNCONFIRMED, value)
- self.hide_unconfirmed_changed.emit(value)
-
- def on_export_enabled_changed(self, state: int):
- self._export_enabled = state == 2
- self.settings.set_value(Settings.Key.RECORDING_TRANSCRIBER_EXPORT_ENABLED, self._export_enabled)
- for widget in (
- self.export_folder_label,
- self.export_folder_line_edit,
- self.export_folder_browse_button,
- self.export_file_name_label,
- self.export_file_name_line_edit,
- self.export_file_type_label,
- self.export_file_type_combo,
- self.export_max_entries_label,
- self.export_max_entries_spin,
- ):
- widget.setEnabled(self._export_enabled)
-
- def on_export_folder_changed(self, text: str):
- self.settings.set_value(Settings.Key.RECORDING_TRANSCRIBER_EXPORT_FOLDER, text)
-
- def on_browse_export_folder(self):
- folder = QFileDialog.getExistingDirectory(self, _("Select Export Folder"))
- if folder:
- self.export_folder_line_edit.setText(folder)
-
- def on_export_file_name_changed(self, text: str):
- self.settings.set_value(Settings.Key.RECORDING_TRANSCRIBER_EXPORT_FILE_NAME, text)
-
- def on_export_file_type_changed(self, index: int):
- file_type = self.export_file_type_combo.itemData(index)
- self.settings.set_value(Settings.Key.RECORDING_TRANSCRIBER_EXPORT_FILE_TYPE, file_type)
-
- def on_export_max_entries_changed(self, value: int):
- self.settings.set_value(Settings.Key.RECORDING_TRANSCRIBER_EXPORT_MAX_ENTRIES, value)
diff --git a/buzz/widgets/transcriber/file_transcriber_widget.py b/buzz/widgets/transcriber/file_transcriber_widget.py
deleted file mode 100644
index a951f07f..00000000
--- a/buzz/widgets/transcriber/file_transcriber_widget.py
+++ /dev/null
@@ -1,194 +0,0 @@
-import logging
-from typing import Optional, List, Tuple
-
-from PyQt6 import QtGui
-from PyQt6.QtCore import pyqtSignal, Qt, QThreadPool
-from PyQt6.QtWidgets import (
- QWidget,
- QVBoxLayout,
- QPushButton,
-)
-
-from buzz.dialogs import show_model_download_error_dialog
-from buzz.locale import _
-from buzz.model_loader import ModelDownloader, WhisperModelSize, ModelType
-from buzz.paths import file_path_as_title
-from buzz.settings.settings import Settings
-from buzz.store.keyring_store import get_password, Key
-from buzz.transcriber.transcriber import (
- FileTranscriptionOptions,
- TranscriptionOptions,
-)
-from buzz.widgets.model_download_progress_dialog import ModelDownloadProgressDialog
-from buzz.widgets.preferences_dialog.models.file_transcription_preferences import (
- FileTranscriptionPreferences,
-)
-from buzz.widgets.transcriber.file_transcription_form_widget import (
- FileTranscriptionFormWidget,
-)
-
-
-class FileTranscriberWidget(QWidget):
- model_download_progress_dialog: Optional[ModelDownloadProgressDialog] = None
- model_loader: Optional[ModelDownloader] = None
- file_transcription_options: FileTranscriptionOptions
- transcription_options: TranscriptionOptions
- is_transcribing = False
- # (TranscriptionOptions, FileTranscriptionOptions, str)
- triggered = pyqtSignal(tuple)
- openai_access_token_changed = pyqtSignal(str)
- settings = Settings()
-
- def __init__(
- self,
- file_paths: Optional[List[str]] = None,
- url: Optional[str] = None,
- parent: Optional[QWidget] = None,
- flags: Qt.WindowType = Qt.WindowType.Widget,
- ) -> None:
- super().__init__(parent, flags)
-
- self.url = url
- self.file_paths = file_paths
-
- self.setWindowTitle(self.get_title())
-
- openai_access_token = get_password(Key.OPENAI_API_KEY)
-
- preferences = self.load_preferences()
-
- (
- self.transcription_options,
- self.file_transcription_options,
- ) = preferences.to_transcription_options(
- openai_access_token=openai_access_token,
- file_paths=self.file_paths,
- url=url,
- )
-
- layout = QVBoxLayout(self)
-
- self.form_widget = FileTranscriptionFormWidget(
- transcription_options=self.transcription_options,
- file_transcription_options=self.file_transcription_options,
- parent=self,
- )
- self.form_widget.openai_access_token_changed.connect(
- self.openai_access_token_changed
- )
-
- self.form_widget.transcription_options_changed.connect(
- self.reset_transcriber_controls
- )
-
- self.run_button = QPushButton(_("Run"), self)
- self.run_button.setDefault(True)
- self.run_button.clicked.connect(self.on_click_run)
-
- layout.addWidget(self.form_widget)
- layout.addWidget(self.run_button, 0, Qt.AlignmentFlag.AlignRight)
-
- self.setLayout(layout)
- self.setFixedWidth(self.sizeHint().width() + 50)
- self.setFixedHeight(self.sizeHint().height())
-
- self.reset_transcriber_controls()
-
- def get_title(self) -> str:
- if self.file_paths is not None:
- return ", ".join([file_path_as_title(path) for path in self.file_paths])
- if self.url is not None:
- return self.url
- return ""
-
- def load_preferences(self):
- self.settings.settings.beginGroup("file_transcriber")
- preferences = FileTranscriptionPreferences.load(settings=self.settings.settings)
- self.settings.settings.endGroup()
- return preferences
-
- def save_preferences(self):
- self.settings.settings.beginGroup("file_transcriber")
- preferences = FileTranscriptionPreferences.from_transcription_options(
- self.transcription_options, self.file_transcription_options
- )
- preferences.save(settings=self.settings.settings)
- self.settings.settings.endGroup()
-
- def on_click_run(self):
- self.run_button.setDisabled(True)
-
- model_path = self.transcription_options.model.get_local_model_path()
- if model_path is not None:
- self.on_model_loaded(model_path)
- return
-
- self.model_loader = ModelDownloader(model=self.transcription_options.model)
- self.model_loader.signals.progress.connect(self.on_download_model_progress)
- self.model_loader.signals.error.connect(self.on_download_model_error)
- self.model_loader.signals.finished.connect(self.on_model_loaded)
- QThreadPool().globalInstance().start(self.model_loader)
-
- def on_model_loaded(self, model_path: str):
- self.reset_transcriber_controls()
-
- self.triggered.emit(
- (self.transcription_options, self.file_transcription_options, model_path)
- )
- self.close()
-
- def on_download_model_progress(self, progress: Tuple[float, float]):
- (current_size, total_size) = progress
-
- if self.model_download_progress_dialog is None:
- self.model_download_progress_dialog = ModelDownloadProgressDialog(
- model_type=self.transcription_options.model.model_type, parent=self
- )
- self.model_download_progress_dialog.canceled.connect(
- self.on_cancel_model_progress_dialog
- )
-
- if self.model_download_progress_dialog is not None and total_size > 0:
- self.model_download_progress_dialog.set_value(
- fraction_completed=current_size / total_size
- )
-
- def on_download_model_error(self, error: str):
- self.reset_model_download()
- show_model_download_error_dialog(self, error)
- self.reset_transcriber_controls()
-
- def reset_transcriber_controls(self):
- button_enabled = True
- if (self.transcription_options.model.model_type == ModelType.FASTER_WHISPER
- and self.transcription_options.model.whisper_model_size == WhisperModelSize.CUSTOM
- and self.transcription_options.model.hugging_face_model_id == ""):
- button_enabled = False
-
- if (self.transcription_options.model.model_type == ModelType.HUGGING_FACE
- and self.transcription_options.model.hugging_face_model_id == ""):
- button_enabled = False
-
- self.run_button.setEnabled(button_enabled)
-
- def on_cancel_model_progress_dialog(self):
- self.reset_transcriber_controls()
- if self.model_loader is not None:
- self.model_loader.cancel()
- self.reset_model_download()
-
- def reset_model_download(self):
- if self.model_download_progress_dialog is not None:
- self.model_download_progress_dialog.close()
- self.model_download_progress_dialog = None
-
- def on_word_level_timings_changed(self, value: int):
- self.transcription_options.word_level_timings = (
- value == Qt.CheckState.Checked.value
- )
-
- def closeEvent(self, event: QtGui.QCloseEvent) -> None:
- if self.model_loader is not None:
- self.model_loader.cancel()
- self.save_preferences()
- super().closeEvent(event)
diff --git a/buzz/widgets/transcriber/file_transcription_form_widget.py b/buzz/widgets/transcriber/file_transcription_form_widget.py
deleted file mode 100644
index 8e9ff952..00000000
--- a/buzz/widgets/transcriber/file_transcription_form_widget.py
+++ /dev/null
@@ -1,124 +0,0 @@
-import logging
-from typing import Optional
-
-from PyQt6.QtCore import pyqtSignal, Qt
-from PyQt6.QtWidgets import QWidget, QVBoxLayout, QCheckBox, QFormLayout, QHBoxLayout
-
-from buzz.locale import _
-from buzz.model_loader import ModelType
-from buzz.transcriber.transcriber import (
- TranscriptionOptions,
- FileTranscriptionOptions,
- OutputFormat,
-)
-from buzz.widgets.transcriber.transcription_options_group_box import (
- TranscriptionOptionsGroupBox,
-)
-
-
-class FileTranscriptionFormWidget(QWidget):
- openai_access_token_changed = pyqtSignal(str)
- transcription_options_changed = pyqtSignal(tuple)
-
- def __init__(
- self,
- transcription_options: TranscriptionOptions,
- file_transcription_options: FileTranscriptionOptions,
- parent: Optional[QWidget] = None,
- ):
- super().__init__(parent)
-
- self.transcription_options = transcription_options
- self.file_transcription_options = file_transcription_options
-
- layout = QVBoxLayout(self)
-
- transcription_options_group_box = TranscriptionOptionsGroupBox(
- default_transcription_options=self.transcription_options, parent=self
- )
- transcription_options_group_box.transcription_options_changed.connect(
- self.on_transcription_options_changed
- )
-
- self.word_level_timings_checkbox = QCheckBox(_("Word-level timings"))
- self.word_level_timings_checkbox.setChecked(
- self.transcription_options.word_level_timings
- )
- self.word_level_timings_checkbox.stateChanged.connect(
- self.on_word_level_timings_changed
- )
-
- file_transcription_layout = QFormLayout()
- file_transcription_layout.addRow("", self.word_level_timings_checkbox)
-
- self.extract_speech_checkbox = QCheckBox(_("Extract speech"))
- self.extract_speech_checkbox.setChecked(
- self.transcription_options.extract_speech
- )
- self.extract_speech_checkbox.stateChanged.connect(
- self.on_extract_speech_changed
- )
-
- file_transcription_layout.addRow("", self.extract_speech_checkbox)
-
- export_format_layout = QHBoxLayout()
- for output_format in OutputFormat:
- export_format_checkbox = QCheckBox(
- f"{output_format.value.upper()}", parent=self
- )
- export_format_checkbox.setChecked(
- output_format in self.file_transcription_options.output_formats
- )
- export_format_checkbox.stateChanged.connect(
- self.get_on_checkbox_state_changed_callback(output_format)
- )
- export_format_layout.addWidget(export_format_checkbox)
-
- file_transcription_layout.addRow(_("Export:"), export_format_layout)
-
- layout.addWidget(transcription_options_group_box)
- layout.addLayout(file_transcription_layout)
- self.setLayout(layout)
-
- def on_transcription_options_changed(
- self, transcription_options: TranscriptionOptions
- ):
- self.transcription_options = transcription_options
- self.transcription_options_changed.emit(
- (self.transcription_options, self.file_transcription_options)
- )
- if self.transcription_options.openai_access_token != "":
- self.openai_access_token_changed.emit(
- self.transcription_options.openai_access_token
- )
-
- def on_word_level_timings_changed(self, value: int):
- self.transcription_options.word_level_timings = (
- value == Qt.CheckState.Checked.value
- )
-
- self.transcription_options_changed.emit(
- (self.transcription_options, self.file_transcription_options)
- )
-
- def on_extract_speech_changed(self, value: int):
- self.transcription_options.extract_speech = (
- value == Qt.CheckState.Checked.value
- )
-
- self.transcription_options_changed.emit(
- (self.transcription_options, self.file_transcription_options)
- )
-
- def get_on_checkbox_state_changed_callback(self, output_format: OutputFormat):
- def on_checkbox_state_changed(state: int):
- if state == Qt.CheckState.Checked.value:
- self.file_transcription_options.output_formats.add(output_format)
- elif state == Qt.CheckState.Unchecked.value:
- self.file_transcription_options.output_formats.remove(output_format)
-
- self.transcription_options_changed.emit(
- (self.transcription_options, self.file_transcription_options)
- )
-
- return on_checkbox_state_changed
diff --git a/buzz/widgets/transcriber/hugging_face_search_line_edit.py b/buzz/widgets/transcriber/hugging_face_search_line_edit.py
deleted file mode 100644
index b53bbfa7..00000000
--- a/buzz/widgets/transcriber/hugging_face_search_line_edit.py
+++ /dev/null
@@ -1,167 +0,0 @@
-import json
-import logging
-from typing import Optional
-
-from PyQt6.QtCore import (
- pyqtSignal,
- QTimer,
- Qt,
- QMetaObject,
- QUrl,
- QUrlQuery,
- QPoint,
- QObject,
- QEvent,
-)
-from PyQt6.QtGui import QKeyEvent
-from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
-from PyQt6.QtWidgets import QListWidget, QWidget, QAbstractItemView, QListWidgetItem, QSizePolicy
-
-from buzz.locale import _
-from buzz.widgets.line_edit import LineEdit
-
-
-# Adapted from https://github.com/ismailsunni/scripts/blob/master/autocomplete_from_url.py
-class HuggingFaceSearchLineEdit(LineEdit):
- model_selected = pyqtSignal(str)
- popup: QListWidget
-
- def __init__(
- self,
- default_value: str = "",
- network_access_manager: Optional[QNetworkAccessManager] = None,
- parent: Optional[QWidget] = None,
- ):
- super().__init__(default_value, parent)
- self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
- self.setPlaceholderText(_("Huggingface ID of a model"))
-
- self.setMinimumWidth(50)
-
- self.timer = QTimer(self)
- self.timer.setSingleShot(True)
- self.timer.setInterval(250)
- self.timer.timeout.connect(self.fetch_models)
-
- # Restart debounce timer each time editor text changes
- self.textEdited.connect(self.timer.start)
- self.textEdited.connect(self.on_text_edited)
-
- if network_access_manager is None:
- network_access_manager = QNetworkAccessManager(self)
-
- self.network_manager = network_access_manager
- self.network_manager.finished.connect(self.on_request_response)
-
- self.popup = QListWidget()
- self.popup.setWindowFlags(Qt.WindowType.Popup)
- self.popup.setFocusProxy(self)
- self.popup.setMouseTracking(True)
- self.popup.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
- self.popup.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
- self.popup.installEventFilter(self)
- self.popup.itemClicked.connect(self.on_select_item)
-
- def focusInEvent(self, event):
- super().focusInEvent(event)
- # Defer selectAll to run after mouse events are processed
- QTimer.singleShot(0, self.selectAll)
-
- def on_text_edited(self, text: str):
- self.model_selected.emit(text)
-
- def on_select_item(self):
- self.popup.hide()
- self.setFocus()
-
- item = self.popup.currentItem()
- self.setText(item.text())
- QMetaObject.invokeMethod(self, "returnPressed")
- self.model_selected.emit(item.data(Qt.ItemDataRole.UserRole))
-
- def fetch_models(self):
- text = self.text()
- if len(text) < 3:
- return
-
- url = QUrl("https://huggingface.co/api/models")
-
- query = QUrlQuery()
- query.addQueryItem("filter", "whisper")
- query.addQueryItem("search", text)
-
- url.setQuery(query)
-
- return self.network_manager.get(QNetworkRequest(url))
-
- def on_popup_selected(self):
- self.timer.stop()
-
- def on_request_response(self, network_reply: QNetworkReply):
- if network_reply.error() != QNetworkReply.NetworkError.NoError:
- logging.debug(
- "Error fetching Hugging Face models: %s", network_reply.error()
- )
- return
-
- models = json.loads(network_reply.readAll().data())
-
- # TODO Possibly need to include text entered in the search box as item in popup
- # as not all models are tagged with 'whisper'
- if len(models) > 0:
- self.popup.setUpdatesEnabled(False)
- self.popup.clear()
-
- for model in models:
- model_id = model.get("id")
-
- item = QListWidgetItem(self.popup)
- item.setText(model_id)
- item.setData(Qt.ItemDataRole.UserRole, model_id)
-
- self.popup.setCurrentItem(self.popup.item(0))
- self.popup.setFixedWidth(self.popup.sizeHintForColumn(0) + 20)
- self.popup.setFixedHeight(
- self.popup.sizeHintForRow(0) * min(len(models), 8)
- ) # show max 8 models, then scroll
- self.popup.setUpdatesEnabled(True)
- self.popup.move(self.mapToGlobal(QPoint(0, self.height())))
- self.popup.setFocus()
- self.popup.show()
-
- def eventFilter(self, target: QObject, event: QEvent):
- if hasattr(self, "popup") is False or target != self.popup:
- return False
-
- if event.type() == QEvent.Type.MouseButtonPress:
- self.popup.hide()
- self.setFocus()
- return True
-
- if isinstance(event, QKeyEvent):
- key = event.key()
- if key in [Qt.Key.Key_Enter, Qt.Key.Key_Return]:
- if self.popup.currentItem() is not None:
- self.on_select_item()
- return True
-
- if key == Qt.Key.Key_Escape:
- self.setFocus()
- self.popup.hide()
- return True
-
- if key in [
- Qt.Key.Key_Up,
- Qt.Key.Key_Down,
- Qt.Key.Key_Home,
- Qt.Key.Key_End,
- Qt.Key.Key_PageUp,
- Qt.Key.Key_PageDown,
- ]:
- return False
-
- self.setFocus()
- self.event(event)
- self.popup.hide()
-
- return False
diff --git a/buzz/widgets/transcriber/initial_prompt_text_edit.py b/buzz/widgets/transcriber/initial_prompt_text_edit.py
deleted file mode 100644
index 618c2273..00000000
--- a/buzz/widgets/transcriber/initial_prompt_text_edit.py
+++ /dev/null
@@ -1,13 +0,0 @@
-from PyQt6.QtWidgets import QPlainTextEdit, QWidget
-
-from buzz.locale import _
-from buzz.model_loader import ModelType
-
-
-class InitialPromptTextEdit(QPlainTextEdit):
- def __init__(self, text: str, model_type: ModelType, parent: QWidget | None = None):
- super().__init__(text, parent)
- self.setPlaceholderText(_("Enter prompt..."))
- self.setEnabled(model_type.supports_initial_prompt)
- self.setMinimumWidth(350)
- self.setFixedHeight(80)
diff --git a/buzz/widgets/transcriber/languages_combo_box.py b/buzz/widgets/transcriber/languages_combo_box.py
deleted file mode 100644
index 8ebc0e32..00000000
--- a/buzz/widgets/transcriber/languages_combo_box.py
+++ /dev/null
@@ -1,59 +0,0 @@
-from typing import Optional
-import os
-
-from PyQt6.QtCore import pyqtSignal, Qt
-from PyQt6.QtWidgets import QComboBox, QWidget, QFrame
-from PyQt6.QtGui import QStandardItem, QStandardItemModel
-
-from buzz.locale import _
-from buzz.transcriber.transcriber import LANGUAGES
-
-
-class LanguagesComboBox(QComboBox):
- """LanguagesComboBox displays a list of languages available to use with Whisper"""
-
- # language is a language key from whisper.tokenizer.LANGUAGES or '' for "detect language"
- languageChanged = pyqtSignal(str)
-
- def __init__(
- self, default_language: Optional[str], parent: Optional[QWidget] = None
- ) -> None:
- super().__init__(parent)
-
- favorite_languages = os.getenv("BUZZ_FAVORITE_LANGUAGES", '')
- favorite_languages = favorite_languages.split(",")
- favorite_languages = [(lang, LANGUAGES[lang].title()) for lang in favorite_languages
- if lang in LANGUAGES]
- if favorite_languages:
- favorite_languages.insert(0, ("-------", "-------"))
- favorite_languages.append(("-------", "-------"))
-
- whisper_languages = sorted(
- [(lang, LANGUAGES[lang].title()) for lang in LANGUAGES],
- key=lambda lang: lang[1],
- )
- self.languages = [("", _("Detect Language"))] + favorite_languages + whisper_languages
-
- model = QStandardItemModel()
- for lang in self.languages:
- item = QStandardItem(lang[1])
- if lang[0] == "-------":
- item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsSelectable & ~Qt.ItemFlag.ItemIsEnabled)
- model.appendRow(item)
-
- self.setModel(model)
- self.currentIndexChanged.connect(self.on_index_changed)
-
- default_language_key = default_language if default_language != "" else None
- for i, lang in enumerate(self.languages):
- if lang[0] == default_language_key:
- self.setCurrentIndex(i)
-
- def on_index_changed(self, index: int):
- self.languageChanged.emit(self.languages[index][0])
-
- def showPopup(self):
- super().showPopup()
- popup = self.findChild(QFrame)
- if popup and popup.height() > 400:
- popup.setFixedHeight(400)
diff --git a/buzz/widgets/transcriber/mms_language_line_edit.py b/buzz/widgets/transcriber/mms_language_line_edit.py
deleted file mode 100644
index 4f101d6d..00000000
--- a/buzz/widgets/transcriber/mms_language_line_edit.py
+++ /dev/null
@@ -1,48 +0,0 @@
-from typing import Optional
-
-from PyQt6.QtCore import pyqtSignal
-from PyQt6.QtWidgets import QWidget, QSizePolicy
-
-from buzz.locale import _
-from buzz.widgets.line_edit import LineEdit
-
-
-class MMSLanguageLineEdit(LineEdit):
- """Text input for MMS language codes (ISO 639-3).
-
- MMS models support 1000+ languages using ISO 639-3 codes (3 letters).
- Examples: eng (English), fra (French), deu (German), spa (Spanish)
- """
-
- languageChanged = pyqtSignal(str)
-
- def __init__(
- self,
- default_language: str = "eng",
- parent: Optional[QWidget] = None
- ):
- super().__init__(default_language, parent)
- self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
- self.setPlaceholderText(_("e.g., eng, fra, deu"))
- self.setToolTip(
- _("Enter an ISO 639-3 language code (3 letters).\n"
- "Examples: eng (English), fra (French), deu (German),\n"
- "spa (Spanish), lav (Latvian)")
- )
- self.setMaxLength(10) # Allow some flexibility for edge cases
- self.setMinimumWidth(100)
-
- self.textChanged.connect(self._on_text_changed)
-
- def _on_text_changed(self, text: str):
- """Emit language changed signal with cleaned text."""
- cleaned = text.strip().lower()
- self.languageChanged.emit(cleaned)
-
- def language(self) -> str:
- """Get the current language code."""
- return self.text().strip().lower()
-
- def setLanguage(self, language: str):
- """Set the language code."""
- self.setText(language.strip().lower() if language else "eng")
diff --git a/buzz/widgets/transcriber/tasks_combo_box.py b/buzz/widgets/transcriber/tasks_combo_box.py
deleted file mode 100644
index acf9f6ea..00000000
--- a/buzz/widgets/transcriber/tasks_combo_box.py
+++ /dev/null
@@ -1,22 +0,0 @@
-from typing import Optional
-
-from PyQt6.QtCore import pyqtSignal
-from PyQt6.QtWidgets import QComboBox, QWidget
-
-from buzz.transcriber.transcriber import Task, TASK_LABEL_TRANSLATIONS
-
-
-class TasksComboBox(QComboBox):
- """TasksComboBox displays a list of tasks available to use with Whisper"""
-
- taskChanged = pyqtSignal(Task)
-
- def __init__(self, default_task: Task, parent: Optional[QWidget], *args) -> None:
- super().__init__(parent, *args)
- self.tasks = [i for i in Task]
- self.addItems(map(lambda task: TASK_LABEL_TRANSLATIONS[task], self.tasks))
- self.currentIndexChanged.connect(self.on_index_changed)
- self.setCurrentText(TASK_LABEL_TRANSLATIONS[default_task])
-
- def on_index_changed(self, index: int):
- self.taskChanged.emit(self.tasks[index])
diff --git a/buzz/widgets/transcriber/transcription_options_group_box.py b/buzz/widgets/transcriber/transcription_options_group_box.py
deleted file mode 100644
index f3c124d8..00000000
--- a/buzz/widgets/transcriber/transcription_options_group_box.py
+++ /dev/null
@@ -1,310 +0,0 @@
-import os
-import logging
-import platform
-from typing import Optional, List
-
-from PyQt6.QtCore import pyqtSignal, QLocale
-from PyQt6.QtGui import QIcon
-from PyQt6.QtWidgets import QGroupBox, QWidget, QFormLayout, QComboBox, QLabel, QHBoxLayout
-
-from buzz.locale import _
-from buzz.settings.settings import Settings
-from buzz.widgets.icon import INFO_ICON_PATH
-from buzz.model_loader import ModelType, WhisperModelSize, get_whisper_cpp_file_path, is_mms_model
-from buzz.transcriber.transcriber import TranscriptionOptions, Task
-from buzz.widgets.model_type_combo_box import ModelTypeComboBox
-from buzz.widgets.openai_api_key_line_edit import OpenAIAPIKeyLineEdit
-from buzz.widgets.transcriber.advanced_settings_button import AdvancedSettingsButton
-from buzz.widgets.transcriber.advanced_settings_dialog import AdvancedSettingsDialog
-from buzz.widgets.transcriber.hugging_face_search_line_edit import (
- HuggingFaceSearchLineEdit,
-)
-from buzz.widgets.transcriber.languages_combo_box import LanguagesComboBox
-from buzz.widgets.transcriber.mms_language_line_edit import MMSLanguageLineEdit
-from buzz.widgets.transcriber.tasks_combo_box import TasksComboBox
-
-
-class TranscriptionOptionsGroupBox(QGroupBox):
- transcription_options: TranscriptionOptions
- transcription_options_changed = pyqtSignal(TranscriptionOptions)
-
- def __init__(
- self,
- default_transcription_options: TranscriptionOptions = TranscriptionOptions(),
- model_types: Optional[List[ModelType]] = None,
- parent: Optional[QWidget] = None,
- show_recording_settings: bool = False,
- ):
- super().__init__(title="", parent=parent)
- self.settings = Settings()
- self.ui_locale = self.settings.value(Settings.Key.UI_LOCALE, QLocale().name())
- self.transcription_options = default_transcription_options
-
- self.form_layout = QFormLayout(self)
-
- self.model_type_combo_box = ModelTypeComboBox(
- model_types=model_types,
- default_model=default_transcription_options.model.model_type,
- parent=self,
- )
- self.model_type_combo_box.changed.connect(self.on_model_type_changed)
-
- self.advanced_settings_dialog = AdvancedSettingsDialog(
- transcription_options=self.transcription_options,
- parent=self,
- show_recording_settings=show_recording_settings,
- )
- self.advanced_settings_dialog.transcription_options_changed.connect(
- self.on_transcription_options_changed
- )
-
- self.whisper_model_size_combo_box = QComboBox(self)
- self.whisper_model_size_combo_box.addItems(
- [size.value.title() for size in WhisperModelSize if size not in {WhisperModelSize.CUSTOM, WhisperModelSize.LUMII}]
- )
- self.whisper_model_size_combo_box.currentTextChanged.connect(
- self.on_whisper_model_size_changed
- )
-
- self.openai_access_token_edit = OpenAIAPIKeyLineEdit(
- key=default_transcription_options.openai_access_token, parent=self
- )
- self.openai_access_token_edit.key_changed.connect(
- self.on_openai_access_token_edit_changed
- )
-
- self.hugging_face_search_line_edit = HuggingFaceSearchLineEdit(
- default_value=default_transcription_options.model.hugging_face_model_id
- )
- self.hugging_face_search_line_edit.model_selected.connect(
- self.on_hugging_face_model_changed
- )
- self.hugging_face_search_line_edit.setVisible(False)
-
- self.tasks_combo_box = TasksComboBox(
- default_task=self.transcription_options.task, parent=self
- )
- self.tasks_combo_box.taskChanged.connect(self.on_task_changed)
-
- self.languages_combo_box = LanguagesComboBox(
- default_language=self.transcription_options.language, parent=self
- )
- self.languages_combo_box.languageChanged.connect(self.on_language_changed)
-
- # MMS language input (text field for ISO 639-3 codes)
- self.mms_language_line_edit = MMSLanguageLineEdit(
- default_language="eng", parent=self
- )
- self.mms_language_line_edit.languageChanged.connect(self.on_mms_language_changed)
- self.mms_language_line_edit.setVisible(False)
-
- self.advanced_settings_button = AdvancedSettingsButton(self)
- self.advanced_settings_button.clicked.connect(self.open_advanced_settings)
-
- self.form_layout.addRow(_("Model:"), self.model_type_combo_box)
-
- if platform.system() == "Darwin" and platform.machine() == "arm64":
- self.whisper_model_size_layout = QHBoxLayout()
- self.whisper_model_size_layout.setContentsMargins(0, 0, 0, 0)
- self.whisper_model_size_layout.setSpacing(0)
-
- self.whisper_model_size_layout.addWidget(self.whisper_model_size_combo_box)
-
- self.load_note_tooltip_icon = QLabel()
- self.load_note_tooltip_icon.setPixmap(QIcon(INFO_ICON_PATH).pixmap(23, 23))
- self.load_note_tooltip_icon.setToolTip(
- _("First time use of a model may take up to several minutest to load."))
- self.whisper_model_size_layout.addWidget(self.load_note_tooltip_icon)
-
- self.form_layout.addRow("", self.whisper_model_size_layout)
- else:
- self.load_note_tooltip_icon = None
- self.whisper_model_size_layout = None
- self.form_layout.addRow("", self.whisper_model_size_combo_box)
-
- self.form_layout.addRow("", self.hugging_face_search_line_edit)
- self.form_layout.addRow(_("Api Key:"), self.openai_access_token_edit)
- self.form_layout.addRow(_("Task:"), self.tasks_combo_box)
- self.form_layout.addRow(_("Language:"), self.languages_combo_box)
- self.form_layout.addRow(_("Language:"), self.mms_language_line_edit)
-
- self.reset_visible_rows()
-
- self.form_layout.addRow("", self.advanced_settings_button)
-
- self.setLayout(self.form_layout)
-
- def on_openai_access_token_edit_changed(self, access_token: str):
- self.transcription_options.openai_access_token = access_token
- self.transcription_options_changed.emit(self.transcription_options)
-
- def on_language_changed(self, language: str):
- if language == "":
- language = None
-
- self.transcription_options.language = language
- self.transcription_options_changed.emit(self.transcription_options)
-
- def on_mms_language_changed(self, language: str):
- """Handle MMS language code changes."""
- if language == "":
- language = "eng" # Default to English for MMS
-
- self.transcription_options.language = language
- self.transcription_options_changed.emit(self.transcription_options)
-
- def on_task_changed(self, task: Task):
- self.transcription_options.task = task
- self.transcription_options_changed.emit(self.transcription_options)
-
- def open_advanced_settings(self):
- self.advanced_settings_dialog.exec()
-
- def on_transcription_options_changed(
- self, transcription_options: TranscriptionOptions
- ):
- self.transcription_options = transcription_options
- self.transcription_options_changed.emit(transcription_options)
-
- def reset_visible_rows(self):
- model_type = self.transcription_options.model.model_type
- whisper_model_size = self.transcription_options.model.whisper_model_size
-
- if (model_type == ModelType.HUGGING_FACE
- or (whisper_model_size == WhisperModelSize.CUSTOM
- and model_type == ModelType.FASTER_WHISPER)):
- self.transcription_options.model.hugging_face_model_id = (
- self.settings.load_custom_model_id(self.transcription_options.model))
- self.hugging_face_search_line_edit.setText(
- self.transcription_options.model.hugging_face_model_id)
-
- self.form_layout.setRowVisible(
- self.hugging_face_search_line_edit,
- (model_type == ModelType.HUGGING_FACE)
- or (model_type == ModelType.FASTER_WHISPER
- and whisper_model_size == WhisperModelSize.CUSTOM),
- )
-
- # Remove custom model size for whisper
- custom_model_index = (self.whisper_model_size_combo_box
- .findText(WhisperModelSize.CUSTOM.value.title()))
- if model_type == ModelType.WHISPER and custom_model_index != -1:
- self.whisper_model_size_combo_box.removeItem(custom_model_index)
-
- # Add custom model size for whisper_cpp
- custom_model_index = (self.whisper_model_size_combo_box
- .findText(WhisperModelSize.CUSTOM.value.title()))
- if (model_type == ModelType.WHISPER_CPP
- and os.path.isfile(get_whisper_cpp_file_path(size=WhisperModelSize.CUSTOM))
- and custom_model_index == -1):
- self.whisper_model_size_combo_box.addItem(
- WhisperModelSize.CUSTOM.value.title()
- )
-
- # Add custom model size for faster_whisper
- custom_model_index = (self.whisper_model_size_combo_box
- .findText(WhisperModelSize.CUSTOM.value.title()))
- if model_type == ModelType.FASTER_WHISPER and custom_model_index == -1:
- self.whisper_model_size_combo_box.addItem(
- WhisperModelSize.CUSTOM.value.title()
- )
-
- # Leave LUMII model only for Latvian whisper_cpp
- lumii_model_index = (self.whisper_model_size_combo_box
- .findText(WhisperModelSize.LUMII.value.title()))
-
- if lumii_model_index != -1 and (model_type != ModelType.WHISPER_CPP or self.ui_locale != "lv_LV"):
- self.whisper_model_size_combo_box.removeItem(lumii_model_index)
-
- if lumii_model_index == -1 and model_type == ModelType.WHISPER_CPP and self.ui_locale == "lv_LV":
- self.whisper_model_size_combo_box.addItem(
- WhisperModelSize.LUMII.value.title()
- )
-
- self.whisper_model_size_combo_box.setCurrentText(
- self.transcription_options.model.whisper_model_size.value.title()
- )
-
- self.form_layout.setRowVisible(
- self.whisper_model_size_combo_box,
- (model_type == ModelType.WHISPER)
- or (model_type == ModelType.WHISPER_CPP)
- or (model_type == ModelType.FASTER_WHISPER),
- )
- if self.whisper_model_size_layout is not None:
- self.form_layout.setRowVisible(
- self.whisper_model_size_layout,
- (model_type == ModelType.WHISPER)
- or (model_type == ModelType.WHISPER_CPP)
- or (model_type == ModelType.FASTER_WHISPER),
- )
-
- self.form_layout.setRowVisible(
- self.openai_access_token_edit, model_type == ModelType.OPEN_AI_WHISPER_API
- )
-
- # Note on Apple Silicon Macs
- if self.load_note_tooltip_icon is not None:
- self.load_note_tooltip_icon.setVisible(
- self.transcription_options.model.model_type == ModelType.WHISPER_CPP
- )
-
- # Update language widget visibility (MMS vs Whisper)
- self._update_language_widget_visibility()
-
- def on_model_type_changed(self, model_type: ModelType):
- self.transcription_options.model.model_type = model_type
- if not model_type.supports_initial_prompt:
- self.transcription_options.initial_prompt = ""
-
- if (self.transcription_options.model.whisper_model_size == WhisperModelSize.LUMII
- and model_type != ModelType.WHISPER_CPP):
- self.transcription_options.model.whisper_model_size = WhisperModelSize.LARGEV3TURBO
-
- self.reset_visible_rows()
- self.transcription_options_changed.emit(self.transcription_options)
-
- def on_whisper_model_size_changed(self, text: str):
- model_size = WhisperModelSize(text.lower())
- self.transcription_options.model.whisper_model_size = model_size
-
- self.reset_visible_rows()
-
- self.transcription_options_changed.emit(self.transcription_options)
-
- def on_hugging_face_model_changed(self, model: str):
- self.transcription_options.model.hugging_face_model_id = model
- self.transcription_options_changed.emit(self.transcription_options)
-
- self.settings.save_custom_model_id(self.transcription_options.model)
-
- # Update language widget visibility based on whether this is an MMS model
- self._update_language_widget_visibility()
-
- def _update_language_widget_visibility(self):
- """Update language widget visibility based on whether the selected model is MMS."""
- model_type = self.transcription_options.model.model_type
- model_id = self.transcription_options.model.hugging_face_model_id
-
- # Check if this is an MMS model
- is_mms = (model_type == ModelType.HUGGING_FACE and is_mms_model(model_id))
-
- # Show MMS language input for MMS models, show dropdown for others
- self.form_layout.setRowVisible(self.mms_language_line_edit, is_mms)
- self.form_layout.setRowVisible(self.languages_combo_box, not is_mms)
-
- # Sync the language value when switching between MMS and non-MMS
- if is_mms:
- # When switching to MMS, use the MMS language input value
- mms_lang = self.mms_language_line_edit.language()
- if mms_lang:
- self.transcription_options.language = mms_lang
- self.transcription_options_changed.emit(self.transcription_options)
- else:
- # When switching from MMS to a regular model, use the dropdown's current value
- # This prevents invalid MMS language codes (like "eng") being used with Whisper
- current_index = self.languages_combo_box.currentIndex()
- dropdown_lang = self.languages_combo_box.languages[current_index][0]
- if self.transcription_options.language != dropdown_lang:
- self.transcription_options.language = dropdown_lang if dropdown_lang else None
- self.transcription_options_changed.emit(self.transcription_options)
diff --git a/buzz/widgets/transcription_record.py b/buzz/widgets/transcription_record.py
deleted file mode 100644
index 782b4e42..00000000
--- a/buzz/widgets/transcription_record.py
+++ /dev/null
@@ -1,28 +0,0 @@
-from uuid import UUID
-import logging
-from PyQt6.QtSql import QSqlRecord
-
-from buzz.model_loader import TranscriptionModel, ModelType, WhisperModelSize
-from buzz.transcriber.transcriber import Task
-
-
-class TranscriptionRecord:
- @staticmethod
- def id(record: QSqlRecord) -> UUID:
- return UUID(hex=record.value("id"))
-
- @staticmethod
- def model(record: QSqlRecord) -> TranscriptionModel:
- return TranscriptionModel(
- model_type=ModelType(record.value("model_type")),
- whisper_model_size=WhisperModelSize(record.value("whisper_model_size"))
- if record.value("whisper_model_size")
- else None,
- hugging_face_model_id=record.value("hugging_face_model_id")
- if record.value("hugging_face_model_id")
- else None
- )
-
- @staticmethod
- def task(record: QSqlRecord) -> Task:
- return Task(record.value("task"))
diff --git a/buzz/widgets/transcription_task_folder_watcher.py b/buzz/widgets/transcription_task_folder_watcher.py
deleted file mode 100644
index 368c5d2e..00000000
--- a/buzz/widgets/transcription_task_folder_watcher.py
+++ /dev/null
@@ -1,124 +0,0 @@
-import logging
-import os
-from typing import Dict
-
-from PyQt6.QtCore import QFileSystemWatcher, pyqtSignal, QObject
-
-from buzz.store.keyring_store import Key, get_password
-from buzz.transcriber.transcriber import FileTranscriptionTask
-from buzz.model_loader import ModelDownloader
-from buzz.widgets.preferences_dialog.models.folder_watch_preferences import (
- FolderWatchPreferences,
-)
-
-# Supported media file extensions (audio and video)
-SUPPORTED_EXTENSIONS = {
- ".mp3", ".wav", ".m4a", ".ogg", ".opus", ".flac", # audio
- ".mp4", ".webm", ".ogm", ".mov", ".mkv", ".avi", ".wmv", # video
-}
-
-
-class TranscriptionTaskFolderWatcher(QFileSystemWatcher):
- preferences: FolderWatchPreferences
- task_found = pyqtSignal(FileTranscriptionTask)
-
- # TODO: query db instead of passing tasks
- def __init__(
- self,
- tasks: Dict[int, FileTranscriptionTask],
- preferences: FolderWatchPreferences,
- parent: QObject = None,
- ):
- super().__init__(parent)
- self.tasks = tasks
- self.paths_emitted = set()
- self.set_preferences(preferences)
- self.directoryChanged.connect(self.find_tasks)
-
- def set_preferences(self, preferences: FolderWatchPreferences):
- self.preferences = preferences
- if len(self.directories()) > 0:
- self.removePaths(self.directories())
- if preferences.enabled:
- # Add the input directory and all subdirectories to the watcher
- for dirpath, dirnames, _ in os.walk(preferences.input_directory):
- # Skip hidden directories
- dirnames[:] = [d for d in dirnames if not d.startswith(".")]
- self.addPath(dirpath)
- logging.debug(
- 'Watching for media files in "%s" and subdirectories',
- preferences.input_directory,
- )
-
- def find_tasks(self):
- input_directory = self.preferences.input_directory
- tasks = {task.file_path: task for task in self.tasks.values()}
-
- if not self.preferences.enabled:
- return
-
- for dirpath, dirnames, filenames in os.walk(input_directory):
- for filename in filenames:
- file_path = os.path.join(dirpath, filename)
- file_ext = os.path.splitext(filename)[1].lower()
-
- # Check for temp conversion files (e.g., .ogg.wav)
- name_without_ext = os.path.splitext(filename)[0]
- secondary_ext = os.path.splitext(name_without_ext)[1].lower()
- is_temp_conversion_file = secondary_ext in SUPPORTED_EXTENSIONS
-
- if (
- filename.startswith(".") # hidden files
- or file_ext not in SUPPORTED_EXTENSIONS # non-media files
- or is_temp_conversion_file # temp conversion files like .ogg.wav
- or "_speech.mp3" in filename # extracted speech output files
- or file_path in tasks # file already in tasks
- or file_path in self.paths_emitted # file already emitted
- ):
- continue
-
- openai_access_token = get_password(Key.OPENAI_API_KEY)
- (
- transcription_options,
- file_transcription_options,
- ) = self.preferences.file_transcription_options.to_transcription_options(
- openai_access_token=openai_access_token,
- file_paths=[file_path],
- )
- model_path = transcription_options.model.get_local_model_path()
-
- if model_path is None:
- ModelDownloader(model=transcription_options.model).run()
- model_path = transcription_options.model.get_local_model_path()
-
- # Preserve subdirectory structure in output directory
- relative_path = os.path.relpath(dirpath, input_directory)
- if relative_path == ".":
- output_directory = self.preferences.output_directory
- else:
- output_directory = os.path.join(
- self.preferences.output_directory, relative_path
- )
-
- # Create output directory if it doesn't exist
- os.makedirs(output_directory, exist_ok=True)
-
- task = FileTranscriptionTask(
- file_path=file_path,
- original_file_path=file_path,
- transcription_options=transcription_options,
- file_transcription_options=file_transcription_options,
- model_path=model_path,
- output_directory=output_directory,
- source=FileTranscriptionTask.Source.FOLDER_WATCH,
- delete_source_file=self.preferences.delete_processed_files,
- )
- self.task_found.emit(task)
- self.paths_emitted.add(file_path)
-
- # Filter out hidden directories and add new subdirectories to the watcher
- dirnames[:] = [d for d in dirnames if not d.startswith(".")]
- for dirname in dirnames:
- subdir_path = os.path.join(dirpath, dirname)
- if subdir_path not in self.directories():
- self.addPath(subdir_path)
diff --git a/buzz/widgets/transcription_tasks_table_widget.py b/buzz/widgets/transcription_tasks_table_widget.py
deleted file mode 100644
index 3fe0db7b..00000000
--- a/buzz/widgets/transcription_tasks_table_widget.py
+++ /dev/null
@@ -1,807 +0,0 @@
-import enum
-import logging
-import os
-from dataclasses import dataclass
-from datetime import datetime, timedelta
-from enum import auto
-from typing import Optional, List
-from uuid import UUID
-
-from PyQt6 import QtGui
-from PyQt6.QtCore import Qt
-from PyQt6.QtCore import pyqtSignal, QModelIndex
-from PyQt6.QtSql import QSqlTableModel, QSqlRecord
-from PyQt6.QtGui import QKeySequence
-from PyQt6.QtWidgets import (
- QApplication,
- QWidget,
- QMenu,
- QHeaderView,
- QTableView,
- QAbstractItemView,
- QStyledItemDelegate,
-)
-
-from buzz.db.entity.transcription import Transcription
-from buzz.locale import _
-from buzz.settings.settings import Settings
-from buzz.transcriber.transcriber import FileTranscriptionTask, Task, TASK_LABEL_TRANSLATIONS
-from buzz.widgets.record_delegate import RecordDelegate
-from buzz.widgets.transcription_record import TranscriptionRecord
-
-
-class Column(enum.Enum):
- ID = 0
- ERROR_MESSAGE = 1
- EXPORT_FORMATS = 2
- FILE = 3
- OUTPUT_FOLDER = 4
- PROGRESS = 5
- LANGUAGE = 6
- MODEL_TYPE = 7
- SOURCE = 8
- STATUS = 9
- TASK = 10
- TIME_ENDED = 11
- TIME_QUEUED = 12
- TIME_STARTED = 13
- URL = 14
- WHISPER_MODEL_SIZE = 15
- HUGGING_FACE_MODEL_ID = 16
- WORD_LEVEL_TIMINGS = 17
- EXTRACT_SPEECH = 18
- NAME = 19
- NOTES = 20
-
-
-@dataclass
-class ColDef:
- id: str
- header: str
- column: Column
- width: Optional[int] = None
- delegate: Optional[QStyledItemDelegate] = None
- hidden_toggleable: bool = True
-
-
-def format_record_status_text(record: QSqlRecord) -> str:
- status = FileTranscriptionTask.Status(record.value("status"))
- match status:
- case FileTranscriptionTask.Status.IN_PROGRESS:
- in_progress_label = _("In Progress")
- return f'{in_progress_label} ({record.value("progress") :.0%})'
- case FileTranscriptionTask.Status.COMPLETED:
- status = _("Completed")
- started_at = record.value("time_started")
- completed_at = record.value("time_ended")
- if started_at != "" and completed_at != "":
- status += f" ({TranscriptionTasksTableWidget.format_timedelta(datetime.fromisoformat(completed_at) - datetime.fromisoformat(started_at))})"
- return status
- case FileTranscriptionTask.Status.FAILED:
- failed_label = _("Failed")
- return f'{failed_label} ({record.value("error_message")})'
- case FileTranscriptionTask.Status.CANCELED:
- return _("Canceled")
- case FileTranscriptionTask.Status.QUEUED:
- return _("Queued")
- case _: # Case to handle UNKNOWN status
- return ""
-
-column_definitions = [
- ColDef(
- id="file_name",
- header=_("File Name / URL"),
- column=Column.FILE,
- width=400,
- delegate=RecordDelegate(
- text_getter=lambda record: record.value("name") or (
- os.path.basename(record.value("file")) if record.value("file")
- else record.value("url") or ""
- )
- ),
- hidden_toggleable=False,
- ),
- ColDef(
- id="model",
- header=_("Model"),
- column=Column.MODEL_TYPE,
- width=180,
- delegate=RecordDelegate(
- text_getter=lambda record: str(TranscriptionRecord.model(record))
- ),
- ),
- ColDef(
- id="task",
- header=_("Task"),
- column=Column.TASK,
- width=120,
- delegate=RecordDelegate(
- text_getter=lambda record: TASK_LABEL_TRANSLATIONS[Task(record.value("task"))]
- ),
- ),
- ColDef(
- id="status",
- header=_("Status"),
- column=Column.STATUS,
- width=180,
- delegate=RecordDelegate(text_getter=format_record_status_text),
- hidden_toggleable=True,
- ),
-
- ColDef(
- id="date_completed",
- header=_("Date Completed"),
- column=Column.TIME_ENDED,
- width=180,
- delegate=RecordDelegate(
- text_getter=lambda record: datetime.fromisoformat(
- record.value("time_ended")
- ).strftime("%Y-%m-%d %H:%M:%S")
- if record.value("time_ended") != ""
- else ""
- ),
- ), ColDef(
- id="date_added",
- header=_("Date Added"),
- column=Column.TIME_QUEUED,
- width=180,
- delegate=RecordDelegate(
- text_getter=lambda record: datetime.fromisoformat(
- record.value("time_queued")
- ).strftime("%Y-%m-%d %H:%M:%S")
- ),
- ),
- ColDef(
- id="notes",
- header=_("Notes"),
- column=Column.NOTES,
- width=300,
- delegate=RecordDelegate(
- text_getter=lambda record: record.value("notes") or ""
- ),
- hidden_toggleable=True,
- ),
-]
-
-class TranscriptionTasksTableHeaderView(QHeaderView):
- def __init__(self, orientation, parent=None):
- super().__init__(orientation, parent)
-
- def contextMenuEvent(self, event):
- menu = QMenu(self)
-
- # Add reset column order option
- menu.addAction(_("Reset Column Order")).triggered.connect(self.parent().reset_column_order)
- menu.addSeparator()
-
- # Add column visibility toggles
- for definition in column_definitions:
- if definition.hidden_toggleable:
- action = menu.addAction(definition.header)
- action.setCheckable(True)
- action.setChecked(not self.parent().isColumnHidden(definition.column.value))
- action.toggled.connect(
- lambda checked, column_index=definition.column.value: self.on_column_checked(
- column_index, checked
- )
- )
- menu.exec(event.globalPos())
-
- def on_column_checked(self, column_index: int, checked: bool):
- # Find the column definition for this index
- column_def = None
- for definition in column_definitions:
- if definition.column.value == column_index:
- column_def = definition
- break
-
- # If we're hiding the column, save its current width first
- if not checked and not self.parent().isColumnHidden(column_index):
- current_width = self.parent().columnWidth(column_index)
- if current_width > 0: # Only save if there's a meaningful width
- self.parent().settings.begin_group(self.parent().settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_WIDTHS)
- self.parent().settings.settings.setValue(column_def.id, current_width)
- self.parent().settings.end_group()
-
- # Update the visibility state on the table view (not header view)
- self.parent().setColumnHidden(column_index, not checked)
-
- # Save current column order before any reloading
- self.parent().save_column_order()
-
- # Save both visibility and widths after the change
- self.parent().save_column_visibility()
- self.parent().save_column_widths()
-
- # Ensure settings are synchronized
- self.parent().settings.settings.sync()
-
- # Force a complete refresh of the table
- self.parent().viewport().update()
- self.parent().repaint()
- self.parent().horizontalHeader().update()
- self.parent().updateGeometry()
- self.parent().adjustSize()
-
- # Force a model refresh to ensure the view is updated
- self.parent().model().layoutChanged.emit()
-
- self.parent().reload_column_order_from_settings()
-
-class TranscriptionTasksTableWidget(QTableView):
- return_clicked = pyqtSignal()
- delete_requested = pyqtSignal()
-
- def __init__(self, parent: Optional[QWidget] = None):
- super().__init__(parent)
- self.transcription_service = None
-
- self.setHorizontalHeader(TranscriptionTasksTableHeaderView(Qt.Orientation.Horizontal, self))
-
- self._model = QSqlTableModel()
- self._model.setTable("transcription")
- self._model.setEditStrategy(QSqlTableModel.EditStrategy.OnManualSubmit)
- self._model.setSort(Column.TIME_QUEUED.value, Qt.SortOrder.DescendingOrder)
-
- self.setModel(self._model)
-
- for i in range(self.model().columnCount()):
- self.hideColumn(i)
-
- self.settings = Settings()
-
- # Set up column headers and delegates
- for definition in column_definitions:
- self.model().setHeaderData(
- definition.column.value,
- Qt.Orientation.Horizontal,
- definition.header,
- )
- if definition.delegate is not None:
- self.setItemDelegateForColumn(
- definition.column.value, definition.delegate
- )
-
- # Load column visibility
- self.load_column_visibility()
-
- self.model().select()
- self.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
- self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
- self.verticalHeader().hide()
- self.setAlternatingRowColors(True)
-
- # Enable column sorting and moving
- self.setSortingEnabled(True)
- self.horizontalHeader().setSectionsMovable(True)
- self.horizontalHeader().setSectionsClickable(True)
- self.horizontalHeader().setSortIndicatorShown(True)
-
- # Connect signals for column resize and move
- self.horizontalHeader().sectionResized.connect(self.on_column_resized)
- self.horizontalHeader().sectionMoved.connect(self.on_column_moved)
- self.horizontalHeader().sortIndicatorChanged.connect(self.on_sort_indicator_changed)
-
- # Load saved column order, widths, and sort state
- self.load_column_order()
- self.load_column_widths()
- self.load_sort_state()
-
-
- # Reload column visibility after all reordering is complete
- self.load_column_visibility()
-
- def contextMenuEvent(self, event):
- menu = QMenu(self)
-
- # Add transcription actions if a row is selected
- selected_rows = self.selectionModel().selectedRows()
- if selected_rows:
- transcription = self.transcription(selected_rows[0])
-
- # Add restart/continue action for failed/canceled tasks
- if transcription.status in ["failed", "canceled"]:
- restart_action = menu.addAction(_("Restart Transcription"))
- restart_action.triggered.connect(self.on_restart_transcription_action)
- menu.addSeparator()
-
- rename_action = menu.addAction(_("Rename"))
- rename_action.triggered.connect(self.on_rename_action)
-
- notes_action = menu.addAction(_("Add/Edit Notes"))
- notes_action.triggered.connect(self.on_notes_action)
-
- menu.exec(event.globalPos())
-
- def save_column_visibility(self):
- self.settings.begin_group(
- Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_VISIBILITY
- )
- for definition in column_definitions:
- self.settings.settings.setValue(
- definition.id, not self.isColumnHidden(definition.column.value)
- )
- self.settings.end_group()
-
- def on_column_resized(self, logical_index: int, old_size: int, new_size: int):
- """Handle column resize events"""
- self.save_column_widths()
-
- def on_column_moved(self, logical_index: int, old_visual_index: int, new_visual_index: int):
- """Handle column move events"""
- self.save_column_order()
- # Refresh visibility after column move to ensure it's maintained
- self.load_column_visibility()
-
- def on_sort_indicator_changed(self, logical_index: int, order: Qt.SortOrder):
- """Handle sort indicator change events"""
- self.save_sort_state()
-
- def on_double_click(self, index: QModelIndex):
- """Handle double-click events - trigger notes edit for notes column"""
- if index.column() == Column.NOTES.value:
- self.on_notes_action()
-
- def save_column_widths(self):
- """Save current column widths to settings"""
- self.settings.begin_group(Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_WIDTHS)
- for definition in column_definitions:
- # Only save width if column is visible and has a meaningful width
- if not self.isColumnHidden(definition.column.value):
- width = self.columnWidth(definition.column.value)
- if width > 0: # Only save if there's a meaningful width
- self.settings.settings.setValue(definition.id, width)
- self.settings.end_group()
-
- def save_column_order(self):
- """Save current column order to settings"""
- self.settings.begin_group(Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_ORDER)
- header = self.horizontalHeader()
- for visual_index in range(header.count()):
- logical_index = header.logicalIndex(visual_index)
- # Find the column definition for this logical index
- for definition in column_definitions:
- if definition.column.value == logical_index:
- self.settings.settings.setValue(definition.id, visual_index)
- break
- self.settings.end_group()
-
- def load_column_widths(self):
- """Load saved column widths from settings"""
- self.settings.begin_group(Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_WIDTHS)
- for definition in column_definitions:
- if definition.width is not None: # Only load if column has a default width
- saved_width = self.settings.settings.value(definition.id, definition.width)
- if saved_width is not None:
- self.setColumnWidth(definition.column.value, int(saved_width))
- self.settings.end_group()
-
- def save_sort_state(self):
- """Save current sort state to settings"""
- self.settings.begin_group(Settings.Key.TRANSCRIPTION_TASKS_TABLE_SORT_STATE)
- header = self.horizontalHeader()
- self.settings.settings.setValue("column", header.sortIndicatorSection())
- self.settings.settings.setValue("order", header.sortIndicatorOrder().value)
- self.settings.end_group()
-
- def load_sort_state(self):
- """Load saved sort state from settings"""
- self.settings.begin_group(Settings.Key.TRANSCRIPTION_TASKS_TABLE_SORT_STATE)
- column = self.settings.settings.value("column")
- order = self.settings.settings.value("order")
- self.settings.end_group()
-
- if column is not None and order is not None:
- sort_order = Qt.SortOrder(int(order))
- self.sortByColumn(int(column), sort_order)
-
- def load_column_visibility(self):
- """Load saved column visibility from settings"""
- self.settings.begin_group(Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_VISIBILITY)
- for definition in column_definitions:
- visible = True
- if definition.hidden_toggleable:
- value = self.settings.settings.value(definition.id, "true")
- visible = value in {"true", "True", True}
-
- self.setColumnHidden(definition.column.value, not visible)
- self.settings.end_group()
-
- # Force a refresh of the table layout
- self.horizontalHeader().update()
- self.viewport().update()
- self.updateGeometry()
-
- def load_column_order(self):
- """Load saved column order from settings"""
- self.settings.begin_group(Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_ORDER)
-
- # Create a mapping of column IDs to their saved visual positions
- column_positions = {}
- for definition in column_definitions:
- saved_position = self.settings.settings.value(definition.id)
- if saved_position is not None:
- column_positions[definition.column.value] = int(saved_position)
-
- self.settings.end_group()
-
- # Apply the saved order
- if column_positions:
- header = self.horizontalHeader()
- for logical_index, visual_position in column_positions.items():
- if 0 <= visual_position < header.count():
- header.moveSection(header.visualIndex(logical_index), visual_position)
-
- def reset_column_order(self):
- """Reset column order to default"""
-
- # Reset column widths to defaults
- for definition in column_definitions:
- if definition.width is not None:
- self.setColumnWidth(definition.column.value, definition.width)
-
- # Show all columns
- for definition in column_definitions:
- self.setColumnHidden(definition.column.value, False)
-
- # Restore default column order
- header = self.horizontalHeader()
- # Move each section to its default position in order
- # To avoid index shifting, move from left to right
- for target_visual_index, definition in enumerate(column_definitions):
- logical_index = definition.column.value
- current_visual_index = header.visualIndex(logical_index)
- if current_visual_index != target_visual_index:
- header.moveSection(current_visual_index, target_visual_index)
-
- # Reset sort to default (TIME_QUEUED descending)
- self.sortByColumn(Column.TIME_QUEUED.value, Qt.SortOrder.DescendingOrder)
-
- # Clear saved settings
- self.settings.begin_group(Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_ORDER)
- self.settings.settings.remove("")
- self.settings.end_group()
-
- self.settings.begin_group(Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_WIDTHS)
- self.settings.settings.remove("")
- self.settings.end_group()
-
- self.settings.begin_group(Settings.Key.TRANSCRIPTION_TASKS_TABLE_SORT_STATE)
- self.settings.settings.remove("")
- self.settings.end_group()
-
- # Save the reset state for visibility, widths, and sort
- self.save_column_visibility()
- self.save_column_widths()
- self.save_sort_state()
-
- # Force a refresh of the table layout
- self.horizontalHeader().update()
- self.viewport().update()
- self.updateGeometry()
-
- def reload_column_order_from_settings(self):
- """Reload column order, width, and visibility from settings"""
-
- # --- Load column visibility ---
- self.settings.begin_group(Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_VISIBILITY)
- visibility_settings = {}
- for definition in column_definitions:
- vis = self.settings.settings.value(definition.id)
- if vis is not None:
- visibility_settings[definition.id] = str(vis).lower() not in ("0", "false", "no")
- self.settings.end_group()
-
- # --- Load column widths ---
- self.settings.begin_group(Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_WIDTHS)
- width_settings = {}
- for definition in column_definitions:
- width = self.settings.settings.value(definition.id)
- if width is not None:
- try:
- width_settings[definition.id] = int(width)
- except Exception:
- pass
- self.settings.end_group()
-
- # --- Load column order ---
- self.settings.begin_group(Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_ORDER)
- order_settings = {}
- for definition in column_definitions:
- pos = self.settings.settings.value(definition.id)
- if pos is not None:
- try:
- order_settings[definition.column.value] = int(pos)
- except Exception:
- pass
- self.settings.end_group()
-
- # --- Apply visibility, widths, and order ---
- header = self.horizontalHeader()
-
- # First, set visibility and width for each column
- for definition in column_definitions:
- is_visible = visibility_settings.get(definition.id, True)
- width = width_settings.get(definition.id, definition.width)
- self.setColumnHidden(definition.column.value, not is_visible)
- if width is not None:
- self.setColumnWidth(definition.column.value, max(width, 100))
-
- # Then, apply column order
- # Build a list of (logical_index, visual_position) for ALL columns (including hidden ones)
- all_columns = [
- (definition.column.value, order_settings.get(definition.column.value, idx))
- for idx, definition in enumerate(column_definitions)
- ]
- # Sort by saved visual position
- all_columns.sort(key=lambda x: x[1])
-
- # Move sections to match the saved order
- for target_visual, (logical_index, _) in enumerate(all_columns):
- current_visual = header.visualIndex(logical_index)
- if current_visual != target_visual:
- header.moveSection(current_visual, target_visual)
-
- def copy_selected_fields(self):
- selected_text = ""
- for row in self.selectionModel().selectedRows():
- row_index = row.row()
- file_name = self.model().data(self.model().index(row_index, Column.FILE.value))
- url = self.model().data(self.model().index(row_index, Column.URL.value))
-
- selected_text += f"{file_name}{url}\n"
-
- selected_text = selected_text.rstrip("\n")
- QApplication.clipboard().setText(selected_text)
-
- def mouseDoubleClickEvent(self, event: QtGui.QMouseEvent) -> None:
- """Override double-click to prevent default behavior when clicking on notes column"""
- index = self.indexAt(event.pos())
- if index.isValid() and index.column() == Column.NOTES.value:
- # Handle our custom double-click action without triggering default behavior
- self.on_double_click(index)
- event.accept()
- else:
- # For other columns, use default behavior
- super().mouseDoubleClickEvent(event)
-
- def keyPressEvent(self, event: QtGui.QKeyEvent) -> None:
- if event.key() == Qt.Key.Key_Return:
- self.return_clicked.emit()
-
- if event.key() == Qt.Key.Key_Delete:
- if self.selectionModel().selectedRows():
- self.delete_requested.emit()
- return
-
- if event.matches(QKeySequence.StandardKey.Copy):
- self.copy_selected_fields()
- return
-
- super().keyPressEvent(event)
-
- def selected_transcriptions(self) -> List[Transcription]:
- selected = self.selectionModel().selectedRows()
- return [self.transcription(row) for row in selected]
-
- def delete_transcriptions(self, rows: List[QModelIndex]):
- for row in rows:
- self.model().removeRow(row.row())
- self.model().submitAll()
-
- def transcription(self, index: QModelIndex) -> Transcription:
- return Transcription.from_record(self.model().record(index.row()))
-
- def refresh_all(self):
- self.model().select()
-
- def refresh_row(self, id: UUID):
- for i in range(self.model().rowCount()):
- record = self.model().record(i)
- if record.value("id") == str(id):
- self.model().selectRow(i)
- return
-
- @staticmethod
- def format_timedelta(delta: timedelta):
- mm, ss = divmod(delta.seconds, 60)
- result = f"{ss}s"
- if mm == 0:
- return result
- hh, mm = divmod(mm, 60)
- result = f"{mm}m {result}"
- if hh == 0:
- return result
- return f"{hh}h {result}"
-
- def on_rename_action(self):
- selected_rows = self.selectionModel().selectedRows()
- if not selected_rows:
- return
-
- # Get the first selected transcription
- transcription = self.transcription(selected_rows[0])
-
- # Get current name or fallback to file name
- current_name = transcription.name or (
- transcription.url if transcription.url
- else os.path.basename(transcription.file) if transcription.file
- else ""
- )
-
- # Show input dialog
- from PyQt6.QtWidgets import QInputDialog
- new_name, ok = QInputDialog.getText(
- self,
- _("Rename Transcription"),
- _("Enter new name:"),
- text=current_name
- )
-
- if ok and new_name.strip():
- # Update the transcription name
- from uuid import UUID
- self.transcription_service.update_transcription_name(
- UUID(transcription.id),
- new_name.strip()
- )
- self.refresh_all()
-
- def on_notes_action(self):
- selected_rows = self.selectionModel().selectedRows()
- if not selected_rows:
- return
-
- # Get the first selected transcription
- transcription = self.transcription(selected_rows[0])
-
- # Show input dialog for notes
- from PyQt6.QtWidgets import QInputDialog
- current_notes = transcription.notes or ""
- new_notes, ok = QInputDialog.getMultiLineText(
- self,
- _("Notes"),
- _("Enter some relevant notes for this transcription:"),
- text=current_notes
- )
-
- if ok:
- # Update the transcription notes
- from uuid import UUID
- self.transcription_service.update_transcription_notes(
- UUID(transcription.id),
- new_notes
- )
- self.refresh_all()
-
- def on_restart_transcription_action(self):
- """Restart transcription for failed or canceled tasks"""
- selected_rows = self.selectionModel().selectedRows()
- if not selected_rows:
- return
-
- # Get the first selected transcription
- transcription = self.transcription(selected_rows[0])
-
- # Check if the task can be restarted
- if transcription.status not in ["failed", "canceled"]:
- from PyQt6.QtWidgets import QMessageBox
- QMessageBox.information(
- self,
- _("Cannot Restart"),
- _("Only failed or canceled transcriptions can be restarted.")
- )
- return
-
- try:
- self.transcription_service.reset_transcription_for_restart(UUID(transcription.id))
- self._restart_transcription_task(transcription)
- self.refresh_all()
- except Exception as e:
- from PyQt6.QtWidgets import QMessageBox
- QMessageBox.warning(
- self,
- _("Error"),
- _("Failed to restart transcription: {}").format(str(e))
- )
-
- def _restart_transcription_task(self, transcription):
- """Create a new FileTranscriptionTask and add it to the queue worker"""
- from buzz.transcriber.transcriber import (
- FileTranscriptionTask,
- TranscriptionOptions,
- FileTranscriptionOptions,
- Task
- )
- from buzz.model_loader import TranscriptionModel, ModelType
- from buzz.transcriber.transcriber import OutputFormat
-
- # Recreate the transcription options from the database record
- from buzz.model_loader import WhisperModelSize
-
- # Convert string whisper_model_size to enum if it exists
- whisper_model_size = None
- if transcription.whisper_model_size:
- try:
- whisper_model_size = WhisperModelSize(transcription.whisper_model_size)
- except ValueError:
- # If the stored value is invalid, use a default
- whisper_model_size = WhisperModelSize.TINY
-
- transcription_options = TranscriptionOptions(
- language=transcription.language if transcription.language else None,
- task=Task(transcription.task) if transcription.task else Task.TRANSCRIBE,
- model=TranscriptionModel(
- model_type=ModelType(transcription.model_type) if transcription.model_type else ModelType.WHISPER,
- whisper_model_size=whisper_model_size,
- hugging_face_model_id=transcription.hugging_face_model_id
- ),
- word_level_timings=transcription.word_level_timings == "1" if transcription.word_level_timings else False,
- extract_speech=transcription.extract_speech == "1" if transcription.extract_speech else False,
- initial_prompt="", # Not stored in database, use default
- openai_access_token="", # Not stored in database, use default
- enable_llm_translation=False, # Not stored in database, use default
- llm_prompt="", # Not stored in database, use default
- llm_model="" # Not stored in database, use default
- )
-
- # Recreate the file transcription options
- output_formats = set()
- if transcription.export_formats:
- for format_str in transcription.export_formats.split(','):
- try:
- output_formats.add(OutputFormat(format_str.strip()))
- except ValueError:
- pass # Skip invalid formats
-
- file_transcription_options = FileTranscriptionOptions(
- url=transcription.url if transcription.url else None,
- output_formats=output_formats
- )
-
- # Get the model path from the transcription options
- model_path = transcription_options.model.get_local_model_path()
- if model_path is None:
- # If model is not available locally, we need to download it
- from buzz.model_loader import ModelDownloader
- ModelDownloader(model=transcription_options.model).run()
- model_path = transcription_options.model.get_local_model_path()
-
- if model_path is None:
- from PyQt6.QtWidgets import QMessageBox
- QMessageBox.warning(
- self,
- _("Error"),
- _("Could not restart transcription: model not available and could not be downloaded.")
- )
- return
-
- # Create the new task
- task = FileTranscriptionTask(
- transcription_options=transcription_options,
- file_transcription_options=file_transcription_options,
- model_path=model_path,
- file_path=transcription.file if transcription.file else None,
- url=transcription.url if transcription.url else None,
- output_directory=transcription.output_folder if transcription.output_folder else None,
- source=FileTranscriptionTask.Source(transcription.source) if transcription.source else FileTranscriptionTask.Source.FILE_IMPORT,
- uid=UUID(transcription.id)
- )
-
- # Add the task to the queue worker
- # We need to access the main window's transcriber worker
- # This is a bit of a hack, but it's the cleanest way given the current architecture
- main_window = self.parent()
- while main_window and not hasattr(main_window, 'transcriber_worker'):
- main_window = main_window.parent()
-
- if main_window and hasattr(main_window, 'transcriber_worker'):
- main_window.transcriber_worker.add_task(task)
- else:
- # Fallback: show error if we can't find the transcriber worker
- from PyQt6.QtWidgets import QMessageBox
- QMessageBox.warning(
- self,
- _("Error"),
- _("Could not restart transcription: transcriber worker not found.")
- )
\ No newline at end of file
diff --git a/buzz/widgets/transcription_viewer/__init__.py b/buzz/widgets/transcription_viewer/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/buzz/widgets/transcription_viewer/export_transcription_menu.py b/buzz/widgets/transcription_viewer/export_transcription_menu.py
deleted file mode 100644
index 2ef41a7f..00000000
--- a/buzz/widgets/transcription_viewer/export_transcription_menu.py
+++ /dev/null
@@ -1,95 +0,0 @@
-import logging
-from PyQt6.QtGui import QAction
-from PyQt6.QtCore import pyqtSignal
-from PyQt6.QtWidgets import QWidget, QMenu, QFileDialog
-
-from buzz.db.entity.transcription import Transcription
-from buzz.db.service.transcription_service import TranscriptionService
-from buzz.locale import _
-from buzz.transcriber.file_transcriber import write_output
-from buzz.transcriber.transcriber import (
- OutputFormat,
- Segment,
-)
-
-
-class ExportTranscriptionMenu(QMenu):
- def __init__(
- self,
- transcription: Transcription,
- transcription_service: TranscriptionService,
- has_translation: bool,
- translation: pyqtSignal,
- parent: QWidget | None = None,
- ):
- super().__init__(parent)
-
- self.transcription = transcription
- self.transcription_service = transcription_service
-
- translation.connect(self.on_translation_available)
-
- text_label = _("Text")
- translation_label = _("Translation")
- self.text_actions = [
- QAction(text=f"{output_format.value.upper()} - {text_label}", parent=self)
- for output_format in OutputFormat
- ]
- self.translation_actions = [
- QAction(text=f"{output_format.value.upper()} - {translation_label}", parent=self)
- for output_format in OutputFormat
- ]
- for action in self.translation_actions:
- action.setVisible(has_translation)
- actions = self.text_actions + self.translation_actions
- self.addActions(actions)
- self.triggered.connect(self.on_menu_triggered)
-
- @staticmethod
- def extract_format_and_segment_key(action_text: str):
- parts = action_text.split('-')
- output_format = parts[0].strip()
- label = parts[1].strip() if len(parts) > 1 else None
- segment_key = 'translation' if label == _('Translation') else 'text'
-
- return output_format, segment_key
-
- def on_translation_available(self):
- for action in self.translation_actions:
- action.setVisible(True)
-
- def on_menu_triggered(self, action: QAction):
- segments = [
- Segment(
- start=segment.start_time,
- end=segment.end_time,
- text=segment.text,
- translation=segment.translation)
- for segment in self.transcription_service.get_transcription_segments(
- transcription_id=self.transcription.id_as_uuid
- )
- ]
-
- output_format_value, segment_key = self.extract_format_and_segment_key(action.text())
- output_format = OutputFormat(output_format_value.lower())
-
- default_path = self.transcription.get_output_file_path(
- output_format=output_format
- )
-
- (output_file_path, nil) = QFileDialog.getSaveFileName(
- self,
- _("Save File"),
- default_path,
- _("Text files") + f" (*.{output_format.value})",
- )
-
- if output_file_path == "":
- return
-
- write_output(
- path=output_file_path,
- segments=segments,
- output_format=output_format,
- segment_key=segment_key
- )
diff --git a/buzz/widgets/transcription_viewer/speaker_identification_widget.py b/buzz/widgets/transcription_viewer/speaker_identification_widget.py
deleted file mode 100644
index 94368d0e..00000000
--- a/buzz/widgets/transcription_viewer/speaker_identification_widget.py
+++ /dev/null
@@ -1,800 +0,0 @@
-import re
-import os
-import logging
-import ssl
-import time
-import random
-from typing import Optional
-
-# Fix SSL certificate verification for bundled applications (macOS, Windows)
-# This must be done before importing libraries that download from Hugging Face
-try:
- import certifi
- os.environ.setdefault('REQUESTS_CA_BUNDLE', certifi.where())
- os.environ.setdefault('SSL_CERT_FILE', certifi.where())
- os.environ.setdefault('SSL_CERT_DIR', os.path.dirname(certifi.where()))
- # Also update the default SSL context for urllib
- ssl._create_default_https_context = lambda: ssl.create_default_context(cafile=certifi.where())
-except ImportError:
- pass
-
-import faster_whisper
-import torch
-from PyQt6.QtMultimedia import QMediaPlayer, QAudioOutput
-from PyQt6.QtCore import Qt, QThread, QObject, pyqtSignal, QUrl, QTimer
-from PyQt6.QtGui import QFont
-from PyQt6.QtWidgets import (
- QWidget,
- QFormLayout,
- QVBoxLayout,
- QHBoxLayout,
- QLabel,
- QProgressBar,
- QPushButton,
- QCheckBox,
- QGroupBox,
- QSpacerItem,
- QSizePolicy,
- QLayout,
-)
-from buzz.locale import _
-from buzz.db.entity.transcription import Transcription
-from buzz.db.service.transcription_service import TranscriptionService
-from buzz.paths import file_path_as_title
-from buzz.settings.settings import Settings
-from buzz.widgets.line_edit import LineEdit
-from buzz.transcriber.transcriber import Segment
-
-
-
-def process_in_batches(
- items,
- process_func,
- batch_size=200,
- chunk_size=230,
- smaller_batch_size=100,
- exception_types=(AssertionError,),
- **process_func_kwargs
-):
- """
- Process items in batches with automatic fallback to smaller batches on errors.
-
- This is a generic batch processing function that can be used with any processing
- function that has chunk size limitations. It automatically retries with smaller
- batches when specified exceptions occur.
-
- Args:
- items: List of items to process
- process_func: Callable that processes a batch. Should accept (batch, chunk_size, **kwargs)
- and return a list of results
- batch_size: Initial batch size (default: 200)
- chunk_size: Maximum chunk size for the processing function (default: 230)
- smaller_batch_size: Fallback batch size when errors occur (default: 100)
- exception_types: Tuple of exception types to catch and retry with smaller batches
- (default: (AssertionError,))
- **process_func_kwargs: Additional keyword arguments to pass to process_func
-
- Returns:
- List of processed results (concatenated from all batches)
-
- Example:
- >>> def my_predict(batch, chunk_size):
- ... return [f"processed_{item}" for item in batch]
- >>> results = process_in_batches(
- ... items=["a", "b", "c"],
- ... process_func=my_predict,
- ... batch_size=2
- ... )
- """
- all_results = []
-
- for i in range(0, len(items), batch_size):
- batch = items[i:i + batch_size]
- try:
- batch_results = process_func(batch, chunk_size=min(chunk_size, len(batch)), **process_func_kwargs)
- all_results.extend(batch_results)
- except exception_types as e:
- # If batch still fails, try with even smaller chunks
- logging.warning(f"Batch processing failed, trying smaller chunks: {e}")
- for j in range(0, len(batch), smaller_batch_size):
- smaller_batch = batch[j:j + smaller_batch_size]
- smaller_results = process_func(smaller_batch, chunk_size=min(chunk_size, len(smaller_batch)), **process_func_kwargs)
- all_results.extend(smaller_results)
-
- return all_results
-
-SENTENCE_END = re.compile(r'.*[.!?。!?]')
-
-class IdentificationWorker(QObject):
- finished = pyqtSignal(list)
- progress_update = pyqtSignal(str)
- error = pyqtSignal(str)
-
- def __init__(self, transcription, transcription_service):
- super().__init__()
- self.transcription = transcription
- self.transcription_service = transcription_service
- self._is_cancelled = False
-
- def cancel(self):
- """Request cancellation of the worker."""
- self._is_cancelled = True
-
- def get_transcript(self, audio, **kwargs) -> dict:
- buzz_segments = self.transcription_service.get_transcription_segments(
- transcription_id=self.transcription.id_as_uuid
- )
-
- segments = []
- words = []
- text = ""
- for buzz_segment in buzz_segments:
- words.append({
- 'word': buzz_segment.text + " ",
- 'start': buzz_segment.start_time / 100,
- 'end': buzz_segment.end_time / 100,
- })
- text += buzz_segment.text + " "
-
- if SENTENCE_END.match(buzz_segment.text):
- segments.append({
- 'text': text,
- 'words': words
- })
- words = []
- text = ""
-
- return {
- 'language': self.transcription.language,
- 'segments': segments
- }
-
- def run(self):
- try:
- from ctc_forced_aligner.ctc_forced_aligner import (
- generate_emissions,
- get_alignments,
- get_spans,
- load_alignment_model,
- postprocess_results,
- preprocess_text,
- )
- from whisper_diarization.helpers import (
- get_realigned_ws_mapping_with_punctuation,
- get_sentences_speaker_mapping,
- get_words_speaker_mapping,
- langs_to_iso,
- punct_model_langs,
- )
- from deepmultilingualpunctuation.deepmultilingualpunctuation import PunctuationModel
- from whisper_diarization.diarization import MSDDDiarizer
- except ImportError as e:
- logging.exception("Failed to import speaker identification libraries: %s", e)
- self.error.emit(
- _("Speaker identification is not available: failed to load required libraries.")
- + f"\n\n{e}"
- )
- return
-
- diarizer_model = None
- alignment_model = None
-
- try:
- logging.debug("Speaker identification worker: Starting")
- self.progress_update.emit(_("1/8 Collecting transcripts"))
-
- if self._is_cancelled:
- logging.debug("Speaker identification worker: Cancelled at step 1")
- return
-
- # Step 1 - Get transcript
- # TODO - Add detected language to the transcript, detect and store separately in metadata
- # Will also be relevant for template parsing of transcript file names
- # - See diarize.py for example on how to get this info from whisper transcript, maybe other whisper models also have it
- language = self.transcription.language if self.transcription.language else "en"
-
- segments = self.transcription_service.get_transcription_segments(
- transcription_id=self.transcription.id_as_uuid
- )
-
- full_transcript = " ".join(segment.text for segment in segments)
- full_transcript = re.sub(r' {2,}', ' ', full_transcript)
-
- if self._is_cancelled:
- logging.debug("Speaker identification worker: Cancelled at step 2")
- return
-
- self.progress_update.emit(_("2/8 Loading audio"))
- audio_waveform = faster_whisper.decode_audio(self.transcription.file)
-
- # Step 2 - Forced alignment
- force_cpu = os.getenv("BUZZ_FORCE_CPU", "false")
- use_cuda = torch.cuda.is_available() and force_cpu == "false"
- device = "cuda" if use_cuda else "cpu"
- torch_dtype = torch.float16 if use_cuda else torch.float32
-
- logging.debug(f"Speaker identification worker: Using device={device}")
-
- if self._is_cancelled:
- logging.debug("Speaker identification worker: Cancelled at step 3")
- return
-
- self.progress_update.emit(_("3/8 Loading alignment model"))
- alignment_model = None
- alignment_tokenizer = None
- for attempt in range(3):
- try:
- alignment_model, alignment_tokenizer = load_alignment_model(
- device,
- dtype=torch_dtype,
- )
- break
- except Exception as e:
- if attempt < 2:
- logging.warning(
- f"Speaker identification: Failed to load alignment model "
- f"(attempt {attempt + 1}/3), retrying: {e}"
- )
- # On retry, try using cached models only (offline mode)
- # Set at runtime by modifying the library constants directly
- # (env vars are only read at import time)
- try:
- import huggingface_hub.constants
- huggingface_hub.constants.HF_HUB_OFFLINE = True
- logging.debug("Speaker identification: Enabled HF offline mode")
- except Exception as offline_err:
- logging.warning(f"Failed to set offline mode: {offline_err}")
- self.progress_update.emit(
- _("3/8 Loading alignment model (retrying with cache...)")
- )
- time.sleep(2 ** attempt) # 1s, 2s backoff
- else:
- raise RuntimeError(
- _("Failed to load alignment model. "
- "Please check your internet connection and try again.")
- ) from e
-
- if self._is_cancelled:
- logging.debug("Speaker identification worker: Cancelled at step 4")
- return
-
- self.progress_update.emit(_("4/8 Processing audio"))
- logging.debug("Speaker identification worker: Generating emissions")
- emissions, stride = generate_emissions(
- alignment_model,
- torch.from_numpy(audio_waveform)
- .to(alignment_model.dtype)
- .to(alignment_model.device),
- batch_size=1 if device == "cpu" else 8,
- )
- logging.debug("Speaker identification worker: Emissions generated")
-
- # Clean up alignment model
- del alignment_model
- alignment_model = None
- torch.cuda.empty_cache()
-
- if self._is_cancelled:
- logging.debug("Speaker identification worker: Cancelled at step 5")
- return
-
- self.progress_update.emit(_("5/8 Preparing transcripts"))
- tokens_starred, text_starred = preprocess_text(
- full_transcript,
- romanize=True,
- language=langs_to_iso[language],
- )
-
- segments, scores, blank_token = get_alignments(
- emissions,
- tokens_starred,
- alignment_tokenizer,
- )
-
- spans = get_spans(tokens_starred, segments, blank_token)
-
- word_timestamps = postprocess_results(text_starred, spans, stride, scores)
-
- if self._is_cancelled:
- logging.debug("Speaker identification worker: Cancelled at step 6")
- return
-
- # Step 3 - Diarization
- self.progress_update.emit(_("6/8 Identifying speakers"))
-
- # Silence NeMo's verbose logging
- logging.getLogger("nemo_logging").setLevel(logging.ERROR)
- try:
- # Also try to silence NeMo's internal logging system
- from nemo.utils import logging as nemo_logging
- nemo_logging.setLevel(logging.ERROR)
- except (ImportError, AttributeError):
- pass
-
- logging.debug("Speaker identification worker: Creating diarizer model")
- diarizer_model = MSDDDiarizer(device)
- logging.debug("Speaker identification worker: Running diarization (this may take a while on CPU)")
- speaker_ts = diarizer_model.diarize(torch.from_numpy(audio_waveform).unsqueeze(0))
- logging.debug("Speaker identification worker: Diarization complete")
-
- if self._is_cancelled:
- logging.debug("Speaker identification worker: Cancelled after diarization")
- return
-
- # Clean up diarizer model immediately after use
- del diarizer_model
- diarizer_model = None
- torch.cuda.empty_cache()
-
- if self._is_cancelled:
- logging.debug("Speaker identification worker: Cancelled at step 7")
- return
-
- # Step 4 - Reading timestamps <> Speaker Labels mapping
- self.progress_update.emit(_("7/8 Mapping speakers to transcripts"))
-
- wsm = get_words_speaker_mapping(word_timestamps, speaker_ts, "start")
-
- if language in punct_model_langs:
- # restoring punctuation in the transcript to help realign the sentences
- punct_model = PunctuationModel(model="kredor/punctuate-all")
-
- words_list = list(map(lambda x: x["word"], wsm))
-
- # Process in batches to avoid chunk size errors
- def predict_wrapper(batch, chunk_size, **kwargs):
- return punct_model.predict(batch, chunk_size=chunk_size)
-
- labled_words = process_in_batches(
- items=words_list,
- process_func=predict_wrapper
- )
-
- ending_puncts = ".?!。!?"
- model_puncts = ".,;:!?。!?"
-
- # We don't want to punctuate U.S.A. with a period. Right?
- is_acronym = lambda x: re.fullmatch(r"\b(?:[a-zA-Z]\.){2,}", x)
-
- for word_dict, labeled_tuple in zip(wsm, labled_words):
- word = word_dict["word"]
- if (
- word
- and labeled_tuple[1] in ending_puncts
- and (word[-1] not in model_puncts or is_acronym(word))
- ):
- word += labeled_tuple[1]
- if word.endswith(".."):
- word = word.rstrip(".")
- word_dict["word"] = word
-
- else:
- logging.warning(
- f"Punctuation restoration is not available for {language} language."
- " Using the original punctuation."
- )
-
- wsm = get_realigned_ws_mapping_with_punctuation(wsm)
- ssm = get_sentences_speaker_mapping(wsm, speaker_ts)
-
- logging.debug("Speaker identification worker: Finished successfully")
- self.progress_update.emit(_("8/8 Identification done"))
- self.finished.emit(ssm)
-
- except Exception as e:
- logging.error(f"Speaker identification worker: Error - {e}", exc_info=True)
- self.progress_update.emit(_("0/0 Error identifying speakers"))
- self.error.emit(str(e))
- # Emit empty list so the UI can reset properly
- self.finished.emit([])
-
- finally:
- # Ensure cleanup happens regardless of how we exit
- logging.debug("Speaker identification worker: Cleaning up resources")
- if diarizer_model is not None:
- try:
- del diarizer_model
- except Exception:
- pass
- if alignment_model is not None:
- try:
- del alignment_model
- except Exception:
- pass
- torch.cuda.empty_cache()
- # Reset offline mode so it doesn't affect other operations
- try:
- import huggingface_hub.constants
- huggingface_hub.constants.HF_HUB_OFFLINE = False
- except Exception:
- pass
-
-
-class SpeakerIdentificationWidget(QWidget):
- resize_button_clicked = pyqtSignal()
- transcription: Transcription
- settings = Settings()
-
- def __init__(
- self,
- transcription: Transcription,
- transcription_service: TranscriptionService,
- parent: Optional["QWidget"] = None,
- flags: Qt.WindowType = Qt.WindowType.Widget,
- transcriptions_updated_signal: Optional[pyqtSignal] = None,
- ) -> None:
- super().__init__(parent, flags)
- self.transcription = transcription
- self.transcription_service = transcription_service
- self.transcriptions_updated_signal = transcriptions_updated_signal
-
- self.identification_result = None
-
- self.thread = None
- self.worker = None
- self.needs_layout_update = False
-
- self.setMinimumWidth(650)
- self.setMinimumHeight(400)
-
- self.setWindowTitle(file_path_as_title(transcription.file))
-
- layout = QFormLayout(self)
- layout.setSizeConstraint(QLayout.SizeConstraint.SetMinAndMaxSize)
-
- # Step 1: Identify speakers
- step_1_label = QLabel(_("Step 1: Identify speakers"), self)
- font = step_1_label.font()
- font.setWeight(QFont.Weight.Bold)
- step_1_label.setFont(font)
- layout.addRow(step_1_label)
-
- step_1_group_box = QGroupBox(self)
- step_1_group_box.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
- step_1_layout = QVBoxLayout(step_1_group_box)
-
- self.step_1_row = QHBoxLayout()
-
- self.step_1_button = QPushButton(_("Identify"))
- self.step_1_button.setMinimumWidth(200)
- self.step_1_button.clicked.connect(self.on_identify_button_clicked)
-
- self.cancel_button = QPushButton(_("Cancel"))
- self.cancel_button.setMinimumWidth(200)
- self.cancel_button.setVisible(False)
- self.cancel_button.clicked.connect(self.on_cancel_button_clicked)
-
- # Progress container with label and bar
- progress_container = QVBoxLayout()
-
- self.progress_label = QLabel(self)
- if os.path.isfile(self.transcription.file):
- self.progress_label.setText(_("Ready to identify speakers"))
- else:
- self.progress_label.setText(_("Audio file not found"))
- self.step_1_button.setEnabled(False)
-
- self.progress_bar = QProgressBar(self)
- self.progress_bar.setMinimumWidth(400)
- self.progress_bar.setRange(0, 8)
- self.progress_bar.setValue(0)
-
- progress_container.addWidget(self.progress_label)
- progress_container.addWidget(self.progress_bar)
-
- self.step_1_row.addLayout(progress_container)
-
- button_container = QVBoxLayout()
- button_container.addWidget(self.step_1_button)
- button_container.addWidget(self.cancel_button)
- self.step_1_row.addLayout(button_container)
-
- step_1_layout.addLayout(self.step_1_row)
-
- layout.addRow(step_1_group_box)
-
- # Spacer
- spacer = QSpacerItem(0, 10, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
- layout.addItem(spacer)
-
- # Step 2: Name speakers
- step_2_label = QLabel(_("Step 2: Name speakers"), self)
- font = step_2_label.font()
- font.setWeight(QFont.Weight.Bold)
- step_2_label.setFont(font)
- layout.addRow(step_2_label)
-
- self.step_2_group_box = QGroupBox(self)
- self.step_2_group_box.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
- self.step_2_group_box.setEnabled(False)
- step_2_layout = QVBoxLayout(self.step_2_group_box)
-
- self.speaker_preview_row = QVBoxLayout()
-
- self.speaker_0_input = LineEdit("Speaker 0", self)
-
- self.speaker_0_preview_button = QPushButton(_("Play sample"))
- self.speaker_0_preview_button.setMinimumWidth(200)
- self.speaker_0_preview_button.clicked.connect(lambda: self.on_speaker_preview("Speaker 0"))
-
- speaker_0_layout = QHBoxLayout()
- speaker_0_layout.addWidget(self.speaker_0_input)
- speaker_0_layout.addWidget(self.speaker_0_preview_button)
-
- self.speaker_preview_row.addLayout(speaker_0_layout)
-
- step_2_layout.addLayout(self.speaker_preview_row)
-
- layout.addRow(self.step_2_group_box)
-
- # Save button
- self.merge_speaker_sentences = QCheckBox(_("Merge speaker sentences"))
- self.merge_speaker_sentences.setChecked(True)
- self.merge_speaker_sentences.setEnabled(False)
- self.merge_speaker_sentences.setMinimumWidth(250)
-
- self.save_button = QPushButton(_("Save"))
- self.save_button.setEnabled(False)
- self.save_button.clicked.connect(self.on_save_button_clicked)
-
- layout.addRow(self.merge_speaker_sentences)
- layout.addRow(self.save_button)
-
- self.setLayout(layout)
-
- # Invisible preview player
- url = QUrl.fromLocalFile(self.transcription.file)
- self.player = QMediaPlayer()
- self.audio_output = QAudioOutput()
- self.player.setAudioOutput(self.audio_output)
- self.player.setSource(url)
- self.player_timer = None
-
- def on_identify_button_clicked(self):
- self.step_1_button.setEnabled(False)
- self.step_1_button.setVisible(False)
- self.cancel_button.setVisible(True)
-
- # Clean up any existing thread before starting a new one
- self._cleanup_thread()
-
- logging.debug("Speaker identification: Starting identification thread")
-
- self.thread = QThread()
- self.worker = IdentificationWorker(
- self.transcription,
- self.transcription_service
- )
- self.worker.moveToThread(self.thread)
- self.thread.started.connect(self.worker.run)
- self.worker.finished.connect(self._on_thread_finished)
- self.worker.progress_update.connect(self.on_progress_update)
- self.worker.error.connect(self.on_identification_error)
-
- self.thread.start()
-
- def on_cancel_button_clicked(self):
- """Handle cancel button click."""
- logging.debug("Speaker identification: Cancel requested by user")
- self.cancel_button.setEnabled(False)
- self.progress_label.setText(_("Cancelling..."))
- self._cleanup_thread()
- self._reset_buttons()
- self.progress_label.setText(_("Cancelled"))
- self.progress_bar.setValue(0)
-
- def _reset_buttons(self):
- """Reset identify/cancel buttons to initial state."""
- self.step_1_button.setVisible(True)
- self.step_1_button.setEnabled(True)
- self.cancel_button.setVisible(False)
- self.cancel_button.setEnabled(True)
-
- def _on_thread_finished(self, result):
- """Handle thread completion and cleanup."""
- logging.debug("Speaker identification: Thread finished")
- if self.thread is not None:
- self.thread.quit()
- self.thread.wait(5000)
- self._reset_buttons()
- self.on_identification_finished(result)
-
- def on_identification_error(self, error_message):
- """Handle identification error."""
- logging.error(f"Speaker identification error: {error_message}")
- self._reset_buttons()
- self.progress_bar.setValue(0)
-
- def on_progress_update(self, progress):
- self.progress_label.setText(progress)
-
- progress_value = 0
- if progress and progress[0].isdigit():
- progress_value = int(progress[0])
- self.progress_bar.setValue(progress_value)
- else:
- logging.error(f"Invalid progress format: {progress}")
-
- if progress_value == 8:
- self.step_2_group_box.setEnabled(True)
- self.merge_speaker_sentences.setEnabled(True)
- self.save_button.setEnabled(True)
-
- def on_identification_finished(self, result):
- self.identification_result = result
-
- # Handle empty results (error case)
- if not result:
- logging.debug("Speaker identification: Empty result received")
- return
-
- unique_speakers = {entry['speaker'] for entry in result}
-
- while self.speaker_preview_row.count():
- item = self.speaker_preview_row.takeAt(0)
- widget = item.widget()
- if widget:
- widget.deleteLater()
- else:
- layout = item.layout()
- if layout:
- while layout.count():
- sub_item = layout.takeAt(0)
- sub_widget = sub_item.widget()
- if sub_widget:
- sub_widget.deleteLater()
-
- for speaker in sorted(unique_speakers):
- speaker_input = LineEdit(speaker, self)
- speaker_input.setMinimumWidth(200)
-
- speaker_preview_button = QPushButton(_("Play sample"))
- speaker_preview_button.setMinimumWidth(200)
- speaker_preview_button.clicked.connect(lambda checked, s=speaker: self.on_speaker_preview(s))
-
- speaker_layout = QHBoxLayout()
- speaker_layout.addWidget(speaker_input)
- speaker_layout.addWidget(speaker_preview_button)
-
- self.speaker_preview_row.addLayout(speaker_layout)
-
- # Trigger layout update to properly size the new widgets
- self.layout().activate()
- self.adjustSize()
- # Schedule update if window is minimized
- self.needs_layout_update = True
-
- def on_speaker_preview(self, speaker_id):
- if self.player_timer:
- self.player_timer.stop()
-
- speaker_records = [record for record in self.identification_result if record['speaker'] == speaker_id]
-
- if speaker_records:
- random_record = random.choice(speaker_records)
-
- start_time = random_record['start_time']
- end_time = random_record['end_time']
-
- self.player.setPosition(int(start_time))
- self.player.play()
-
- self.player_timer = QTimer(self)
- self.player_timer.setSingleShot(True)
- self.player_timer.timeout.connect(self.player.stop)
- self.player_timer.start(min(end_time, 10 * 1000)) # 10 seconds
-
- def on_save_button_clicked(self):
- speaker_names = []
- for i in range(self.speaker_preview_row.count()):
- item = self.speaker_preview_row.itemAt(i)
- if item.layout():
- for j in range(item.layout().count()):
- sub_item = item.layout().itemAt(j)
- widget = sub_item.widget()
- if isinstance(widget, LineEdit):
- speaker_names.append(widget.text())
-
- unique_speakers = {entry['speaker'] for entry in self.identification_result}
- original_speakers = sorted(unique_speakers)
- speaker_mapping = dict(zip(original_speakers, speaker_names))
-
- segments = []
- if self.merge_speaker_sentences.isChecked():
- previous_segment = None
-
- for entry in self.identification_result:
- speaker_name = speaker_mapping.get(entry['speaker'], entry['speaker'])
-
- if previous_segment and previous_segment['speaker'] == speaker_name:
- previous_segment['end_time'] = entry['end_time']
- previous_segment['text'] += " " + entry['text']
- else:
- if previous_segment:
- segment = Segment(
- start=previous_segment['start_time'],
- end=previous_segment['end_time'],
- text=f"{previous_segment['speaker']}: {previous_segment['text']}"
- )
- segments.append(segment)
- previous_segment = {
- 'start_time': entry['start_time'],
- 'end_time': entry['end_time'],
- 'speaker': speaker_name,
- 'text': entry['text']
- }
-
- if previous_segment:
- segment = Segment(
- start=previous_segment['start_time'],
- end=previous_segment['end_time'],
- text=f"{previous_segment['speaker']}: {previous_segment['text']}"
- )
- segments.append(segment)
- else:
- for entry in self.identification_result:
- speaker_name = speaker_mapping.get(entry['speaker'], entry['speaker'])
- segment = Segment(
- start=entry['start_time'],
- end=entry['end_time'],
- text=f"{speaker_name}: {entry['text']}"
- )
- segments.append(segment)
-
- new_transcript_id = self.transcription_service.copy_transcription(
- self.transcription.id_as_uuid
- )
-
- self.transcription_service.update_transcription_as_completed(new_transcript_id, segments)
-
- # TODO - See if we can get rows in the transcription viewer to be of variable height
- # If text is longer they should expand
- if self.transcriptions_updated_signal:
- self.transcriptions_updated_signal.emit(new_transcript_id)
-
- self.player.stop()
-
- if self.player_timer:
- self.player_timer.stop()
-
- self.close()
-
- def changeEvent(self, event):
- super().changeEvent(event)
-
- # Handle window activation (restored from minimized or brought to front)
- if self.needs_layout_update:
- self.layout().activate()
- self.adjustSize()
- self.needs_layout_update = False
-
- def closeEvent(self, event):
- self.hide()
-
- # Stop media player
- self.player.stop()
- if self.player_timer:
- self.player_timer.stop()
-
- # Clean up thread if running
- self._cleanup_thread()
-
- super().closeEvent(event)
-
- def _cleanup_thread(self):
- """Properly clean up the worker thread."""
- if self.worker is not None:
- # Request cancellation first
- self.worker.cancel()
-
- if self.thread is not None and self.thread.isRunning():
- logging.debug("Speaker identification: Stopping running thread")
- self.thread.quit()
- if not self.thread.wait(10000): # Wait up to 10 seconds
- logging.warning("Speaker identification: Thread did not quit, terminating")
- self.thread.terminate()
- if not self.thread.wait(2000):
- logging.error("Speaker identification: Thread failed to terminate")
-
- self.thread = None
- self.worker = None
diff --git a/buzz/widgets/transcription_viewer/transcription_resizer_widget.py b/buzz/widgets/transcription_viewer/transcription_resizer_widget.py
deleted file mode 100644
index 08f3cb48..00000000
--- a/buzz/widgets/transcription_viewer/transcription_resizer_widget.py
+++ /dev/null
@@ -1,444 +0,0 @@
-import re
-import os
-import logging
-import stable_whisper
-import srt
-from pathlib import Path
-from srt_equalizer import srt_equalizer
-from typing import Optional
-from PyQt6.QtCore import Qt, QThread, QObject, pyqtSignal
-from PyQt6.QtGui import QFont
-from PyQt6.QtWidgets import (
- QWidget,
- QFormLayout,
- QVBoxLayout,
- QHBoxLayout,
- QLabel,
- QSpinBox,
- QPushButton,
- QCheckBox,
- QGroupBox,
- QSpacerItem,
- QSizePolicy,
-)
-from buzz.locale import _, languages
-from buzz import whisper_audio
-from buzz.db.entity.transcription import Transcription
-from buzz.db.service.transcription_service import TranscriptionService
-from buzz.paths import file_path_as_title
-from buzz.settings.settings import Settings
-from buzz.widgets.line_edit import LineEdit
-from buzz.transcriber.transcriber import Segment
-from buzz.widgets.preferences_dialog.models.file_transcription_preferences import (
- FileTranscriptionPreferences,
-)
-
-
-SENTENCE_END = re.compile(r'.*[.!?。!?]')
-
-# Languages that don't use spaces between words
-NON_SPACE_LANGUAGES = {"zh", "ja", "th", "lo", "km", "my"}
-
-class TranscriptionWorker(QObject):
- finished = pyqtSignal(list)
-
- def __init__(self, transcription, transcription_options, transcription_service, regroup_string: str):
- super().__init__()
- self.transcription = transcription
- self.transcription_options = transcription_options
- self.transcription_service = transcription_service
- self.regroup_string = regroup_string
-
- def get_transcript(self, audio, **kwargs) -> dict:
- buzz_segments = self.transcription_service.get_transcription_segments(
- transcription_id=self.transcription.id_as_uuid
- )
-
- # Check if the language uses spaces between words
- language = self.transcription.language or ""
- is_non_space_language = language in NON_SPACE_LANGUAGES
-
- # For non-space languages, don't add spaces between words
- separator = "" if is_non_space_language else " "
-
- segments = []
- words = []
- text = ""
- for buzz_segment in buzz_segments:
- words.append({
- 'word': buzz_segment.text + separator,
- 'start': buzz_segment.start_time / 100,
- 'end': buzz_segment.end_time / 100,
- })
- text += buzz_segment.text + separator
-
- if SENTENCE_END.match(buzz_segment.text):
- segments.append({
- 'text': text,
- 'words': words
- })
- words = []
- text = ""
-
- # Add any remaining words that weren't terminated by sentence-ending punctuation
- if words:
- segments.append({
- 'text': text,
- 'words': words
- })
-
- return {
- 'language': self.transcription.language,
- 'segments': segments
- }
-
- def run(self):
- transcription_file = self.transcription.file
- transcription_file_exists = os.path.exists(transcription_file)
-
- transcription_file_path = Path(transcription_file)
- speech_path = transcription_file_path.with_name(f"{transcription_file_path.stem}_speech.mp3")
- if self.transcription_options.extract_speech and os.path.exists(speech_path):
- transcription_file = str(speech_path)
- transcription_file_exists = True
- # TODO - Fix VAD and Silence suppression that fails to work/download Vad model in compilded form on Mac and Windows
-
- try:
- result = stable_whisper.transcribe_any(
- self.get_transcript,
- audio = whisper_audio.load_audio(transcription_file),
- input_sr=whisper_audio.SAMPLE_RATE,
- # vad=transcription_file_exists,
- # suppress_silence=transcription_file_exists,
- vad=False,
- suppress_silence=False,
- regroup=self.regroup_string,
- check_sorted=False,
- )
- except Exception as e:
- logging.error(f"Error in TranscriptionWorker: {e}")
- return
-
- segments = []
- for segment in result.segments:
- segments.append(
- Segment(
- start=int(segment.start * 100),
- end=int(segment.end * 100),
- text=segment.text
- )
- )
-
- self.finished.emit(segments)
-
-
-class TranscriptionResizerWidget(QWidget):
- resize_button_clicked = pyqtSignal()
- transcription: Transcription
- settings = Settings()
-
- def __init__(
- self,
- transcription: Transcription,
- transcription_service: TranscriptionService,
- parent: Optional["QWidget"] = None,
- flags: Qt.WindowType = Qt.WindowType.Widget,
- transcriptions_updated_signal: Optional[pyqtSignal] = None,
- ) -> None:
- super().__init__(parent, flags)
- self.transcription = transcription
- self.transcription_service = transcription_service
- self.transcriptions_updated_signal = transcriptions_updated_signal
-
- self.new_transcript_id = None
- self.thread = None
- self.worker = None
-
- self.setMinimumWidth(600)
- self.setMinimumHeight(300)
-
- self.setWindowTitle(file_path_as_title(transcription.file))
-
- preferences = self.load_preferences()
-
- (
- self.transcription_options,
- self.file_transcription_options,
- ) = preferences.to_transcription_options(
- openai_access_token=''
- )
-
- layout = QFormLayout(self)
-
- # Extend segment endings
- extend_label = QLabel(_("Extend end time"), self)
- font = extend_label.font()
- font.setWeight(QFont.Weight.Bold)
- extend_label.setFont(font)
- layout.addRow(extend_label)
-
- extend_group_box = QGroupBox(self)
- extend_layout = QVBoxLayout(extend_group_box)
-
- self.extend_row = QHBoxLayout()
-
- self.extend_amount_label = QLabel(_("Extend endings by up to (seconds)"), self)
-
- self.extend_amount_input = LineEdit("0.2", self)
- self.extend_amount_input.setMaximumWidth(60)
-
- self.extend_button = QPushButton(_("Extend endings"))
- self.extend_button.clicked.connect(self.on_extend_button_clicked)
-
- self.extend_row.addWidget(self.extend_amount_label)
- self.extend_row.addWidget(self.extend_amount_input)
- self.extend_row.addWidget(self.extend_button)
-
- extend_layout.addLayout(self.extend_row)
-
- layout.addRow(extend_group_box)
-
- # Spacer
- spacer1 = QSpacerItem(0, 10, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
- layout.addItem(spacer1)
-
- # Resize longer subtitles
- resize_label = QLabel(_("Resize Options"), self)
- font = resize_label.font()
- font.setWeight(QFont.Weight.Bold)
- resize_label.setFont(font)
- layout.addRow(resize_label)
-
- resize_group_box = QGroupBox(self)
- resize_layout = QVBoxLayout(resize_group_box)
-
- self.resize_row = QHBoxLayout()
-
- self.desired_subtitle_length_label = QLabel(_("Desired subtitle length"), self)
-
- self.target_chars_spin_box = QSpinBox(self)
- self.target_chars_spin_box.setMinimum(1)
- self.target_chars_spin_box.setMaximum(100)
- self.target_chars_spin_box.setValue(42)
-
- self.resize_button = QPushButton(_("Resize"))
- self.resize_button.clicked.connect(self.on_resize_button_clicked)
-
- self.resize_row.addWidget(self.desired_subtitle_length_label)
- self.resize_row.addWidget(self.target_chars_spin_box)
- self.resize_row.addWidget(self.resize_button)
-
- resize_layout.addLayout(self.resize_row)
-
- resize_group_box.setEnabled(self.transcription.word_level_timings != 1)
- if self.transcription.word_level_timings == 1:
- resize_group_box.setToolTip(_("Available only if word level timings were disabled during transcription"))
-
- layout.addRow(resize_group_box)
-
- # Spacer
- spacer2 = QSpacerItem(0, 10, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
- layout.addItem(spacer2)
-
- # Merge words into subtitles
- merge_options_label = QLabel(_("Merge Options"), self)
- font = merge_options_label.font()
- font.setWeight(QFont.Weight.Bold)
- merge_options_label.setFont(font)
- layout.addRow(merge_options_label)
-
- merge_options_group_box = QGroupBox(self)
- merge_options_layout = QVBoxLayout(merge_options_group_box)
-
- self.merge_options_row = QVBoxLayout()
-
- self.merge_by_gap = QCheckBox(_("Merge by gap"))
- self.merge_by_gap.setChecked(True)
- self.merge_by_gap.setMinimumWidth(250)
- self.merge_by_gap_input = LineEdit("0.2", self)
- merge_by_gap_layout = QHBoxLayout()
- merge_by_gap_layout.addWidget(self.merge_by_gap)
- merge_by_gap_layout.addWidget(self.merge_by_gap_input)
-
- self.split_by_punctuation = QCheckBox(_("Split by punctuation"))
- self.split_by_punctuation.setChecked(True)
- self.split_by_punctuation.setMinimumWidth(250)
- self.split_by_punctuation_input = LineEdit(".* /./. /。/?/? /?/!/! /!/,/, ", self)
- split_by_punctuation_layout = QHBoxLayout()
- split_by_punctuation_layout.addWidget(self.split_by_punctuation)
- split_by_punctuation_layout.addWidget(self.split_by_punctuation_input)
-
- self.split_by_max_length = QCheckBox(_("Split by max length"))
- self.split_by_max_length.setChecked(True)
- self.split_by_max_length.setMinimumWidth(250)
- self.split_by_max_length_input = LineEdit("42", self)
- split_by_max_length_layout = QHBoxLayout()
- split_by_max_length_layout.addWidget(self.split_by_max_length)
- split_by_max_length_layout.addWidget(self.split_by_max_length_input)
-
- self.merge_options_row.addLayout(merge_by_gap_layout)
- self.merge_options_row.addLayout(split_by_punctuation_layout)
- self.merge_options_row.addLayout(split_by_max_length_layout)
-
- self.merge_button = QPushButton(_("Merge"))
- self.merge_button.clicked.connect(self.on_merge_button_clicked)
-
- self.merge_options_row.addWidget(self.merge_button)
-
- merge_options_layout.addLayout(self.merge_options_row)
-
- merge_options_group_box.setEnabled(self.transcription.word_level_timings == 1)
- if self.transcription.word_level_timings != 1:
- merge_options_group_box.setToolTip(_("Available only if word level timings were enabled during transcription"))
-
- layout.addRow(merge_options_group_box)
-
- self.setLayout(layout)
-
- def load_preferences(self):
- self.settings.settings.beginGroup("file_transcriber")
- preferences = FileTranscriptionPreferences.load(settings=self.settings.settings)
- self.settings.settings.endGroup()
- return preferences
-
- def on_resize_button_clicked(self):
- segments = self.transcription_service.get_transcription_segments(
- transcription_id=self.transcription.id_as_uuid
- )
-
- subs = []
- for segment in segments:
- subtitle = srt.Subtitle(
- index=segment.id,
- start=segment.start_time,
- end=segment.end_time,
- content=segment.text
- )
- subs.append(subtitle)
-
- resized_subs = []
- last_index = 0
-
- # Limit each subtitle to a maximum character length, splitting into
- # multiple subtitle items if necessary.
- for sub in subs:
- new_subs = srt_equalizer.split_subtitle(
- sub=sub, target_chars=self.target_chars_spin_box.value(), start_from_index=last_index, method="punctuation")
- last_index = new_subs[-1].index
- resized_subs.extend(new_subs)
-
- segments = [
- Segment(
- round(sub.start),
- round(sub.end),
- sub.content
- )
- for sub in resized_subs
- if round(sub.start) != round(sub.end)
- ]
-
- new_transcript_id = self.transcription_service.copy_transcription(
- self.transcription.id_as_uuid
- )
- self.transcription_service.update_transcription_as_completed(new_transcript_id, segments)
-
- if self.transcriptions_updated_signal:
- self.transcriptions_updated_signal.emit(new_transcript_id)
-
- def on_extend_button_clicked(self):
- try:
- extend_amount_seconds = float(self.extend_amount_input.text())
- except ValueError:
- extend_amount_seconds = 0.2
-
- # Convert seconds to milliseconds (internal time unit)
- extend_amount = int(extend_amount_seconds * 1000)
-
- segments = self.transcription_service.get_transcription_segments(
- transcription_id=self.transcription.id_as_uuid
- )
-
- extended_segments = []
- for i, segment in enumerate(segments):
- new_end = segment.end_time + extend_amount
-
- # Ensure segment end doesn't exceed start of next segment
- if i < len(segments) - 1:
- next_start = segments[i + 1].start_time
- new_end = min(new_end, next_start)
-
- extended_segments.append(
- Segment(
- start=segment.start_time,
- end=new_end,
- text=segment.text
- )
- )
-
- new_transcript_id = self.transcription_service.copy_transcription(
- self.transcription.id_as_uuid
- )
- self.transcription_service.update_transcription_as_completed(new_transcript_id, extended_segments)
-
- if self.transcriptions_updated_signal:
- self.transcriptions_updated_signal.emit(new_transcript_id)
-
- def on_merge_button_clicked(self):
- self.new_transcript_id = self.transcription_service.copy_transcription(
- self.transcription.id_as_uuid
- )
- self.transcription_service.update_transcription_progress(self.new_transcript_id, 0.0)
-
- if self.transcriptions_updated_signal:
- self.transcriptions_updated_signal.emit(self.new_transcript_id)
-
- regroup_string = ''
- if self.merge_by_gap.isChecked():
- regroup_string += f'mg={self.merge_by_gap_input.text()}'
-
- if self.split_by_max_length.isChecked():
- regroup_string += f'++{self.split_by_max_length_input.text()}+1'
-
- if self.split_by_punctuation.isChecked():
- if regroup_string:
- regroup_string += '_'
- regroup_string += f'sp={self.split_by_punctuation_input.text()}'
-
- if self.split_by_max_length.isChecked():
- if regroup_string:
- regroup_string += '_'
- regroup_string += f'sl={self.split_by_max_length_input.text()}'
-
- regroup_string = os.getenv("BUZZ_MERGE_REGROUP_RULE", regroup_string)
-
- self.hide()
-
- self.thread = QThread()
- self.worker = TranscriptionWorker(
- self.transcription,
- self.transcription_options,
- self.transcription_service,
- regroup_string
- )
- self.worker.moveToThread(self.thread)
- self.thread.started.connect(self.worker.run)
- self.worker.finished.connect(self.thread.quit)
- self.worker.finished.connect(self.worker.deleteLater)
- self.thread.finished.connect(self.thread.deleteLater)
- self.worker.finished.connect(self.on_transcription_completed)
-
- self.thread.start()
-
- def on_transcription_completed(self, segments):
- if self.new_transcript_id is not None:
- self.transcription_service.update_transcription_as_completed(self.new_transcript_id, segments)
-
- if self.transcriptions_updated_signal:
- self.transcriptions_updated_signal.emit(self.new_transcript_id)
-
- self.close()
-
- def closeEvent(self, event):
- self.hide()
-
- super().closeEvent(event)
diff --git a/buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py b/buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
deleted file mode 100644
index 2d3e5014..00000000
--- a/buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py
+++ /dev/null
@@ -1,385 +0,0 @@
-import enum
-import logging
-from dataclasses import dataclass
-from typing import Optional
-from uuid import UUID
-
-from PyQt6.QtCore import pyqtSignal, Qt, QModelIndex, QItemSelection, QEvent, QRegularExpression, QObject
-from PyQt6.QtGui import QRegularExpressionValidator
-from PyQt6.QtSql import QSqlTableModel, QSqlRecord
-from PyQt6.QtGui import QFontMetrics, QTextOption
-from PyQt6.QtWidgets import (
- QWidget,
- QTableView,
- QStyledItemDelegate,
- QAbstractItemView,
- QTextEdit,
- QLineEdit,
-)
-
-from buzz.locale import _
-from buzz.translator import Translator
-from buzz.transcriber.file_transcriber import to_timestamp
-
-
-class Column(enum.Enum):
- ID = 0
- END = enum.auto()
- START = enum.auto()
- TEXT = enum.auto()
- TRANSLATION = enum.auto()
- TRANSCRIPTION_ID = enum.auto()
-
-
-@dataclass
-class ColDef:
- id: str
- header: str
- column: Column
- delegate: Optional[QStyledItemDelegate] = None
-
-
-def parse_timestamp(timestamp_str: str) -> Optional[int]:
- """Parse timestamp string (HH:MM:SS.mmm) to milliseconds"""
- try:
- # Handle formats like "00:01:23.456" or "1:23.456" or "23.456"
- parts = timestamp_str.strip().split(':')
-
- if len(parts) == 3: # HH:MM:SS.mmm
- hours = int(parts[0])
- minutes = int(parts[1])
- seconds_parts = parts[2].split('.')
- elif len(parts) == 2: # MM:SS.mmm
- hours = 0
- minutes = int(parts[0])
- seconds_parts = parts[1].split('.')
- elif len(parts) == 1: # SS.mmm
- hours = 0
- minutes = 0
- seconds_parts = parts[0].split('.')
- else:
- return None
-
- seconds = int(seconds_parts[0])
- milliseconds = int(seconds_parts[1]) if len(seconds_parts) > 1 else 0
-
- total_ms = hours * 3600 * 1000 + minutes * 60 * 1000 + seconds * 1000 + milliseconds
- return total_ms
- except (ValueError, IndexError):
- return None
-
-
-class TimeStampLineEdit(QLineEdit):
- """Custom QLineEdit for timestamp editing with keyboard shortcuts"""
-
- def __init__(self, parent=None):
- super().__init__(parent)
- self._milliseconds = 0
-
- # Set up validator to only allow digits, colons, and dots
- regex = QRegularExpression(r'^[0-9:.]*$')
- validator = QRegularExpressionValidator(regex, self)
- self.setValidator(validator)
-
- def set_milliseconds(self, ms: int):
- self._milliseconds = ms
- self.setText(to_timestamp(ms))
-
- def get_milliseconds(self) -> int:
- parsed = parse_timestamp(self.text())
- if parsed is not None:
- return parsed
- return self._milliseconds
-
- def keyPressEvent(self, event):
- if event.text() == '+':
- self._milliseconds += 500 # Add 500ms (0.5 seconds)
- self.setText(to_timestamp(self._milliseconds))
- event.accept()
- elif event.text() == '-':
- self._milliseconds = max(0, self._milliseconds - 500) # Subtract 500ms
- self.setText(to_timestamp(self._milliseconds))
- event.accept()
- else:
- super().keyPressEvent(event)
-
- def focusOutEvent(self, event):
- # Strip any invalid characters and reformat on focus out
- parsed = parse_timestamp(self.text())
- if parsed is not None:
- self._milliseconds = parsed
- self.setText(to_timestamp(parsed))
- else:
- # If parsing failed, restore the last valid value
- self.setText(to_timestamp(self._milliseconds))
- super().focusOutEvent(event)
-
-
-class TimeStampDelegate(QStyledItemDelegate):
- def displayText(self, value, locale):
- return to_timestamp(value)
-
-
-class TimeStampEditorDelegate(QStyledItemDelegate):
- """Delegate for editing timestamps with overlap prevention"""
-
- timestamp_editing = pyqtSignal(int, int, int) # Signal: (row, column, new_value_ms)
-
- def createEditor(self, parent, option, index):
- editor = TimeStampLineEdit(parent)
- # Connect text changed signal to emit live updates
- editor.textChanged.connect(lambda: self.on_editor_text_changed(editor, index))
- return editor
-
- def on_editor_text_changed(self, editor, index):
- """Emit signal when editor text changes with the current value"""
- new_value_ms = editor.get_milliseconds()
- self.timestamp_editing.emit(index.row(), index.column(), new_value_ms)
-
- def setEditorData(self, editor, index):
- # Get value in milliseconds from database
- value = index.model().data(index, Qt.ItemDataRole.EditRole)
- if value is not None:
- editor.set_milliseconds(value)
-
- def setModelData(self, editor, model, index):
- # Get value in milliseconds from editor
- new_value_ms = editor.get_milliseconds()
- current_row = index.row()
- column = index.column()
-
- # Get current segment's start and end
- start_col = Column.START.value
- end_col = Column.END.value
-
- if column == start_col:
- # Editing START time
- end_time_ms = model.record(current_row).value("end_time")
-
- if end_time_ms is None:
- logging.warning("End time is None, cannot validate")
- return
-
- # Validate: start must be less than end
- if new_value_ms >= end_time_ms:
- logging.warning(f"Start time ({new_value_ms}) must be less than end time ({end_time_ms})")
- return
-
- # Check if new start overlaps with previous segment's end
- if current_row > 0:
- prev_end_time_ms = model.record(current_row - 1).value("end_time")
- if prev_end_time_ms is not None and new_value_ms < prev_end_time_ms:
- # Update previous segment's end to match new start
- model.setData(model.index(current_row - 1, end_col), new_value_ms)
-
- elif column == end_col:
- # Editing END time
- start_time_ms = model.record(current_row).value("start_time")
-
- if start_time_ms is None:
- logging.warning("Start time is None, cannot validate")
- return
-
- # Validate: end must be greater than start
- if new_value_ms <= start_time_ms:
- logging.warning(f"End time ({new_value_ms}) must be greater than start time ({start_time_ms})")
- return
-
- # Check if new end overlaps with next segment's start
- if current_row < model.rowCount() - 1:
- next_start_time_ms = model.record(current_row + 1).value("start_time")
- if next_start_time_ms is not None and new_value_ms > next_start_time_ms:
- # Update next segment's start to match new end
- model.setData(model.index(current_row + 1, start_col), new_value_ms)
-
- # Set the new value
- model.setData(index, new_value_ms)
-
- def displayText(self, value, locale):
- return to_timestamp(value)
-
-
-class CustomTextEdit(QTextEdit):
- """Custom QTextEdit that handles Tab/Enter/Esc keys to save and close editor"""
-
- def __init__(self, parent=None):
- super().__init__(parent)
-
- def keyPressEvent(self, event):
- # Tab, Enter, or Esc: save and close editor
- if event.key() in (Qt.Key.Key_Tab, Qt.Key.Key_Return, Qt.Key.Key_Enter, Qt.Key.Key_Escape):
- # Close the editor which will trigger setModelData to save
- self.clearFocus()
- event.accept()
- else:
- super().keyPressEvent(event)
-
-
-class WordWrapDelegate(QStyledItemDelegate):
- def createEditor(self, parent, option, index):
- editor = CustomTextEdit(parent)
- editor.setWordWrapMode(QTextOption.WrapMode.WordWrap)
- editor.setAcceptRichText(False)
- editor.setTabChangesFocus(True)
-
- return editor
-
- def setModelData(self, editor, model, index):
- model.setData(index, editor.toPlainText())
-
-
-class TranscriptionSegmentModel(QSqlTableModel):
- def __init__(self, transcription_id: UUID):
- super().__init__()
- self.setTable("transcription_segment")
- self.setEditStrategy(QSqlTableModel.EditStrategy.OnFieldChange)
- self.setFilter(f"transcription_id = '{transcription_id}'")
-
-
-class TranscriptionSegmentsEditorWidget(QTableView):
- PARENT_PADDINGS = 40
- segment_selected = pyqtSignal(QSqlRecord)
- timestamp_being_edited = pyqtSignal(int, int, int) # Signal: (row, column, new_value_ms)
-
- def keyPressEvent(self, event):
- # Allow Enter/Return to trigger editing
- if event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
- current_index = self.currentIndex()
- if current_index.isValid() and not self.state() == QAbstractItemView.State.EditingState:
- self.edit(current_index)
- event.accept()
- return
- super().keyPressEvent(event)
-
- def __init__(
- self,
- transcription_id: UUID,
- translator: Translator,
- parent: Optional[QWidget]
- ):
- super().__init__(parent)
-
- self._last_highlighted_row = -1
- self.translator = translator
- self.translator.translation.connect(self.update_translation)
-
- model = TranscriptionSegmentModel(transcription_id=transcription_id)
- self.setModel(model)
-
- timestamp_editor_delegate = TimeStampEditorDelegate()
- # Connect delegate's signal to widget's signal
- timestamp_editor_delegate.timestamp_editing.connect(self.timestamp_being_edited.emit)
-
- word_wrap_delegate = WordWrapDelegate()
-
- self.column_definitions: list[ColDef] = [
- ColDef("start", _("Start"), Column.START, delegate=timestamp_editor_delegate),
- ColDef("end", _("End"), Column.END, delegate=timestamp_editor_delegate),
- ColDef("text", _("Text"), Column.TEXT, delegate=word_wrap_delegate),
- ColDef("translation", _("Translation"), Column.TRANSLATION, delegate=word_wrap_delegate),
- ]
-
- for i in range(model.columnCount()):
- self.hideColumn(i)
-
- for definition in self.column_definitions:
- model.setHeaderData(
- definition.column.value,
- Qt.Orientation.Horizontal,
- definition.header,
- )
- self.showColumn(definition.column.value)
- if definition.delegate is not None:
- self.setItemDelegateForColumn(
- definition.column.value, definition.delegate
- )
-
- self.setAlternatingRowColors(True)
- self.verticalHeader().hide()
- self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
- self.setSelectionMode(QTableView.SelectionMode.SingleSelection)
- self.setEditTriggers(
- QAbstractItemView.EditTrigger.EditKeyPressed |
- QAbstractItemView.EditTrigger.DoubleClicked
- )
- self.selectionModel().selectionChanged.connect(self.on_selection_changed)
- model.select()
- model.rowsInserted.connect(self.init_row_height)
-
- self.has_translations = self.has_non_empty_translation()
-
- # Show start before end
- self.horizontalHeader().swapSections(1, 2)
-
- self.init_row_height()
-
- self.setColumnWidth(Column.START.value, 120)
- self.setColumnWidth(Column.END.value, 120)
-
- self.setWordWrap(True)
-
- def init_row_height(self):
- font_metrics = QFontMetrics(self.font())
- max_row_height = font_metrics.height() * 4
- row_count = self.model().rowCount()
-
- for row in range(row_count):
- self.setRowHeight(row, max_row_height)
-
- def has_non_empty_translation(self) -> bool:
- for i in range(self.model().rowCount()):
- if self.model().record(i).value("translation").strip():
- return True
- return False
-
- def resizeEvent(self, event):
- super().resizeEvent(event)
-
- if not self.has_translations:
- self.hideColumn(Column.TRANSLATION.value)
- else:
- self.showColumn(Column.TRANSLATION.value)
-
- text_column_count = 2 if self.has_translations else 1
-
- time_column_widths = self.columnWidth(Column.START.value) + self.columnWidth(Column.END.value)
- text_column_width = (
- int((self.parent().width() - self.PARENT_PADDINGS - time_column_widths) / text_column_count))
-
- self.setColumnWidth(Column.TEXT.value, text_column_width)
- self.setColumnWidth(Column.TRANSLATION.value, text_column_width)
-
- def update_translation(self, translation: str, segment_id: Optional[int] = None):
- self.has_translations = True
- self.resizeEvent(None)
-
- for row in range(self.model().rowCount()):
- if self.model().record(row).value("id") == segment_id:
- self.model().setData(self.model().index(row, Column.TRANSLATION.value), translation)
- break
-
- def on_selection_changed(
- self, selected: QItemSelection, _deselected: QItemSelection
- ):
- if selected.indexes():
- self.segment_selected.emit(self.segment(selected.indexes()[0]))
-
- def segment(self, index: QModelIndex) -> QSqlRecord:
- return self.model().record(index.row())
-
- def segments(self) -> list[QSqlRecord]:
- return [self.model().record(i) for i in range(self.model().rowCount())]
-
- def highlight_and_scroll_to_row(self, row_index: int):
- """Highlight a specific row and scroll it into view"""
- if 0 <= row_index < self.model().rowCount():
- # Only set focus if we're actually moving to a different row to avoid audio crackling
- if self._last_highlighted_row != row_index:
- self.setFocus()
- self._last_highlighted_row = row_index
-
- # Select the row
- self.selectRow(row_index)
- # Scroll to the row with better positioning
- model_index = self.model().index(row_index, 0)
- self.scrollTo(model_index, QAbstractItemView.ScrollHint.PositionAtCenter)
diff --git a/buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py b/buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
deleted file mode 100644
index 610f7d79..00000000
--- a/buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py
+++ /dev/null
@@ -1,66 +0,0 @@
-import logging
-from enum import Enum
-from typing import Optional
-
-from PyQt6.QtCore import pyqtSignal, Qt
-from PyQt6.QtGui import QKeySequence
-from PyQt6.QtWidgets import QToolButton, QWidget, QMenu
-
-from buzz.locale import _
-from buzz.settings.shortcut import Shortcut
-from buzz.settings.shortcuts import Shortcuts
-from buzz.widgets.icon import VisibilityIcon
-
-
-class ViewMode(Enum):
- TEXT = "Text"
- TRANSLATION = "Translation"
- TIMESTAMPS = "Timestamps"
-
-
-class TranscriptionViewModeToolButton(QToolButton):
- view_mode_changed = pyqtSignal(ViewMode)
-
- def __init__(
- self,
- shortcuts: Shortcuts,
- has_translation: bool,
- translation: pyqtSignal,
- parent: Optional[QWidget] = None
- ):
- super().__init__(parent)
-
- self.setText(_("View"))
- self.setIcon(VisibilityIcon(self))
- self.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
- self.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
- self.setMinimumWidth(80)
-
- translation.connect(self.on_translation_available)
-
- menu = QMenu(self)
-
- menu.addAction(
- _("Text"),
- QKeySequence(shortcuts.get(Shortcut.VIEW_TRANSCRIPT_TEXT)),
- lambda: self.view_mode_changed.emit(ViewMode.TEXT),
- )
-
- self.translation_action = menu.addAction(
- _("Translation"),
- QKeySequence(shortcuts.get(Shortcut.VIEW_TRANSCRIPT_TRANSLATION)),
- lambda: self.view_mode_changed.emit(ViewMode.TRANSLATION)
- )
- self.translation_action.setVisible(has_translation)
-
- menu.addAction(
- _("Timestamps"),
- QKeySequence(shortcuts.get(Shortcut.VIEW_TRANSCRIPT_TIMESTAMPS)),
- lambda: self.view_mode_changed.emit(ViewMode.TIMESTAMPS),
- )
-
- self.setMenu(menu)
- self.clicked.connect(self.showMenu)
-
- def on_translation_available(self):
- self.translation_action.setVisible(True)
diff --git a/buzz/widgets/transcription_viewer/transcription_viewer_widget.py b/buzz/widgets/transcription_viewer/transcription_viewer_widget.py
deleted file mode 100644
index 546936a6..00000000
--- a/buzz/widgets/transcription_viewer/transcription_viewer_widget.py
+++ /dev/null
@@ -1,1643 +0,0 @@
-import os
-import logging
-import platform
-from typing import Optional
-from uuid import UUID
-
-from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer
-from PyQt6.QtGui import QTextCursor
-from PyQt6.QtMultimedia import QMediaPlayer
-from PyQt6.QtSql import QSqlRecord
-from PyQt6.QtWidgets import (
- QWidget,
- QVBoxLayout,
- QHBoxLayout,
- QToolButton,
- QLabel,
- QMessageBox,
- QLineEdit,
- QPushButton,
- QFrame,
- QCheckBox,
- QComboBox,
- QScrollArea,
- QSizePolicy,
- QStackedWidget,
- QSplitter
-)
-
-from buzz.locale import _
-from buzz.db.entity.transcription import Transcription
-from buzz.db.service.transcription_service import TranscriptionService
-from buzz.paths import file_path_as_title
-from buzz.settings.shortcuts import Shortcuts
-from buzz.settings.shortcut import Shortcut
-from buzz.settings.settings import Settings
-from buzz.store.keyring_store import get_password, Key
-from buzz.transcriber.file_transcriber import is_video_file
-from buzz.widgets.audio_player import AudioPlayer
-from buzz.widgets.video_player import VideoPlayer
-from buzz.widgets.icon import (
- FileDownloadIcon,
- TranslateIcon,
- ResizeIcon,
- ScrollToCurrentIcon,
- VisibilityIcon,
- SpeakerIdentificationIcon,
-)
-from buzz.translator import Translator
-from buzz.widgets.text_display_box import TextDisplayBox
-from buzz.widgets.toolbar import ToolBar
-from buzz.transcriber.transcriber import TranscriptionOptions, Segment
-from buzz.widgets.transcriber.advanced_settings_dialog import AdvancedSettingsDialog
-from buzz.widgets.transcription_viewer.export_transcription_menu import (
- ExportTranscriptionMenu,
-)
-from buzz.widgets.preferences_dialog.models.file_transcription_preferences import (
- FileTranscriptionPreferences,
-)
-from buzz.widgets.transcription_viewer.transcription_segments_editor_widget import (
- TranscriptionSegmentsEditorWidget,
-)
-from buzz.widgets.transcription_viewer.transcription_view_mode_tool_button import (
- TranscriptionViewModeToolButton,
- ViewMode
-)
-from buzz.widgets.transcription_viewer.transcription_resizer_widget import TranscriptionResizerWidget
-
-# Underlying libs do not support intel Macs
-if not (platform.system() == "Darwin" and platform.machine() == "x86_64"):
- from buzz.widgets.transcription_viewer.speaker_identification_widget import SpeakerIdentificationWidget
-
-
-class TranscriptionViewerWidget(QWidget):
- resize_button_clicked = pyqtSignal()
- transcription: Transcription
- settings = Settings()
-
- def __init__(
- self,
- transcription: Transcription,
- transcription_service: TranscriptionService,
- shortcuts: Shortcuts,
- parent: Optional["QWidget"] = None,
- flags: Qt.WindowType = Qt.WindowType.Widget,
- transcriptions_updated_signal: Optional[pyqtSignal] = None,
- ) -> None:
- super().__init__(parent, flags)
- self.transcription = transcription
- self.transcription_service = transcription_service
- self.shortcuts = shortcuts
-
- self.setMinimumWidth(800)
- self.setMinimumHeight(500)
-
- self.setWindowTitle(file_path_as_title(transcription.file))
-
- self.transcription_resizer_dialog = None
- self.speaker_identification_dialog = None
- self.transcriptions_updated_signal = transcriptions_updated_signal
-
- self.translation_thread = None
- self.translator = None
- self.view_mode = ViewMode.TIMESTAMPS
-
- # Search functionality
- self.search_text = ""
- self.current_search_index = 0
- self.search_results = []
- self.search_debounce_timer = QTimer()
- self.search_debounce_timer.setSingleShot(True)
- self.search_debounce_timer.timeout.connect(self.perform_search)
-
- # Loop functionality
- self.segment_looping_enabled = self.settings.settings.value(
- "transcription_viewer/segment_looping_enabled", False, type=bool)
- # UI visibility preferences
- self.playback_controls_visible = self.settings.settings.value(
- "transcription_viewer/playback_controls_visible", False, type=bool)
- self.find_widget_visible = self.settings.settings.value(
- "transcription_viewer/find_widget_visible", False, type=bool)
-
- # Currently selected segment for loop functionality
- self.currently_selected_segment = None
-
- # Can't reuse this globally, as transcripts may get translated, so need to get them each time
- segments = self.transcription_service.get_transcription_segments(
- transcription_id=self.transcription.id_as_uuid
- )
- self.has_translations = any(segment.translation.strip()
- for segment in segments)
-
- self.openai_access_token = get_password(Key.OPENAI_API_KEY)
-
- preferences = self.load_preferences()
-
- (
- self.transcription_options,
- self.file_transcription_options,
- ) = preferences.to_transcription_options(
- openai_access_token=self.openai_access_token,
- )
-
- self.transcription_options_dialog = AdvancedSettingsDialog(
- transcription_options=self.transcription_options, parent=self
- )
- self.transcription_options_dialog.transcription_options_changed.connect(
- self.on_transcription_options_changed
- )
-
- self.translator = Translator(
- self.transcription_options,
- self.transcription_options_dialog,
- )
-
- self.translation_thread = QThread()
- self.translator.moveToThread(self.translation_thread)
-
- self.translation_thread.started.connect(self.translator.start)
-
- self.translation_thread.start()
-
- self.table_widget = TranscriptionSegmentsEditorWidget(
- transcription_id=UUID(hex=transcription.id),
- translator=self.translator,
-
- parent=self
- )
- self.table_widget.segment_selected.connect(self.on_segment_selected)
- self.table_widget.timestamp_being_edited.connect(
- self.on_timestamp_being_edited)
-
- self.text_display_box = TextDisplayBox(self)
-
- # Determine if source is video
- self.is_video = is_video_file(transcription.file) if transcription.file else False
-
- self.audio_player = AudioPlayer(file_path=transcription.file)
- self.video_player = None
-
- # Stack widget is to switch between audio and video
- self.media_player_stack = QStackedWidget()
- self.media_player_stack.addWidget(self.audio_player)
-
- # Only create video player if source is a video file
- if self.is_video:
- self.video_player = VideoPlayer(file_path=transcription.file)
- self.media_player_stack.addWidget(self.video_player)
-
- self.current_media_player = None
- self.load_transcription_media()
-
- # Connect audio player signals
- self.audio_player.position_ms_changed.connect(
- self.on_audio_player_position_ms_changed
- )
-
- # Connect video player signals (only if video player exists)
- if self.video_player:
- self.video_player.position_ms_changed.connect(
- self.on_audio_player_position_ms_changed
- )
-
- # Connect to playback state changes to automatically show controls
- self.audio_player.media_player.playbackStateChanged.connect(
- self.on_audio_playback_state_changed
- )
-
- if self.video_player:
- self.video_player.media_player.playbackStateChanged.connect(
- self.on_audio_playback_state_changed
- )
-
- # Create a better current segment display that handles long text
- self.current_segment_frame = QFrame()
- self.current_segment_frame.setFrameStyle(QFrame.Shape.NoFrame)
-
- segment_layout = QVBoxLayout(self.current_segment_frame)
- # Minimal margins for clean appearance
- segment_layout.setContentsMargins(4, 4, 4, 4)
- segment_layout.setSpacing(0) # No spacing between elements
-
- # Text display - centered with scroll capability (no header label)
- self.current_segment_text = QLabel("")
- self.current_segment_text.setAlignment(
- Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop)
- self.current_segment_text.setWordWrap(True)
- self.current_segment_text.setStyleSheet(
- "color: #666; line-height: 1.2; margin: 0; padding: 4px;")
- self.current_segment_text.setMinimumHeight(
- 60) # Ensure minimum height for text
- # Make it scrollable for long text
- self.current_segment_scroll_area = QScrollArea()
- self.current_segment_scroll_area.setWidget(self.current_segment_text)
- self.current_segment_scroll_area.setWidgetResizable(True)
- self.current_segment_scroll_area.setFrameStyle(QFrame.Shape.NoFrame)
- self.current_segment_scroll_area.setHorizontalScrollBarPolicy(
- Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
- self.current_segment_scroll_area.setVerticalScrollBarPolicy(
- Qt.ScrollBarPolicy.ScrollBarAsNeeded)
- self.current_segment_scroll_area.setStyleSheet(
- "QScrollBar:vertical { width: 12px; } QScrollBar::handle:vertical { background: #ccc; border-radius: 6px; }")
- # Ensure the text label can expand to show all content
- self.current_segment_text.setSizePolicy(
- QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred)
- # Add scroll area to layout (simplified single-widget layout)
- segment_layout.addWidget(self.current_segment_scroll_area)
-
- # Initially hide the frame until there's content
- self.current_segment_frame.hide()
-
- layout = QVBoxLayout(self)
-
- toolbar = ToolBar(self)
-
- view_mode_tool_button = TranscriptionViewModeToolButton(
- shortcuts,
- self.has_translations,
- self.translator.translation,
- )
- view_mode_tool_button.view_mode_changed.connect(
- self.on_view_mode_changed)
- toolbar.addWidget(view_mode_tool_button)
-
- export_tool_button = QToolButton()
- export_tool_button.setText(_("Export"))
- export_tool_button.setIcon(FileDownloadIcon(self))
- export_tool_button.setToolButtonStyle(
- Qt.ToolButtonStyle.ToolButtonTextBesideIcon
- )
- export_tool_button.setMinimumWidth(100)
-
- export_transcription_menu = ExportTranscriptionMenu(
- transcription,
- transcription_service,
- self.has_translations,
- self.translator.translation,
- self
- )
- export_tool_button.setMenu(export_transcription_menu)
- export_tool_button.setPopupMode(
- QToolButton.ToolButtonPopupMode.MenuButtonPopup)
- export_tool_button.clicked.connect(export_tool_button.showMenu)
- toolbar.addWidget(export_tool_button)
-
- translate_button = QToolButton()
- translate_button.setText(_("Translate"))
- translate_button.setIcon(TranslateIcon(self))
- translate_button.setToolButtonStyle(
- Qt.ToolButtonStyle.ToolButtonTextBesideIcon
- )
- translate_button.clicked.connect(self.on_translate_button_clicked)
-
- toolbar.addWidget(translate_button)
-
- resize_button = QToolButton()
- resize_button.setText(_("Resize"))
- resize_button.setObjectName("resize_button")
- resize_button.setIcon(ResizeIcon(self))
- resize_button.setToolButtonStyle(
- Qt.ToolButtonStyle.ToolButtonTextBesideIcon
- )
- resize_button.clicked.connect(self.on_resize_button_clicked)
-
- toolbar.addWidget(resize_button)
-
- # Underlying libs do not support intel Macs
- if not (platform.system() == "Darwin" and platform.machine() == "x86_64"):
- speaker_identification_button = QToolButton()
- speaker_identification_button.setText(_("Identify Speakers"))
- speaker_identification_button.setObjectName("speaker_identification_button")
- speaker_identification_button.setIcon(SpeakerIdentificationIcon(self))
- speaker_identification_button.setToolButtonStyle(
- Qt.ToolButtonStyle.ToolButtonTextBesideIcon
- )
- speaker_identification_button.clicked.connect(self.on_speaker_identification_button_clicked)
-
- toolbar.addWidget(speaker_identification_button)
-
- # Add Find button
- self.find_button = QToolButton()
- self.find_button.setText(_("Find"))
- # Using visibility icon for search
- self.find_button.setIcon(VisibilityIcon(self))
- self.find_button.setToolButtonStyle(
- Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
- self.find_button.setToolTip(_("Show/Hide Search Bar (Ctrl+F)"))
- # Make button checkable to show state
- self.find_button.setCheckable(True)
- # Initially unchecked (search hidden)
- self.find_button.setChecked(False)
- self.find_button.clicked.connect(self.toggle_search_bar_visibility)
- toolbar.addWidget(self.find_button)
-
- layout.setMenuBar(toolbar)
-
- # Search bar
- self.create_search_bar()
- # Search frame (minimal space)
- layout.addWidget(self.search_frame, 0) # Stretch factor 0 (minimal)
-
- # Use splitter for resizable media player
- self.media_splitter = QSplitter(Qt.Orientation.Vertical)
- self.media_splitter.setHandleWidth(8) # Make splitter handle easier to grab
- self.media_splitter.addWidget(self.table_widget)
- self.media_splitter.addWidget(self.media_player_stack)
- # Make splitter collapsible but with minimum sizes
- # Don't allow tabe to collapse completely
- self.media_splitter.setCollapsible(0, False)
- # Don't allow media player to collapse completely
- self.media_splitter.setCollapsible(1, False)
- # Connect splitter to save sizes when user resizes
- self.media_splitter.splitterMoved.connect(self.on_splitter_moved)
- # Loop controls section (minimal space)
- self.create_loop_controls()
- # Stretch factor 0 (minimal)
- layout.addWidget(self.loop_controls_frame, 0)
-
- # Add splitter to layout (table + media player)
- layout.addWidget(self.media_splitter, 1) # Stretch factor 1 (majority)
- # Text display box (minimal space)
- # Stretch factor 0 (minimal)
- layout.addWidget(self.text_display_box, 0)
-
- # Add current segment display (minimal space)
- # Stretch factor 0 (minimal)
- layout.addWidget(self.current_segment_frame, 1)
-
- # Initially hide the current segment frame until a segment is selected
- self.current_segment_frame.hide()
-
- self.setLayout(layout)
-
- # Set up keyboard shortcuts
- self.setup_shortcuts()
-
- # Restore UI state from settings
- self.restore_ui_state()
-
- # Restore geometry from settings
- self.load_geometry()
-
- self.reset_view()
-
- def load_transcription_media(self):
- if self.is_video and self.video_player:
- self.media_player_stack.setCurrentWidget(self.video_player)
- self.current_media_player = self.video_player
- else:
- self.media_player_stack.setCurrentWidget(self.audio_player)
- self.current_media_player = self.audio_player
-
- # Load splitter sizes after determining media type
- if hasattr(self, 'media_splitter'):
- self.load_splitter_sizes()
-
- def on_transcript_segment_clicked(self, segment):
- if not self.current_media_player:
- return
-
- start_time_ms = int(segment.start_time)
- self.current_media_player.set_position(start_time_ms)
- if self.current_media_player.media_player.playbackState() != QMediaPlayer.PlaybackState.PlayingState:
- self.current_media_player.media_player.play()
-
- def restore_ui_state(self):
- """Restore UI state from settings"""
- # Restore playback controls visibility
- if self.playback_controls_visible:
- self.show_loop_controls()
-
- # Restore find widget visibility
- if self.find_widget_visible:
- self.show_search_bar()
-
- def create_search_bar(self):
- """Create the search bar widget"""
- self.search_frame = QFrame()
- self.search_frame.setFrameStyle(QFrame.Shape.StyledPanel)
- self.search_frame.setMaximumHeight(60)
-
- search_layout = QHBoxLayout(self.search_frame)
- search_layout.setContentsMargins(10, 5, 10, 5)
-
- # Find label
- search_label = QLabel(_("Find:"))
- search_label.setStyleSheet("font-weight: bold;")
- search_layout.addWidget(search_label)
-
- # Find input - make it wider for better usability
- self.search_input = QLineEdit()
- self.search_input.setPlaceholderText(_("Enter text to find..."))
- self.search_input.textChanged.connect(self.on_search_text_changed)
- self.search_input.returnPressed.connect(self.search_next)
- self.search_input.setMinimumWidth(300) # Increased from 200 to 300
-
- # Add keyboard shortcuts for search navigation
- from PyQt6.QtGui import QKeySequence
- self.search_input.installEventFilter(self)
-
- search_layout.addWidget(self.search_input)
-
- # Search buttons - make them consistent height and remove hardcoded font sizes
- self.search_prev_button = QPushButton("↑")
- self.search_prev_button.setToolTip(_("Previous match (Shift+Enter)"))
- self.search_prev_button.clicked.connect(self.search_previous)
- self.search_prev_button.setEnabled(False)
- self.search_prev_button.setMaximumWidth(40)
- self.search_prev_button.setMinimumHeight(
- 30) # Ensure consistent height
- search_layout.addWidget(self.search_prev_button)
-
- self.search_next_button = QPushButton("↓")
- self.search_next_button.setToolTip(_("Next match (Ctrl+Enter)"))
- self.search_next_button.clicked.connect(self.search_next)
- self.search_next_button.setEnabled(False)
- self.search_next_button.setMaximumWidth(40)
- self.search_next_button.setMinimumHeight(
- 30) # Ensure consistent height
- search_layout.addWidget(self.search_next_button)
-
- # Clear button - make it bigger to accommodate different language translations
- self.clear_search_button = QPushButton(_("Clear"))
- self.clear_search_button.clicked.connect(self.clear_search)
- self.clear_search_button.setMaximumWidth(80) # Increased from 60 to 80
- self.clear_search_button.setMinimumHeight(
- 30) # Ensure consistent height
- search_layout.addWidget(self.clear_search_button)
-
- # Results label
- self.search_results_label = QLabel("")
- self.search_results_label.setStyleSheet("color: #666;")
- search_layout.addWidget(self.search_results_label)
-
- search_layout.addStretch()
-
- # Initially hide the search bar
- self.search_frame.hide()
-
- def create_loop_controls(self):
- """Create the loop controls widget"""
- self.loop_controls_frame = QFrame()
- self.loop_controls_frame.setFrameStyle(QFrame.Shape.StyledPanel)
- self.loop_controls_frame.setMaximumHeight(50)
-
- loop_layout = QHBoxLayout(self.loop_controls_frame)
- loop_layout.setContentsMargins(10, 5, 10, 5)
- # Add some spacing between elements for better visual separation
- loop_layout.setSpacing(8)
- # Loop controls label
- loop_label = QLabel(_("Playback Controls:"))
- loop_label.setStyleSheet("font-weight: bold;")
- loop_layout.addWidget(loop_label)
-
- # Loop toggle button
- self.loop_toggle = QCheckBox(_("Loop Segment"))
- self.loop_toggle.setChecked(self.segment_looping_enabled)
- self.loop_toggle.setToolTip(
- _("Enable/disable looping when clicking on transcript segments"))
- self.loop_toggle.toggled.connect(self.on_loop_toggle_changed)
- loop_layout.addWidget(self.loop_toggle)
-
- # Follow audio toggle button
- self.follow_audio_enabled = self.settings.settings.value(
- "transcription_viewer/follow_audio_enabled", False, type=bool)
- self.follow_audio_toggle = QCheckBox(_("Follow Audio"))
- self.follow_audio_toggle.setChecked(self.follow_audio_enabled)
- self.follow_audio_toggle.setToolTip(
- _("Enable/disable following the current audio position in the transcript. When enabled, automatically scrolls to current text."))
- self.follow_audio_toggle.toggled.connect(
- self.on_follow_audio_toggle_changed)
- loop_layout.addWidget(self.follow_audio_toggle)
-
- # Visual separator
- separator1 = QFrame()
- separator1.setFrameShape(QFrame.Shape.VLine)
- separator1.setFrameShadow(QFrame.Shadow.Sunken)
- separator1.setMaximumHeight(20)
- loop_layout.addWidget(separator1)
-
- # Speed controls
- speed_label = QLabel("Speed:")
- speed_label.setStyleSheet("font-weight: bold;")
- loop_layout.addWidget(speed_label)
-
- self.speed_combo = QComboBox()
- self.speed_combo.setEditable(True)
- self.speed_combo.addItems(
- ["0.5x", "0.75x", "1x", "1.25x", "1.5x", "2x"])
- self.speed_combo.setInsertPolicy(QComboBox.InsertPolicy.NoInsert)
- self.speed_combo.currentTextChanged.connect(self.on_speed_changed)
- self.speed_combo.setMaximumWidth(80)
- loop_layout.addWidget(self.speed_combo)
-
- self.speed_down_btn = QPushButton("-")
- self.speed_down_btn.setMaximumWidth(40) # Match search button width
- self.speed_down_btn.setMinimumHeight(30) # Match search button height
- self.speed_down_btn.clicked.connect(self.decrease_speed)
- loop_layout.addWidget(self.speed_down_btn)
-
- self.speed_up_btn = QPushButton("+")
- self.speed_up_btn.setMaximumWidth(40) # Match speed down button width
- self.speed_up_btn.setMinimumHeight(30) # Match search button height
- self.speed_up_btn.clicked.connect(self.increase_speed)
- loop_layout.addWidget(self.speed_up_btn)
-
- # Initialize speed control with current value from audio player
- self.initialize_speed_control()
-
- # Visual separator
- separator2 = QFrame()
- separator2.setFrameShape(QFrame.Shape.VLine)
- separator2.setFrameShadow(QFrame.Shadow.Sunken)
- separator2.setMaximumHeight(20)
- loop_layout.addWidget(separator2)
-
- # Scroll to current button
- self.scroll_to_current_button = QPushButton(_("Scroll to Current"))
- self.scroll_to_current_button.setIcon(ScrollToCurrentIcon(self))
- self.scroll_to_current_button.setToolTip(
- _("Scroll to the currently spoken text"))
- self.scroll_to_current_button.clicked.connect(
- self.on_scroll_to_current_button_clicked)
- self.scroll_to_current_button.setMinimumHeight(30)
- self.scroll_to_current_button.setStyleSheet(
- "QPushButton { padding: 4px 8px; }") # Better padding
- loop_layout.addWidget(self.scroll_to_current_button)
-
- loop_layout.addStretch()
-
- # Initially hide the loop controls frame
- self.loop_controls_frame.hide()
-
- def show_loop_controls(self):
- """Show the loop controls when audio is playing"""
- self.loop_controls_frame.show()
-
- # Save the visibility state to settings
- self.playback_controls_visible = True
- self.settings.settings.setValue(
- "transcription_viewer/playback_controls_visible", self.playback_controls_visible)
-
- def hide_loop_controls(self):
- """Hide the loop controls when audio is not playing"""
- self.loop_controls_frame.hide()
-
- # Save the visibility state to settings
- self.playback_controls_visible = False
- self.settings.settings.setValue(
- "transcription_viewer/playback_controls_visible", self.playback_controls_visible)
-
- def toggle_playback_controls_visibility(self):
- """Toggle the visibility of playback controls manually"""
- if self.loop_controls_frame.isVisible():
- self.hide_loop_controls()
- else:
- self.show_loop_controls()
-
- def toggle_audio_playback(self):
- """Toggle audio playback (play/pause)"""
- if self.current_media_player and self.current_media_player.media_player.playbackState() == QMediaPlayer.PlaybackState.PlayingState:
- self.current_media_player.media_player.pause()
- else:
- self.current_media_player.media_player.play()
-
- def replay_current_segment(self):
- """Rewind current segment to its start and play if not already playing"""
- if self.currently_selected_segment is None:
- return
-
- # Get the start time of the currently selected segment
- start_time = self.currently_selected_segment.value("start_time")
-
- # Set position to the start of the segment
- if self.current_media_player:
- self.current_media_player.set_position(start_time)
-
- # If audio is not playing, start playing
- if self.audio_player.media_player.playbackState() != QMediaPlayer.PlaybackState.PlayingState:
- self.audio_player.media_player.play()
-
- def decrease_segment_start(self):
- """Decrease the start time of the current segment by 0.5 seconds"""
- self._adjust_segment_timestamp("start_time", -500)
-
- def increase_segment_start(self):
- """Increase the start time of the current segment by 0.5 seconds"""
- self._adjust_segment_timestamp("start_time", 500)
-
- def decrease_segment_end(self):
- """Decrease the end time of the current segment by 0.5 seconds"""
- self._adjust_segment_timestamp("end_time", -500)
-
- def increase_segment_end(self):
- """Increase the end time of the current segment by 0.5 seconds"""
- self._adjust_segment_timestamp("end_time", 500)
-
- def _adjust_segment_timestamp(self, field: str, delta_ms: int):
- """Helper method to adjust a segment's timestamp"""
- if self.currently_selected_segment is None:
- return
-
- # Get current segment row and ID
- segment_id = self.currently_selected_segment.value("id")
- segments = self.table_widget.segments()
- current_row = -1
- for i, segment in enumerate(segments):
- if segment.value("id") == segment_id:
- current_row = i
- break
-
- if current_row == -1:
- return
-
- # Get FRESH current values from the model (not from cached segment)
- from buzz.widgets.transcription_viewer.transcription_segments_editor_widget import Column
- start_col = Column.START.value
- end_col = Column.END.value
-
- current_start_time = self.table_widget.model().record(
- current_row).value("start_time")
- current_end_time = self.table_widget.model().record(current_row).value("end_time")
-
- # Calculate new value based on CURRENT database value
- if field == "start_time":
- current_value = current_start_time
- other_value = current_end_time
- else:
- current_value = current_end_time
- other_value = current_start_time
-
- new_value = current_value + delta_ms
-
- if field == "start_time":
- # Ensure start time doesn't go below 0
- new_value = max(0, new_value)
- # Ensure start time is less than end time
- if new_value >= current_end_time:
- return
-
- # Check overlap with previous segment
- if current_row > 0:
- prev_end = self.table_widget.model().record(current_row - 1).value("end_time")
- if new_value < prev_end:
- # Update previous segment's end time
- self.table_widget.model().setData(
- self.table_widget.model().index(current_row - 1, end_col),
- new_value
- )
- else: # end_time
- # Ensure end time is greater than start time
- if new_value <= current_start_time:
- return
-
- # Check overlap with next segment
- if current_row < len(segments) - 1:
- next_start = self.table_widget.model().record(
- current_row + 1).value("start_time")
- if new_value > next_start:
- # Update next segment's start time
- self.table_widget.model().setData(
- self.table_widget.model().index(current_row + 1, start_col),
- new_value
- )
-
- # Update the timestamp
- column = start_col if field == "start_time" else end_col
- self.table_widget.model().setData(
- self.table_widget.model().index(current_row, column),
- new_value
- )
-
- # Refresh the currently_selected_segment reference with fresh data from model
- self.currently_selected_segment = self.table_widget.model().record(current_row)
-
- # Update loop range if looping is enabled
- if self.segment_looping_enabled:
- updated_start = self.currently_selected_segment.value("start_time")
- updated_end = self.currently_selected_segment.value("end_time")
-
- if self.current_media_player:
- self.current_media_player.set_range(
- (updated_start, updated_end))
-
- def on_audio_playback_state_changed(self, state):
- """Handle audio playback state changes to automatically show playback controls"""
- from PyQt6.QtMultimedia import QMediaPlayer
-
- if state == QMediaPlayer.PlaybackState.PlayingState:
- # Show playback controls when audio starts playing
- if self.view_mode == ViewMode.TIMESTAMPS:
- self.show_loop_controls()
-
- def initialize_speed_control(self):
- """Initialize the speed control with current value from audio player"""
- try:
- # Get current speed from audio player
- if self.current_media_player:
- current_speed = self.current_media_player.media_player.playbackRate()
- # Ensure it's within valid range
- current_speed = max(0.1, min(5.0, current_speed))
- # Set the combo box text
- speed_text = f"{current_speed:.2f}x"
- self.speed_combo.setCurrentText(speed_text)
- except Exception as e:
- logging.warning(f"Could not initialize speed control: {e}")
- # Default to 1.0x
- self.speed_combo.setCurrentText("1.0x")
-
- def on_speed_changed(self, speed_text: str):
- """Handle speed change from the combo box"""
- try:
- # Extract the numeric value from speed text (e.g., "1.5x" -> 1.5)
- clean_text = speed_text.replace('x', '').strip()
- speed_value = float(clean_text)
-
- # Clamp the speed value to valid range
- speed_value = max(0.1, min(5.0, speed_value))
-
- # Update the combo box text to show the clamped value
- if not speed_text.endswith('x'):
- speed_text = f"{speed_value:.2f}x"
-
- # Block signals to prevent recursion
- self.speed_combo.blockSignals(True)
- self.speed_combo.setCurrentText(speed_text)
- self.speed_combo.blockSignals(False)
-
- # Set the playback rate on the audio player
- if self.current_media_player:
- self.current_media_player.media_player.setPlaybackRate(
- speed_value)
- # Save the new rate to settings
- self.settings.set_value(
- self.settings.Key.AUDIO_PLAYBACK_RATE, speed_value)
- except ValueError:
- logging.warning(f"Invalid speed value: {speed_text}")
- # Reset to current valid value
- current_text = self.speed_combo.currentText()
- if current_text != speed_text:
- self.speed_combo.setCurrentText(current_text)
-
- def increase_speed(self):
- """Increase speed by 0.05"""
- current_speed = self.get_current_speed()
- new_speed = min(5.0, current_speed + 0.05)
- self.set_speed(new_speed)
-
- def decrease_speed(self):
- """Decrease speed by 0.05"""
- current_speed = self.get_current_speed()
- new_speed = max(0.1, current_speed - 0.05)
- self.set_speed(new_speed)
-
- def get_current_speed(self) -> float:
- """Get the current playback speed as a float"""
- try:
- speed_text = self.speed_combo.currentText()
- return float(speed_text.replace('x', ''))
- except ValueError:
- return 1.0
-
- def set_speed(self, speed: float):
- """Set the playback speed programmatically"""
- # Clamp the speed value to valid range
- speed = max(0.1, min(5.0, speed))
-
- # Update the combo box
- speed_text = f"{speed:.2f}x"
- self.speed_combo.setCurrentText(speed_text)
-
- # Set the playback rate on the audio player
- self.audio_player.media_player.setPlaybackRate(speed)
-
- # Save the new rate to settings
- self.settings.set_value(self.settings.Key.AUDIO_PLAYBACK_RATE, speed)
-
- def on_search_text_changed(self, text: str):
- """Handle search text changes"""
- self.search_text = text.strip()
- if self.search_text:
- # Debounce search to avoid UI jumping while typing
- if len(self.search_text) >= 2:
- self.search_debounce_timer.start(300) # 300ms delay
- self.search_frame.show()
- else:
- self.search_debounce_timer.stop()
- self.clear_search()
- # Don't hide the search frame immediately, let user clear it manually
-
- def perform_search(self):
- """Perform the actual search based on current view mode"""
- self.search_results = []
- self.current_search_index = 0
-
- if self.view_mode == ViewMode.TIMESTAMPS:
- self.search_in_table()
- else: # TEXT or TRANSLATION mode
- self.search_in_text()
-
- self.update_search_ui()
-
- def search_in_table(self):
- """Search in the table view (segments)"""
- segments = self.table_widget.segments()
- search_text_lower = self.search_text.lower()
-
- # Limit search results to avoid performance issues with very long segments
- max_results = 100
-
- for i, segment in enumerate(segments):
- if len(self.search_results) >= max_results:
- break
-
- text = segment.value("text").lower()
- if search_text_lower in text:
- self.search_results.append(("table", i, segment))
-
- # Also search in translations if available
- if self.has_translations:
- for i, segment in enumerate(segments):
- if len(self.search_results) >= max_results:
- break
-
- translation = segment.value("translation").lower()
- if search_text_lower in translation:
- self.search_results.append(("table", i, segment))
-
- def search_in_text(self):
- """Search in the text display box"""
- text = self.text_display_box.toPlainText()
- search_text_lower = self.search_text.lower()
- text_lower = text.lower()
-
- # Limit search results to avoid performance issues with very long text
- max_results = 100
-
- start = 0
- result_count = 0
- while True:
- pos = text_lower.find(search_text_lower, start)
- if pos == -1 or result_count >= max_results:
- break
- self.search_results.append(
- ("text", pos, pos + len(self.search_text)))
- start = pos + 1
- result_count += 1
-
- def update_search_ui(self):
- """Update the search UI elements"""
- if self.search_results:
- # Show "1 of X matches" format for consistency with navigation
- if len(self.search_results) >= 100:
- self.search_results_label.setText(_("1 of 100+ matches"))
- else:
- self.search_results_label.setText(
- _("1 of ") + str(len(self.search_results)) + _(" matches"))
- self.search_prev_button.setEnabled(True)
- self.search_next_button.setEnabled(True)
- self.highlight_current_match()
- else:
- self.search_results_label.setText(_("No matches found"))
- self.search_prev_button.setEnabled(False)
- self.search_next_button.setEnabled(False)
-
- def highlight_current_match(self):
- """Highlight the current search match"""
- if not self.search_results:
- return
-
- match_type, match_data, _ = self.search_results[self.current_search_index]
-
- if match_type == "table":
- # Highlight in table
- self.highlight_table_match(match_data)
- else: # text
- # Highlight in text display
- self.highlight_text_match(match_data)
-
- def highlight_table_match(self, row_index: int):
- """Highlight a match in the table view"""
- # Select the row containing the match
- self.table_widget.selectRow(row_index)
- # Scroll to the row
- self.table_widget.scrollTo(
- self.table_widget.model().index(row_index, 0))
-
- def highlight_text_match(self, start_pos: int):
- """Highlight a match in the text display"""
- cursor = QTextCursor(self.text_display_box.document())
- cursor.setPosition(start_pos)
- cursor.setPosition(start_pos + len(self.search_text),
- QTextCursor.MoveMode.KeepAnchor)
- # Set the cursor to highlight the text
- self.text_display_box.setTextCursor(cursor)
-
- # Ensure the highlighted text is visible
- self.text_display_box.ensureCursorVisible()
-
- def search_next(self):
- """Go to next search result"""
- if not self.search_results:
- return
-
- self.current_search_index = (
- self.current_search_index + 1) % len(self.search_results)
- self.highlight_current_match()
- self.update_search_results_label()
-
- def search_previous(self):
- """Go to previous search result"""
- if not self.search_results:
- return
-
- self.current_search_index = (
- self.current_search_index - 1) % len(self.search_results)
- self.highlight_current_match()
- self.update_search_results_label()
-
- def search_next_if_results(self):
- """Go to next search result only if there are results (for global shortcut)"""
- if self.search_results:
- self.search_next()
-
- def search_previous_if_results(self):
- """Go to previous search result only if there are results (for global shortcut)"""
- if self.search_results:
- self.search_previous()
-
- def update_search_results_label(self):
- """Update the search results label with current position"""
- if self.search_results:
- if len(self.search_results) >= 100:
- self.search_results_label.setText(
- str(self.current_search_index + 1) + _(" of 100+ matches"))
- else:
- self.search_results_label.setText(str(
- self.current_search_index + 1) + _(" of ") + str(len(self.search_results)) + _(" matches"))
-
- def clear_search(self):
- """Clear the search and reset highlighting"""
- self.search_text = ""
- self.search_results = []
- self.current_search_index = 0
- self.search_input.clear()
- self.search_results_label.setText("")
-
- self.search_prev_button.setEnabled(False)
- self.search_next_button.setEnabled(False)
-
- # Clear text highlighting
- if self.view_mode in (ViewMode.TEXT, ViewMode.TRANSLATION):
- cursor = QTextCursor(self.text_display_box.document())
- cursor.clearSelection()
- self.text_display_box.setTextCursor(cursor)
-
- # Keep search bar visible but clear the input
- self.search_input.setFocus()
-
- def hide_search_bar(self):
- """Hide the search bar completely"""
- self.search_frame.hide()
- self.find_button.setChecked(False) # Sync button state
- self.clear_search()
- self.search_input.clearFocus()
-
- # Save the visibility state to settings
- self.find_widget_visible = False
- self.settings.settings.setValue(
- "transcription_viewer/find_widget_visible", False)
-
- def setup_shortcuts(self):
- """Set up keyboard shortcuts"""
- from PyQt6.QtGui import QShortcut, QKeySequence
-
- # Search shortcut (Ctrl+F)
- search_shortcut = QShortcut(QKeySequence(
- self.shortcuts.get(Shortcut.SEARCH_TRANSCRIPT)), self)
- search_shortcut.activated.connect(self.focus_search_input)
-
- # Search navigation shortcuts (Ctrl+Enter / Shift+Enter)
- search_next_shortcut = QShortcut(QKeySequence(
- self.shortcuts.get(Shortcut.SEARCH_NEXT)), self)
- search_next_shortcut.activated.connect(self.search_next_if_results)
-
- search_prev_shortcut = QShortcut(QKeySequence(
- self.shortcuts.get(Shortcut.SEARCH_PREVIOUS)), self)
- search_prev_shortcut.activated.connect(self.search_previous_if_results)
-
- # Scroll to current text shortcut (Ctrl+G)
- scroll_to_current_shortcut = QShortcut(QKeySequence(
- self.shortcuts.get(Shortcut.SCROLL_TO_CURRENT_TEXT)), self)
- scroll_to_current_shortcut.activated.connect(
- self.on_scroll_to_current_button_clicked)
-
- # Play/Pause audio shortcut (Ctrl+P)
- play_pause_shortcut = QShortcut(QKeySequence(
- self.shortcuts.get(Shortcut.PLAY_PAUSE_AUDIO)), self)
- play_pause_shortcut.activated.connect(self.toggle_audio_playback)
-
- # Replay current segment shortcut (Ctrl+Shift+P)
- replay_segment_shortcut = QShortcut(QKeySequence(
- self.shortcuts.get(Shortcut.REPLAY_CURRENT_SEGMENT)), self)
- replay_segment_shortcut.activated.connect(self.replay_current_segment)
-
- # Playback controls visibility shortcut (Ctrl+Alt+P)
- playback_controls_shortcut = QShortcut(QKeySequence(
- self.shortcuts.get(Shortcut.TOGGLE_PLAYBACK_CONTROLS)), self)
- playback_controls_shortcut.activated.connect(
- self.toggle_playback_controls_visibility)
-
- # Segment timestamp adjustment shortcuts
- decrease_start_shortcut = QShortcut(QKeySequence(
- self.shortcuts.get(Shortcut.DECREASE_SEGMENT_START)), self)
- decrease_start_shortcut.activated.connect(self.decrease_segment_start)
-
- increase_start_shortcut = QShortcut(QKeySequence(
- self.shortcuts.get(Shortcut.INCREASE_SEGMENT_START)), self)
- increase_start_shortcut.activated.connect(self.increase_segment_start)
-
- decrease_end_shortcut = QShortcut(QKeySequence(
- self.shortcuts.get(Shortcut.DECREASE_SEGMENT_END)), self)
- decrease_end_shortcut.activated.connect(self.decrease_segment_end)
-
- increase_end_shortcut = QShortcut(QKeySequence(
- self.shortcuts.get(Shortcut.INCREASE_SEGMENT_END)), self)
- increase_end_shortcut.activated.connect(self.increase_segment_end)
-
- def focus_search_input(self):
- """Toggle the search bar visibility and focus the input field"""
- if self.search_frame.isVisible():
- self.hide_search_bar()
- else:
- self.search_frame.show()
- self.find_button.setChecked(True) # Sync button state
- self.search_input.setFocus()
- self.search_input.selectAll()
-
- # Save the visibility state to settings
- self.find_widget_visible = True
- self.settings.settings.setValue(
- "transcription_viewer/find_widget_visible", True)
-
- def toggle_search_bar_visibility(self):
- """Toggle the search bar visibility"""
- if self.search_frame.isVisible():
- self.hide_search_bar()
- else:
- self.show_search_bar()
-
- # Save the visibility state to settings
- self.find_widget_visible = self.search_frame.isVisible()
- self.settings.settings.setValue(
- "transcription_viewer/find_widget_visible", self.find_widget_visible)
-
- def show_search_bar(self):
- """Show the search bar and focus the input"""
- self.search_frame.show()
- self.find_button.setChecked(True)
- self.search_input.setFocus()
- self.search_input.selectAll()
-
- # Save the visibility state to settings
- self.find_widget_visible = True
- self.settings.settings.setValue(
- "transcription_viewer/find_widget_visible", True)
-
- def eventFilter(self, obj, event):
- """Event filter to handle keyboard shortcuts in search input"""
- from PyQt6.QtCore import QEvent, Qt
-
- if obj == self.search_input and event.type() == QEvent.Type.KeyPress:
- # The event is already a QKeyEvent, no need to create a new one
- if event.key() == Qt.Key.Key_Return and event.modifiers() == Qt.KeyboardModifier.ShiftModifier:
- self.search_previous()
- return True
- elif event.key() == Qt.Key.Key_Escape:
- self.hide_search_bar()
- return True
- return super().eventFilter(obj, event)
-
- def reset_view(self):
- if hasattr(self, 'media_splitter'):
- self.load_splitter_sizes()
-
- if self.view_mode == ViewMode.TIMESTAMPS:
- self.text_display_box.hide()
- self.table_widget.show()
- self.media_splitter.show()
- if self.current_media_player:
- self.current_media_player.show()
- # Show playback controls in timestamps mode
- if self.playback_controls_visible:
- self.loop_controls_frame.show()
- elif self.view_mode == ViewMode.TEXT:
- segments = self.transcription_service.get_transcription_segments(
- transcription_id=self.transcription.id_as_uuid
- )
-
- combined_text = ""
- previous_end_time = None
-
- paragraph_split_time = int(
- os.getenv("BUZZ_PARAGRAPH_SPLIT_TIME", "2000"))
-
- for segment in segments:
- if previous_end_time is not None and (segment.start_time - previous_end_time) >= paragraph_split_time:
- combined_text += "\n\n"
- combined_text += segment.text.strip() + " "
- previous_end_time = segment.end_time
-
- self.text_display_box.setPlainText(combined_text.strip())
- self.text_display_box.show()
- self.table_widget.hide()
- self.media_splitter.hide()
- if self.current_media_player:
- self.current_media_player.hide()
- # Hide playback controls in text mode
- self.loop_controls_frame.hide()
- # Hide current segment display in text mode
- self.current_segment_frame.hide()
- else: # ViewMode.TRANSLATION
- segments = self.transcription_service.get_transcription_segments(
- transcription_id=self.transcription.id_as_uuid
- )
- self.text_display_box.setPlainText(
- " ".join(segment.translation.strip() for segment in segments)
- )
- self.text_display_box.show()
- self.table_widget.hide()
- self.media_splitter.hide()
- if self.current_media_player:
- self.current_media_player.hide()
- # Hide playback controls in translation mode
- self.loop_controls_frame.hide()
- # Hide current segment display in translation mode
- self.current_segment_frame.hide()
-
- # Refresh search if there's active search text
- if self.search_text:
- self.perform_search()
-
- def on_view_mode_changed(self, view_mode: ViewMode) -> None:
- self.view_mode = view_mode
- self.reset_view()
-
- # Refresh search if there's active search text
- if self.search_text:
- self.perform_search()
-
- def on_segment_selected(self, segment: QSqlRecord):
- # Store the currently selected segment for loop functionality
- self.currently_selected_segment = segment
-
- # Show the current segment frame and update the text
- self.current_segment_frame.show()
- self.current_segment_text.setText(segment.value("text"))
-
- # Force the text label to recalculate its size
- self.current_segment_text.adjustSize()
-
- # Resize the frame to fit the text content
- self.resize_current_segment_frame()
-
- # Ensure the scroll area updates properly and shows scrollbars when needed
- self.current_segment_scroll_area.updateGeometry()
- self.current_segment_scroll_area.verticalScrollBar(
- ).setVisible(True) # Ensure scrollbar is visible
-
- start_time_ms = segment.value("start_time")
- end_time_ms = segment.value("end_time")
-
- if not self.current_media_player:
- return
-
- if self.current_media_player.position_ms < start_time_ms or self.current_media_player.position_ms > end_time_ms:
- self.current_media_player.set_position(start_time_ms)
-
- if self.segment_looping_enabled:
- self.current_media_player.set_range((start_time_ms, end_time_ms))
-
- # Reset looping flag to ensure new loops work
- self.current_media_player.is_looping = False
- else:
- segments = self.table_widget.segments()
- for i, seg in enumerate(segments):
- if seg.value("id") == segment.value("id"):
- self.table_widget.highlight_and_scroll_to_row(i)
- break
-
- def on_timestamp_being_edited(self, row: int, column: int, new_value_ms: int):
- """Handle real-time timestamp editing to update loop range immediately"""
- # Only update if looping is enabled and we're editing the currently selected segment
- if not self.segment_looping_enabled or self.currently_selected_segment is None:
- return
-
- # Check if we're editing the currently selected segment
- segments = self.table_widget.segments()
- if row >= len(segments):
- return
-
- edited_segment = segments[row]
- if edited_segment.value("id") != self.currently_selected_segment.value("id"):
- return
-
- # Import Column enum to check which column is being edited
- from buzz.widgets.transcription_viewer.transcription_segments_editor_widget import Column
-
- # Update the loop range based on which timestamp is being edited
- if column == Column.START.value:
- # Editing start time - update loop start
- end_time = edited_segment.value("end_time")
- self.audio_player.set_range((new_value_ms, end_time))
- elif column == Column.END.value:
- # Editing end time - update loop end
- start_time = edited_segment.value("start_time")
- self.audio_player.set_range((start_time, new_value_ms))
-
- def on_audio_player_position_ms_changed(self, position_ms: int) -> None:
- segments = self.table_widget.segments()
- current_segment = next(
- (
- segment
- for segment in segments
- if segment.value("start_time")
- <= position_ms
- < segment.value("end_time")
- ),
- None,
- )
- if current_segment is not None:
- self.current_segment_text.setText(current_segment.value("text"))
- self.current_segment_frame.show() # Show the frame when there's a current segment
-
- # Force the text label to recalculate its size
- self.current_segment_text.adjustSize()
-
- # Resize the frame to fit the text content
- self.resize_current_segment_frame()
-
- # Ensure the scroll area updates properly and shows scrollbars when needed
- self.current_segment_scroll_area.updateGeometry()
- self.current_segment_scroll_area.verticalScrollBar(
- ).setVisible(True) # Ensure scrollbar is visible
- # Update highlighting based on follow audio and loop settings
- if self.follow_audio_enabled:
- # Follow audio mode: highlight the current segment based on audio position
- if not self.segment_looping_enabled or self.currently_selected_segment is None:
- # Normal mode: highlight the current segment
- for i, segment in enumerate(segments):
- if segment.value("id") == current_segment.value("id"):
- self.table_widget.highlight_and_scroll_to_row(i)
- break
- else:
- # Loop mode: only highlight if we're in a different segment than the selected one
- if current_segment.value("id") != self.currently_selected_segment.value("id"):
- for i, segment in enumerate(segments):
- if segment.value("id") == current_segment.value("id"):
- self.table_widget.highlight_and_scroll_to_row(
- i)
- break
- else:
- # Don't follow audio: keep highlighting on the selected segment
- if self.currently_selected_segment is not None:
- # Find and highlight the selected segment
- for i, segment in enumerate(segments):
- if segment.value("id") == self.currently_selected_segment.value("id"):
- self.table_widget.highlight_and_scroll_to_row(i)
- break
- # Don't do any highlighting if no segment is selected and follow is disabled
-
- def resize_current_segment_frame(self):
- """
- Resize the current segment frame to fit its content, using the actual rendered size
- of the text label (including line wrapping). This ensures the frame is tall enough
- for the visible text, up to a reasonable maximum.
- """
- text = self.current_segment_text.text()
- if not text:
- self.current_segment_frame.setMaximumHeight(0)
- self.current_segment_frame.setMinimumHeight(0)
- return
-
- # Calculate the height needed for the text area
- line_height = self.current_segment_text.fontMetrics().lineSpacing()
- max_visible_lines = 3 # Fixed at 3 lines for consistency and clean UI
-
- # Calculate the height needed for the maximum visible lines (25% larger)
- text_height = line_height * max_visible_lines * 1.25
-
- # Add some vertical margins/padding
- margins = 8 # Increased from 2 to 8 for better spacing
-
- # Calculate total height needed (no header height anymore)
- total_height = text_height + margins
-
- # Convert to integer since Qt methods expect int values
- total_height = int(total_height)
-
- # Set maximum height to ensure consistent sizing, but allow minimum to be flexible
- self.current_segment_frame.setMaximumHeight(total_height)
- self.current_segment_frame.setMinimumHeight(total_height)
-
- # Convert text_height to integer since Qt methods expect int values
- text_height = int(text_height)
-
- # Allow the scroll area to be flexible in height for proper scrolling
- self.current_segment_scroll_area.setMinimumHeight(text_height)
- self.current_segment_scroll_area.setMaximumHeight(text_height)
-
- # Allow the text label to size naturally for proper scrolling
- self.current_segment_text.setMinimumHeight(text_height)
-
- def load_preferences(self):
- self.settings.settings.beginGroup("file_transcriber")
- preferences = FileTranscriptionPreferences.load(
- settings=self.settings.settings)
- self.settings.settings.endGroup()
- return preferences
-
- def open_advanced_settings(self):
- self.transcription_options_dialog.show()
-
- def on_transcription_options_changed(
- self, transcription_options: TranscriptionOptions
- ):
- self.transcription_options = transcription_options
-
- def on_translate_button_clicked(self):
- if len(self.openai_access_token) == 0:
- QMessageBox.information(
- self,
- _("API Key Required"),
- _("Please enter OpenAI API Key in preferences")
- )
-
- return
-
- if self.transcription_options.llm_model == "" or self.transcription_options.llm_prompt == "":
- self.transcription_options_dialog.accepted.connect(
- self.run_translation)
- self.transcription_options_dialog.show()
- return
-
- self.run_translation()
-
- def run_translation(self):
- if self.transcription_options.llm_model == "" or self.transcription_options.llm_prompt == "":
- return
-
- segments = self.table_widget.segments()
- for segment in segments:
- self.translator.enqueue(segment.value("text"), segment.value("id"))
-
- def on_resize_button_clicked(self):
- self.transcription_resizer_dialog = TranscriptionResizerWidget(
- transcription=self.transcription,
- transcription_service=self.transcription_service,
- transcriptions_updated_signal=self.transcriptions_updated_signal,
- )
-
- self.transcriptions_updated_signal.connect(self.close)
-
- self.transcription_resizer_dialog.show()
-
- def on_speaker_identification_button_clicked(self):
- # Underlying libs do not support intel Macs
- if not (platform.system() == "Darwin" and platform.machine() == "x86_64"):
- self.speaker_identification_dialog = SpeakerIdentificationWidget(
- transcription=self.transcription,
- transcription_service=self.transcription_service,
- transcriptions_updated_signal=self.transcriptions_updated_signal,
- )
-
- self.transcriptions_updated_signal.connect(self.close)
-
- self.speaker_identification_dialog.show()
-
- pass
-
- def on_loop_toggle_changed(self, enabled: bool):
- """Handle loop toggle state change"""
- self.segment_looping_enabled = enabled
- # Save preference to settings
- self.settings.settings.setValue(
- "transcription_viewer/segment_looping_enabled", enabled)
- if enabled:
- # If looping is re-enabled,and we have a selected segment, return to it
- if self.currently_selected_segment is not None:
- # Find the row index of the selected segment
- segments = self.table_widget.segments()
- for i, segment in enumerate(segments):
- if segment.value("id") == self.currently_selected_segment.value("id"):
- # Highlight and scroll to the selected segment
- self.table_widget.highlight_and_scroll_to_row(i)
-
- start_time_ms = self.currently_selected_segment.value(
- "start_time")
- end_time_ms = self.currently_selected_segment.value(
- "end_time")
- # Set the loop range for the selected segment
- if self.current_media_player:
- self.current_media_player.set_range(
- (start_time_ms, end_time_ms))
-
- # If audio is currently playing and outside the range, jump to the start
- current_pos = self.current_media_player.position_ms
- playback_state = self.current_media_player.media_player.playbackState()
- if (playback_state == QMediaPlayer.PlaybackState.PlayingState and
- (current_pos < start_time_ms or current_pos > end_time_ms)):
- self.current_media_player.set_position(
- start_time_ms)
-
- break
- else:
- # Clear any existing range if looping is disabled
- if self.current_media_player:
- self.current_media_player.clear_range()
-
- def on_follow_audio_toggle_changed(self, enabled: bool):
- """Handle follow audio toggle state change"""
- self.follow_audio_enabled = enabled
- # Save preference to settings
- self.settings.settings.setValue(
- "transcription_viewer/follow_audio_enabled", enabled)
- if enabled:
- # When follow audio is first enabled, automatically scroll to current position
- # This gives immediate feedback that the feature is working
- self.auto_scroll_to_current_position()
- else:
- # If we have a selected segment, highlight it and keep it highlighted
- if self.currently_selected_segment is not None:
- segments = self.table_widget.segments()
- for i, segment in enumerate(segments):
- if segment.value("id") == self.currently_selected_segment.value("id"):
- self.table_widget.highlight_and_scroll_to_row(i)
- break
-
- def on_scroll_to_current_button_clicked(self):
- """Handle scroll to current button click"""
- if not self.current_media_player:
- return
- current_pos = self.current_media_player.position_ms
- segments = self.table_widget.segments()
-
- # Find the current segment based on audio position
- current_segment_index = 0
- current_segment = segments[0]
- for i, segment in enumerate(segments):
- if segment.value("start_time") <= current_pos < segment.value("end_time"):
- current_segment_index = i
- current_segment = segment
- break
-
- # Workaround for scrolling to already selected segment
- if self.currently_selected_segment and self.currently_selected_segment.value("id") == current_segment.value('id'):
- self.highlight_table_match(0)
-
- if self.currently_selected_segment is None:
- self.highlight_table_match(0)
-
- if current_segment_index == 0 and len(segments) > 1:
- self.highlight_table_match(1)
-
- self.highlight_table_match(current_segment_index)
- self.current_media_player.set_position(current_pos)
-
- def auto_scroll_to_current_position(self):
- """
- Automatically scroll to the current audio position.
- This is used when follow audio is first enabled to give immediate feedback.
- """
- try:
- # Only scroll if we're in timestamps view mode (table is visible)
- if self.view_mode != ViewMode.TIMESTAMPS:
- return
-
- if not self.current_media_player:
- return
- current_pos = self.current_media_player.position_ms
- segments = self.table_widget.segments()
-
- # Find the current segment based on audio position
- current_segment = next(
- (segment for segment in segments
- if segment.value("start_time") <= current_pos < segment.value("end_time")),
- None
- )
-
- if current_segment is not None:
- # Find the row index and scroll to it
- for i, segment in enumerate(segments):
- if segment.value("id") == current_segment.value("id"):
- # Use all available scrolling methods to ensure visibility
- # Method 1: Use the table widget's built-in scrolling method
- self.table_widget.highlight_and_scroll_to_row(i)
- break
-
- except Exception as e:
- pass # Silently handle any errors
-
- def resizeEvent(self, event):
- """Save geometry when widget is resized"""
- self.save_geometry()
- self.save_splitter_sizes()
- super().resizeEvent(event)
-
- def closeEvent(self, event):
- """Save geometry when widget is closed"""
- self.save_geometry()
-
- # save splitter sizes before closing
- self.save_splitter_sizes()
-
- self.hide()
-
- # Stop media playback when closing
- if self.current_media_player:
- self.current_media_player.stop()
-
- if self.transcription_resizer_dialog:
- self.transcription_resizer_dialog.close()
-
- if self.speaker_identification_dialog:
- self.speaker_identification_dialog.close()
-
- self.translator.stop()
- self.translation_thread.quit()
-
- # Only wait if thread is actually running
- if self.translation_thread.isRunning():
- # Wait up to 35 seconds for graceful shutdown
- # (30s max API call timeout + 5s buffer)
- if not self.translation_thread.wait(35_000):
- logging.warning(
- "Translation thread did not finish gracefully, terminating")
- # Force terminate the thread if it doesn't stop
- self.translation_thread.terminate()
- # Give it a brief moment to terminate
- if not self.translation_thread.wait(1_000):
- logging.error("Translation thread could not be terminated")
-
- super().closeEvent(event)
-
- def save_geometry(self):
- """Save the widget geometry to settings"""
- self.settings.begin_group(Settings.Key.TRANSCRIPTION_VIEWER)
- self.settings.settings.setValue("geometry", self.saveGeometry())
- self.settings.end_group()
-
- def load_geometry(self):
- """Load the widget geometry from settings"""
- self.settings.begin_group(Settings.Key.TRANSCRIPTION_VIEWER)
- geometry = self.settings.settings.value("geometry")
- if geometry is not None:
- self.restoreGeometry(geometry)
- else:
- # Default size if no saved geometry
- self.resize(1000, 800)
- self.settings.end_group()
-
- def save_splitter_sizes(self):
- """Save splitter sizes to settings"""
- if not hasattr(self, 'media_splitter'):
- return
-
- sizes = self.media_splitter.sizes()
- self.settings.begin_group(Settings.Key.TRANSCRIPTION_VIEWER)
-
- # Save separately for video and audio
- if self.current_media_player == self.video_player:
- self.settings.settings.setValue("video_splitter_sizes", sizes)
- else:
- self.settings.settings.setValue("audio_splitter_sizes", sizes)
-
- self.settings.end_group()
-
- def load_splitter_sizes(self):
- """Load splitter sizes from settings"""
- if not hasattr(self, 'media_splitter'):
- return
-
- self.settings.begin_group(Settings.Key.TRANSCRIPTION_VIEWER)
-
- # Load sizes based on media type
- if self.current_media_player == self.video_player:
- sizes = self.settings.settings.value("video_splitter_sizes")
- if sizes is None:
- sizes = [800, 200]
- else:
- sizes = self.settings.settings.value("audio_splitter_sizes")
- if sizes is None:
- sizes = [950, 50]
-
- self.settings.end_group()
-
- # Apply sizes
- if sizes:
- self.media_splitter.setSizes([int(s) for s in sizes])
-
- def on_splitter_moved(self, pos: int, index: int):
- """Called when user moves the splitter"""
- # Save sizes after a short delay to avoid saving on every pixel move
- QTimer.singleShot(100, self.save_splitter_sizes)
diff --git a/buzz/widgets/update_dialog.py b/buzz/widgets/update_dialog.py
deleted file mode 100644
index 43487284..00000000
--- a/buzz/widgets/update_dialog.py
+++ /dev/null
@@ -1,262 +0,0 @@
-import logging
-import os
-import platform
-import subprocess
-import tempfile
-from typing import Optional
-
-from PyQt6.QtCore import Qt, QUrl
-from PyQt6.QtWidgets import QApplication
-from PyQt6.QtGui import QIcon
-from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
-from PyQt6.QtWidgets import (
- QDialog,
- QVBoxLayout,
- QHBoxLayout,
- QLabel,
- QPushButton,
- QProgressBar,
- QMessageBox,
- QWidget,
- QTextEdit,
-)
-
-from buzz.__version__ import VERSION
-from buzz.locale import _
-from buzz.update_checker import UpdateInfo
-from buzz.widgets.icon import BUZZ_ICON_PATH
-
-class UpdateDialog(QDialog):
- """Dialog shows when an update is available"""
- def __init__(
- self,
- update_info: UpdateInfo,
- network_manager: Optional[QNetworkAccessManager] = None,
- parent: Optional[QWidget] = None
- ):
- super().__init__(parent)
-
- self.update_info = update_info
-
- if network_manager is None:
- network_manager = QNetworkAccessManager(self)
- self.network_manager = network_manager
-
- self._download_reply: Optional[QNetworkReply] = None
- self._temp_file_paths: list = []
- self._pending_urls: list = []
- self._temp_dir: Optional[str] = None
-
- self._setup_ui()
-
- def _setup_ui(self):
- self.setWindowTitle(_("Update Available"))
- self.setWindowIcon(QIcon(BUZZ_ICON_PATH))
- self.setMinimumWidth(450)
-
- layout = QVBoxLayout(self)
- layout.setSpacing(16)
-
- #header
- header_label = QLabel(
- _("A new version of Buzz is available!")
- )
-
- header_label.setStyleSheet("font-size: 16px; font-weight: bold;")
- layout.addWidget(header_label)
-
- #Version info
- version_layout = QHBoxLayout()
-
- current_version_label = QLabel(_("Current version:"))
- current_version_value = QLabel(f"{VERSION} ")
-
- new_version_label = QLabel(_("New version:"))
- new_version_value = QLabel(f"{self.update_info.version} ")
-
- version_layout.addWidget(current_version_label)
- version_layout.addWidget(current_version_value)
- version_layout.addStretch()
- version_layout.addWidget(new_version_label)
- version_layout.addWidget(new_version_value)
-
- layout.addLayout(version_layout)
-
- #Release notes
- if self.update_info.release_notes:
- notes_label = QLabel(_("Release Notes:"))
- notes_label.setStyleSheet("font-weight: bold;")
- layout.addWidget(notes_label)
-
- notes_text = QTextEdit()
- notes_text.setReadOnly(True)
- notes_text.setMarkdown(self.update_info.release_notes)
- notes_text.setMaximumHeight(150)
- layout.addWidget(notes_text)
-
- #progress bar
- self.progress_bar = QProgressBar()
- self.progress_bar.setVisible(False)
- self.progress_bar.setTextVisible(True)
- layout.addWidget(self.progress_bar)
-
- #Status label
- self.status_label = QLabel("")
- self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
- layout.addWidget(self.status_label)
-
- #Buttons
- button_layout = QVBoxLayout()
-
- self.download_button = QPushButton(_("Download and Install"))
- self.download_button.clicked.connect(self._on_download_clicked)
- self.download_button.setDefault(True)
-
- button_layout.addStretch()
- button_layout.addWidget(self.download_button)
-
- layout.addLayout(button_layout)
-
- def _on_download_clicked(self):
- """Starts downloading the installer"""
- if not self.update_info.download_urls:
- QMessageBox.warning(
- self,
- _("Error"),
- _("No download URL available for your platform.")
- )
- return
-
- self.download_button.setEnabled(False)
- self.progress_bar.setVisible(True)
- self.progress_bar.setValue(0)
- self._temp_file_paths = []
- self._pending_urls = list(self.update_info.download_urls)
- self._temp_dir = tempfile.mkdtemp()
- self._download_next_file()
-
- def _download_next_file(self):
- """Download the next file in the queue"""
- if not self._pending_urls:
- self._all_downloads_finished()
- return
-
- url_str = self._pending_urls[0]
- file_index = len(self.update_info.download_urls) - len(self._pending_urls) + 1
- total_files = len(self.update_info.download_urls)
- self.status_label.setText(
- _("Downloading file {} of {}...").format(file_index, total_files)
- )
-
- url = QUrl(url_str)
- request = QNetworkRequest(url)
-
- self._download_reply = self.network_manager.get(request)
- self._download_reply.downloadProgress.connect(self._on_download_progress)
- self._download_reply.finished.connect(self._on_download_finished)
-
- def _on_download_progress(self, bytes_received: int, bytes_total: int):
- """Update the progress bar during download"""
- if bytes_total > 0:
- progress = int((bytes_received / bytes_total) * 100)
- self.progress_bar.setValue(progress)
-
- mb_received = bytes_received / (1024 * 1024)
- mb_total = bytes_total / (1024 * 1024)
- file_index = len(self.update_info.download_urls) - len(self._pending_urls) + 1
- total_files = len(self.update_info.download_urls)
- self.status_label.setText(
- _("Downloading file {} of {} ({:.1f} MB / {:.1f} MB)...").format(
- file_index, total_files, mb_received, mb_total
- )
- )
-
- def _on_download_finished(self):
- """Handles download completion for one file"""
- if self._download_reply is None:
- return
-
- if self._download_reply.error() != QNetworkReply.NetworkError.NoError:
- error_msg = self._download_reply.errorString()
- logging.error(f"Download failed: {error_msg}")
-
- QMessageBox.critical(
- self,
- _("Download Failed"),
- _("Failed to download the update: {}").format(error_msg)
- )
-
- self._reset_ui()
- self._download_reply.deleteLater()
- self._download_reply = None
- return
-
- data = self._download_reply.readAll().data()
- self._download_reply.deleteLater()
- self._download_reply = None
-
- url_str = self._pending_urls.pop(0)
-
- # Extract original filename from URL to preserve it
- original_filename = QUrl(url_str).fileName()
- if not original_filename:
- original_filename = f"download_{len(self._temp_file_paths)}"
-
- try:
- temp_path = os.path.join(self._temp_dir, original_filename)
- with open(temp_path, "wb") as f:
- f.write(data)
- self._temp_file_paths.append(temp_path)
- logging.info(f"File saved to: {temp_path}")
- except Exception as e:
- logging.error(f"Failed to save file: {e}")
- QMessageBox.critical(
- self,
- _("Error"),
- _("Failed to save the installer: {}").format(str(e))
- )
- self._reset_ui()
- return
-
- self._download_next_file()
-
- def _all_downloads_finished(self):
- """All files downloaded, run the installer"""
- self.progress_bar.setValue(100)
- self.status_label.setText(_("Download complete!"))
- self._run_installer()
-
- def _run_installer(self):
- """Run the downloaded installer"""
- if not self._temp_file_paths:
- return
-
- installer_path = self._temp_file_paths[0]
- system = platform.system()
-
- try:
- if system == "Windows":
- subprocess.Popen([installer_path], shell=True)
-
- elif system == "Darwin":
- #open the DMG file
- subprocess.Popen(["open", installer_path])
-
- # Close the app so the installer can replace files
- self.accept()
- QApplication.quit()
-
- except Exception as e:
- logging.error(f"Failed to run installer: {e}")
- QMessageBox.critical(
- self,
- _("Error"),
- _("Failed to run the installer: {}").format(str(e))
- )
-
- def _reset_ui(self):
- """Reset the UI to initial state after an error"""
- self.download_button.setEnabled(True)
- self.progress_bar.setVisible(False)
- self.status_label.setText("")
-
diff --git a/buzz/widgets/video_player.py b/buzz/widgets/video_player.py
deleted file mode 100644
index 3c6288d4..00000000
--- a/buzz/widgets/video_player.py
+++ /dev/null
@@ -1,172 +0,0 @@
-import logging
-from typing import Tuple, Optional
-from PyQt6.QtCore import Qt, QUrl, pyqtSignal, QTime
-from PyQt6.QtMultimedia import QMediaPlayer, QAudioOutput, QMediaDevices
-from PyQt6.QtMultimediaWidgets import QVideoWidget
-from PyQt6.QtWidgets import QWidget, QVBoxLayout, QSlider, QPushButton, QHBoxLayout, QLabel, QSizePolicy
-from buzz.widgets.icon import PlayIcon, PauseIcon
-
-class VideoPlayer(QWidget):
- position_ms_changed = pyqtSignal(int)
-
- def __init__(self, file_path: str, parent=None):
- super().__init__(parent)
-
- self.range_ms: Optional[Tuple[int, int]] = None
- self.position_ms = 0
- self.duration_ms = 0
- self.is_looping = False
- self.is_slider_dragging = False
- self.initial_frame_loaded = False
-
- self.audio_output = QAudioOutput(self)
- self.audio_output.setVolume(100)
-
- # Log audio device info for debugging
- default_device = QMediaDevices.defaultAudioOutput()
- if default_device.isNull():
- logging.warning("No default audio output device found!")
- else:
- logging.info(f"Audio output device: {default_device.description()}")
-
- self.media_player = QMediaPlayer(self)
- self.media_player.setSource(QUrl.fromLocalFile(file_path))
- self.media_player.setAudioOutput(self.audio_output)
-
- self.video_widget = QVideoWidget(self)
- self.media_player.setVideoOutput(self.video_widget)
-
- # Size constraints for video widget
- self.video_widget.setMinimumHeight(200)
- self.video_widget.setMaximumHeight(400)
- self.video_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
-
- self.scrubber = QSlider(Qt.Orientation.Horizontal)
- self.scrubber.setRange(0, 0)
- self.scrubber.sliderMoved.connect(self.on_slider_moved)
- self.scrubber.sliderPressed.connect(self.on_slider_pressed)
- self.scrubber.sliderReleased.connect(self.on_slider_released)
-
- #Track if user is dragging the slider
- self.is_slider_dragging = False
-
- self.play_icon = PlayIcon(self)
- self.pause_icon = PauseIcon(self)
-
- self.play_button = QPushButton("")
- self.play_button.setIcon(self.play_icon)
- self.play_button.clicked.connect(self.toggle_playback)
- self.play_button.setMaximumWidth(40)
- self.play_button.setMinimumHeight(30)
-
- self.time_label = QLabel()
- self.time_label.setAlignment(Qt.AlignmentFlag.AlignRight)
-
- controls = QHBoxLayout()
- controls.addWidget(self.play_button)
- controls.addWidget(self.scrubber)
- controls.addWidget(self.time_label)
-
- layout = QVBoxLayout(self)
- layout.setContentsMargins(0, 0, 0, 0)
- layout.setSpacing(4)
- layout.addWidget(self.video_widget, stretch=1)
- layout.addLayout(controls)
-
- self.setLayout(layout)
-
-
- self.media_player.positionChanged.connect(self.on_position_changed)
- self.media_player.durationChanged.connect(self.on_duration_changed)
- self.media_player.playbackStateChanged.connect(self.on_playback_state_changed)
- self.media_player.mediaStatusChanged.connect(self.on_media_status_changed)
- self.media_player.errorOccurred.connect(self.on_error_occurred)
-
- def on_error_occurred(self, error: QMediaPlayer.Error, error_string: str):
- logging.error(f"Media player error: {error} - {error_string}")
-
- def on_media_status_changed(self, status: QMediaPlayer.MediaStatus):
- # Only do this once on initial load to show first frame
- if self.initial_frame_loaded:
- return
- # Start playback when loaded to trigger frame decoding
- if status == QMediaPlayer.MediaStatus.LoadedMedia:
- self.media_player.play()
- # Pause immediately when buffered to show first frame
- elif status == QMediaPlayer.MediaStatus.BufferedMedia:
- self.initial_frame_loaded = True
- self.media_player.pause()
-
- def toggle_playback(self):
- if self.media_player.playbackState() == QMediaPlayer.PlaybackState.PlayingState:
- self.media_player.pause()
- else:
- self.media_player.play()
-
- def on_slider_moved(self, position):
- self.set_position(position)
-
- def on_slider_pressed(self):
- """Called when user starts dragging the slider"""
- self.is_slider_dragging = True
-
- def on_slider_released(self):
- """Called when user releases the slider"""
- self.is_slider_dragging = False
- # Update position to where use released
- self.set_position(self.scrubber.value())
-
- def set_position(self, position_ms: int):
- self.media_player.setPosition(position_ms)
-
- def on_position_changed(self, position_ms: int):
- # Don't update slider if user is currently dragging it
- if not self.is_slider_dragging:
- self.scrubber.blockSignals(True)
- self.scrubber.setValue(position_ms)
- self.scrubber.blockSignals(False)
-
- self.position_ms = position_ms
- self.position_ms_changed.emit(position_ms)
- self.update_time_label()
-
- # If a range has been selected and video has reached the end of range
- #loop back to the start of the range
- if self.range_ms is not None and not self.is_looping:
- start_range_ms, end_range_ms = self.range_ms
- #Check if video is at or past the end of range
- if position_ms >= (end_range_ms - 50):
- self.is_looping = True
- self.set_position(start_range_ms)
- self.is_looping = False
-
- def on_duration_changed(self, duration_ms: int):
- self.scrubber.setRange(0, duration_ms)
- self.duration_ms = duration_ms
- self.update_time_label()
-
- def on_playback_state_changed(self, state: QMediaPlayer.PlaybackState):
- if state == QMediaPlayer.PlaybackState.PlayingState:
- self.play_button.setIcon(self.pause_icon)
- else:
- self.play_button.setIcon(self.play_icon)
-
- def update_time_label(self):
- position_time = QTime(0, 0).addMSecs(self.position_ms).toString()
- duration_time = QTime(0, 0).addMSecs(self.duration_ms).toString()
- self.time_label.setText(f"{position_time} / {duration_time}")
-
- def set_range(self, range_ms: Tuple[int, int]):
- """Set a loop range. Only jump to start if current position is outside the range."""
- self.range_ms = range_ms
- start_range_ms, end_range_ms = range_ms
-
- if self.position_ms < start_range_ms or self.position_ms > end_range_ms:
- self.set_position(start_range_ms)
-
- def clear_range(self):
- """Clear the current loop range"""
- self.range_ms = None
-
- def stop(self):
- self.media_player.stop()
diff --git a/ctc_forced_aligner b/ctc_forced_aligner
deleted file mode 160000
index 1f0a5f86..00000000
--- a/ctc_forced_aligner
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 1f0a5f860d3d9daf3d94edb1c7d18f90d1702e5b
diff --git a/deepmultilingualpunctuation b/deepmultilingualpunctuation
deleted file mode 160000
index 5a0dd7f4..00000000
--- a/deepmultilingualpunctuation
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 5a0dd7f4fd56687f59405aa8eba1144393d8b74b
diff --git a/demucs_repo b/demucs_repo
deleted file mode 160000
index 4273070a..00000000
--- a/demucs_repo
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 4273070a70ded308ddfd0879d267bbd06f89a1b7
diff --git a/dll_backup/SDL2.dll b/dll_backup/SDL2.dll
deleted file mode 100644
index e26bcb1c..00000000
Binary files a/dll_backup/SDL2.dll and /dev/null differ
diff --git a/docs/.gitignore b/docs/.gitignore
deleted file mode 100644
index b2d6de30..00000000
--- a/docs/.gitignore
+++ /dev/null
@@ -1,20 +0,0 @@
-# Dependencies
-/node_modules
-
-# Production
-/build
-
-# Generated files
-.docusaurus
-.cache-loader
-
-# Misc
-.DS_Store
-.env.local
-.env.development.local
-.env.test.local
-.env.production.local
-
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
diff --git a/docs/README.md b/docs/README.md
deleted file mode 100644
index aaba2fa1..00000000
--- a/docs/README.md
+++ /dev/null
@@ -1,41 +0,0 @@
-# Website
-
-This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator.
-
-### Installation
-
-```
-$ yarn
-```
-
-### Local Development
-
-```
-$ yarn start
-```
-
-This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
-
-### Build
-
-```
-$ yarn build
-```
-
-This command generates static content into the `build` directory and can be served using any static contents hosting service.
-
-### Deployment
-
-Using SSH:
-
-```
-$ USE_SSH=true yarn deploy
-```
-
-Not using SSH:
-
-```
-$ GIT_USER= yarn deploy
-```
-
-If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
diff --git a/docs/babel.config.js b/docs/babel.config.js
deleted file mode 100644
index e00595da..00000000
--- a/docs/babel.config.js
+++ /dev/null
@@ -1,3 +0,0 @@
-module.exports = {
- presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
-};
diff --git a/docs/docs/cli.md b/docs/docs/cli.md
deleted file mode 100644
index a8df135a..00000000
--- a/docs/docs/cli.md
+++ /dev/null
@@ -1,88 +0,0 @@
----
-title: CLI
-sidebar_position: 5
----
-
-## Commands
-
-### `add`
-
-Start a new transcription task.
-
-```
-Usage: buzz add [options] [file url file...]
-
-Options:
- -t, --task The task to perform. Allowed: translate,
- transcribe. Default: transcribe.
- -m, --model-type Model type. Allowed: whisper, whispercpp,
- huggingface, fasterwhisper, openaiapi. Default:
- whisper.
- -s, --model-size Model size. Use only when --model-type is
- whisper, whispercpp, or fasterwhisper. Allowed:
- tiny, base, small, medium, large. Default:
- tiny.
- --hfid Hugging Face model ID. Use only when
- --model-type is huggingface. Example:
- "openai/whisper-tiny"
- -l, --language Language code. Allowed: af (Afrikaans), am
- (Amharic), ar (Arabic), as (Assamese), az
- (Azerbaijani), ba (Bashkir), be (Belarusian),
- bg (Bulgarian), bn (Bengali), bo (Tibetan), br
- (Breton), bs (Bosnian), ca (Catalan), cs
- (Czech), cy (Welsh), da (Danish), de (German),
- el (Greek), en (English), es (Spanish), et
- (Estonian), eu (Basque), fa (Persian), fi
- (Finnish), fo (Faroese), fr (French), gl
- (Galician), gu (Gujarati), ha (Hausa), haw
- (Hawaiian), he (Hebrew), hi (Hindi), hr
- (Croatian), ht (Haitian Creole), hu
- (Hungarian), hy (Armenian), id (Indonesian), is
- (Icelandic), it (Italian), ja (Japanese), jw
- (Javanese), ka (Georgian), kk (Kazakh), km
- (Khmer), kn (Kannada), ko (Korean), la (Latin),
- lb (Luxembourgish), ln (Lingala), lo (Lao), lt
- (Lithuanian), lv (Latvian), mg (Malagasy), mi
- (Maori), mk (Macedonian), ml (Malayalam), mn
- (Mongolian), mr (Marathi), ms (Malay), mt
- (Maltese), my (Myanmar), ne (Nepali), nl
- (Dutch), nn (Nynorsk), no (Norwegian), oc
- (Occitan), pa (Punjabi), pl (Polish), ps
- (Pashto), pt (Portuguese), ro (Romanian), ru
- (Russian), sa (Sanskrit), sd (Sindhi), si
- (Sinhala), sk (Slovak), sl (Slovenian), sn
- (Shona), so (Somali), sq (Albanian), sr
- (Serbian), su (Sundanese), sv (Swedish), sw
- (Swahili), ta (Tamil), te (Telugu), tg (Tajik),
- th (Thai), tk (Turkmen), tl (Tagalog), tr
- (Turkish), tt (Tatar), uk (Ukrainian), ur
- (Urdu), uz (Uzbek), vi (Vietnamese), yi
- (Yiddish), yo (Yoruba), zh (Chinese). Leave
- empty to detect language.
- -p, --prompt Initial prompt.
- -w, --word-timestamps Generate word-level timestamps. (available since 1.2.0)
- -e, --extract-speech Extract speech from audio before transcribing. (available since 1.3.0)
- --openai-token OpenAI access token. Use only when
- --model-type is openaiapi. Defaults to your
- previously saved access token, if one exists.
- --srt Output result in an SRT file.
- --vtt Output result in a VTT file.
- --txt Output result in a TXT file.
- --hide-gui Hide the main application window. (available since 1.2.0)
- -h, --help Displays help on commandline options.
- --help-all Displays help including Qt specific options.
- -v, --version Displays version information.
-
-Arguments:
- files or urls Input file paths or urls. Url import availalbe since 1.2.0.
-```
-
-**Examples**:
-
-```shell
-# Translate two MP3 files from French to English using OpenAI Whisper API
-buzz add --task translate --language fr --model-type openaiapi /Users/user/Downloads/1b3b03e4-8db5-ea2c-ace5-b71ff32e3304.mp3 /Users/user/Downloads/koaf9083k1lkpsfdi0.mp3
-
-# Transcribe an MP4 using Whisper.cpp "small" model and immediately export to SRT and VTT files
-buzz add --task transcribe --model-type whispercpp --model-size small --prompt "My initial prompt" --srt --vtt /Users/user/Downloads/buzz/1b3b03e4-8db5-ea2c-ace5-b71ff32e3304.mp4
-```
diff --git a/docs/docs/faq.md b/docs/docs/faq.md
deleted file mode 100644
index 88723b7f..00000000
--- a/docs/docs/faq.md
+++ /dev/null
@@ -1,96 +0,0 @@
----
-title: FAQ
-sidebar_position: 5
----
-
-### 1. Where are the models stored?
-
-The models are stored:
-
-- Linux: `~/.cache/Buzz`
-- Mac OS: `~/Library/Caches/Buzz`
-- Windows: `%USERPROFILE%\AppData\Local\Buzz\Buzz\Cache`
-
-Paste the location in your file manager to access the models or go to `Help -> Preferences -> Models` and click on `Show file location` button after downloading some model.
-
-### 2. What can I try if the transcription runs too slowly?
-
-Speech recognition requires large amount of computation, so one option is to try using a lower Whisper model size or using a Whisper.cpp model to run speech recognition of your computer. If you have access to a computer with GPU that has at least 6GB of VRAM you can try using the Faster Whisper model.
-
-Buzz also supports using OpenAI API to do speech recognition on a remote server. To use this feature you need to set OpenAI API key in Preferences. See [Preferences](https://chidiwilliams.github.io/buzz/docs/preferences) section for more details.
-
-### 3. How to record system audio?
-
-To transcribe system audio you need to configure virtual audio device and connect output from the applications you want to transcribe to this virtual speaker. After that you can select it as source in the Buzz. See [Usage](https://chidiwilliams.github.io/buzz/docs/usage/live_recording) section for more details.
-
-Relevant tools:
-
-- Mac OS - [BlackHole](https://github.com/ExistentialAudio/BlackHole).
-- Windows - [VB CABLE](https://vb-audio.com/Cable/)
-- Linux - [PulseAudio Volume Control](https://wiki.ubuntu.com/record_system_sound)
-
-### 4. What model should I use?
-
-Model size to use will depend on your hardware and use case. Smaller models will work faster but will have more inaccuracies. Larger models will be more accurate but will require more powerful hardware or longer time to transcribe.
-
-When choosing among large models consider the following. "Large" is the first released older model, "Large-V2" is later updated model with better accuracy, for some languages considered the most robust and stable. "Large-V3" is the latest model with the best accuracy in many cases, but some times can hallucinate or invent words that were never in the audio. "Turbo" model tries to get a good balance between speed and accuracy. The only sure way to know what model best suits your needs is to test them all in your language.
-
-In addition to choosing an appropriate model size you also can choose whisper type.
-- **Whisper** is initial OpenAI implementation, it is accurate but slow and requires a lot of RAM.
-- **Faster Whisper** is an optimized implementation, it is orders of magnitude faster than regular Whisper and requires less RAM. Use this option if you have an Nvidia GPU with at least 6GB of VRAM.
-- **Whisper.cpp** is optimized C++ implementation, it quite fast and efficient and will use any brand of GPU. Whisper.cpp is capable of running real time transcription even on a modern laptop with integrated GPU. It can also run on CPU only. Use this option if you do not have Nvidia GPU.
-- **HuggingFace** option is a `Transformers` implementation and is good in that it supports wide range of custom models that may be optimized for a particular language. This option also supports [MMS](https://ai.meta.com/blog/multilingual-model-speech-recognition/) family of models from Meta AI that support over 1000 of worlds languages as well as [PEFT](https://github.com/huggingface/peft) adjustments to Whisper models.
-
-### 5. How to get GPU acceleration for faster transcription?
-
-On Linux GPU acceleration is supported out of the box on Nvidia GPUs. If you still get any issues install [CUDA 12](https://developer.nvidia.com/cuda-downloads), [cuBLASS](https://developer.nvidia.com/cublas) and [cuDNN](https://developer.nvidia.com/cudnn).
-
-On Windows GPU support is included in the installation `.exe`. CUDA 12 required, computers with older CUDA versions will use CPU. See [this note](https://github.com/chidiwilliams/buzz/blob/main/CONTRIBUTING.md#gpu-support) on enabling CUDA GPU support.
-
-### 6. How to fix `Unanticipated host error[PaErrorCode-9999]`?
-
-Check if there are any system settings preventing apps from accessing the microphone.
-
-On Windows, see if Buzz has permission to use the microphone in Settings -> Privacy -> Microphone.
-
-See method 1 in this video https://www.youtube.com/watch?v=eRcCYgOuSYQ
-
-For method 2 there is no need to uninstall the antivirus, but see if you can temporarily disable it or if there are settings that may prevent Buzz from accessing the microphone.
-
-### 7. Can I use Buzz on a computer without internet?
-
-Yes, Buzz can be used without internet connection if you download the necessary models on some other computer that has the internet and manually move them to the offline computer. The easiest way to find where the models are stored is to go to Help -> Preferences -> Models. Then download some model, and push "Show file location" button. This will open the folder where the models are stored. Copy the models folder to the same location on the offline computer. F.e. for Linux it is `.cache/Buzz/models` in your home directory.
-
-### 8. Buzz crashes, what to do?
-
-If a model download was incomplete or corrupted, Buzz may crash. Try to delete the downloaded model files in `Help -> Preferences -> Models` and re-download them.
-
-If that does not help, check the log file for errors and [report the issue](https://github.com/chidiwilliams/buzz/issues) so we can fix it. If possible attach the log file to the issue. Since Version `1.3.4`, to get to the logs folder go to `Help -> About Buzz` and click on `Show logs` button.
-
-### 9. Where can I get latest development version?
-
-Latest development version will have latest bug fixes and most recent features. If you feel a bit adventurous it is recommended to try the latest development version as they needs some testing before they get released to everybody.
-
-- **Linux** users can get the latest version with this command `sudo snap install buzz --edge`
-
-- **For other** platforms do the following:
- 1. Go to the [build section](https://github.com/chidiwilliams/buzz/actions/workflows/ci.yml?query=branch%3Amain)
- 2. Click on the link to the latest build, the most recent successful build entry in the list
- 3. Scroll down to the artifacts section in the build page
- 4. Download the installation file. Please note that you need to be logged in the Github to see the download links.
- 
-
-### 10. Why is my system theme not applied to Buzz installed from Flatpak?
-
-For dark themes on Gnome environments you may need to install `gnome-themes-extra` package and set the following preferences:
-```
-gsettings set org.gnome.desktop.interface gtk-theme Adwaita-dark
-gsettings set org.gnome.desktop.interface color-scheme prefer-dark
-```
-
-If your system theme is not applied to Buzz installed from Flatpak Linux app store, ensure the desired theme is in `~/.themes` folder.
-
-You may need to copy the system themes to this folder `cp -r /usr/share/themes/ ~/.themes/` and give Flatpaks access to this folder `flatpak override --user --filesystem=~/.themes`.
-
-On Fedora run the following to install the necessary packages
-`sudo dnf install gnome-themes-extra qadwaitadecorations-qt{5,6} qt{5,6}-qtwayland`
\ No newline at end of file
diff --git a/docs/docs/index.md b/docs/docs/index.md
deleted file mode 100644
index 27600e0a..00000000
--- a/docs/docs/index.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
-title: Introduction
-sidebar_position: 1
----
-
-Transcribe and translate audio offline on your personal computer. Powered by
-OpenAI's [Whisper](https://github.com/openai/whisper).
-
-
-[](https://github.com/chidiwilliams/buzz/actions/workflows/ci.yml)
-[](https://codecov.io/github/chidiwilliams/buzz)
-
-[](https://GitHub.com/chidiwilliams/buzz/releases/)
-
-## Features
-
-- Import audio and video files and export transcripts to TXT, SRT, and
- VTT ([Demo](https://www.loom.com/share/cf263b099ac3481082bb56d19b7c87fe))
-- Transcription and translation from your computer's microphones to text (Resource-intensive and may not be
- real-time, [Demo](https://www.loom.com/share/564b753eb4d44b55b985b8abd26b55f7))
- - Presentation window for easy accessibility during events and presentations
- - [Realtime translation](https://chidiwilliams.github.io/buzz/docs/usage/translations) with OpenAI API compatible AI
-- [Advanced Transcription Viewer](https://chidiwilliams.github.io/buzz/docs/usage/transcription_viewer) with search, playback controls, and speed adjustment
-- **Smart Interface** with conditional visibility and state persistence
-- **Professional Controls** including loop segments, follow audio, and keyboard shortcuts
-- Supports [Whisper](https://github.com/openai/whisper#available-models-and-languages),
- [Whisper.cpp](https://github.com/ggerganov/whisper.cpp) (with Vulkan GPU acceleration), [Faster Whisper](https://github.com/guillaumekln/faster-whisper),
- [Whisper-compatible Hugging Face models](https://huggingface.co/models?other=whisper), and
- the [OpenAI Whisper API](https://platform.openai.com/docs/api-reference/introduction)
-- [Command-Line Interface](#command-line-interface)
-- Speech separation before transcription for better accuracy on noisy audio
-- [Speaker identification](https://chidiwilliams.github.io/buzz/docs/usage/speaker_identification) in transcribed media
-- Available on Mac, Windows, and Linux
\ No newline at end of file
diff --git a/docs/docs/installation.md b/docs/docs/installation.md
deleted file mode 100644
index 259bf329..00000000
--- a/docs/docs/installation.md
+++ /dev/null
@@ -1,50 +0,0 @@
----
-title: Installation
-sidebar_position: 2
----
-
-To install Buzz, download the latest version for your operating
-system. Buzz is available on **Mac** (Intel and Apple silicon), **Windows**, and **Linux**.
-
-### macOS
-
-Download the `.dmg` from the [SourceForge](https://sourceforge.net/projects/buzz-captions/files/).
-
-### Windows
-
-Get the installation files from the [SourceForge](https://sourceforge.net/projects/buzz-captions/files/).
-
-App is not signed, you will get a warning when you install it. Select `More info` -> `Run anyway`.
-
-## Linux
-
-Buzz is available as a [Flatpak](https://flathub.org/apps/io.github.chidiwilliams.Buzz) or a [Snap](https://snapcraft.io/buzz).
-
-To install flatpak, run:
-```shell
-flatpak install flathub io.github.chidiwilliams.Buzz
-```
-
-[](https://flathub.org/en/apps/io.github.chidiwilliams.Buzz)
-
-To install snap, run:
-```shell
-sudo apt-get install libportaudio2 libcanberra-gtk-module libcanberra-gtk3-module
-sudo snap install buzz
-sudo snap connect buzz:password-manager-service
-```
-
-[](https://snapcraft.io/buzz)
-
-## PyPI
-
-```shell
-pip install buzz-captions
-python -m buzz
-```
-
-On Linux install system dependencies you may be missing
-```
-sudo apt-get install --no-install-recommends libyaml-dev libtbb-dev libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-shape0 libxcb-cursor0 libportaudio2 gettext libpulse0 ffmpeg
-```
-On versions prior to Ubuntu 24.04 install `sudo apt-get install --no-install-recommends libegl1-mesa`
diff --git a/docs/docs/preferences.md b/docs/docs/preferences.md
deleted file mode 100644
index b6c8bd5e..00000000
--- a/docs/docs/preferences.md
+++ /dev/null
@@ -1,124 +0,0 @@
----
-title: Preferences
-sidebar_position: 4
----
-
-Open the Preferences window from the Menu bar, or click `Ctrl/Cmd + ,`.
-
-## General Preferences
-
-### OpenAI API preferences
-
-**API Key** - key to authenticate your requests to OpenAI API. To get API key from OpenAI see [this article](https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key).
-
-**Base URL** - By default all requests are sent to API provided by OpenAI company. Their API URL is `https://api.openai.com/v1/`. Compatible APIs are also provided by other companies. List of available API URLs and services to run yourself are available on [discussion page](https://github.com/chidiwilliams/buzz/discussions/827)
-
-### Default export file name
-
-Sets the default export file name for file transcriptions. For
-example, a value of `{{ input_file_name }} ({{ task }}d on {{ date_time }})` will save TXT exports
-as `Input Filename (transcribed on 19-Sep-2023 20-39-25).txt` by default.
-
-Available variables:
-
-| Key | Description | Example |
-| ----------------- | ----------------------------------------- | ---------------------------------------------------------------- |
-| `input_file_name` | File name of the imported file | `audio` (e.g. if the imported file path was `/path/to/audio.wav` |
-| `task` | Transcription task | `transcribe`, `translate` |
-| `language` | Language code | `en`, `fr`, `yo`, etc. |
-| `model_type` | Model type | `Whisper`, `Whisper.cpp`, `Faster Whisper`, etc. |
-| `model_size` | Model size | `tiny`, `base`, `small`, `medium`, `large`, etc. |
-| `date_time` | Export time (format: `%d-%b-%Y %H-%M-%S`) | `19-Sep-2023 20-39-25` |
-
-### Live transcript exports
-
-Live transcription export can be used to integrate Buzz with other applications like OBS Studio.
-When enabled, live text transcripts will be exported to a text file as they get generated and translated.
-
-If AI translation is enabled for live recordings, the translated text will also be exported to the text file.
-Filename for the translated text will end with `.translated.txt`.
-
-### Live transcription mode
-
-Three transcription modes are available:
-
-**Append below** - New sentences will be added below existing with an empty space between them.
-Last sentence will be at the bottom.
-
-**Append above** - New sentences will be added above existing with an empty space between them.
-Last sentence will be at the top.
-
-**Append and correct** - New sentences will be added at the end of existing transcript without extra spaces between.
-This mode will also try to correct errors at the end of previously transcribed sentences. This mode requires more
-processing power and more powerful hardware to work.
-
-## Model Preferences
-
-This section lets you download new models for transcription and delete unused ones.
-
-For Whisper.cpp you can also download custom models. Select `Custom` in the model size list and paste the download url
-to the model `.bin` file. Use the link from "download" button from the Huggingface.
-
-To improve transcription speed and memory usage you can select a quantized version of some
-larger model. For example `q_5` version. Whisper.cpp base models in different quantizations are [available here](https://huggingface.co/ggerganov/whisper.cpp/tree/main). See also [custom models](https://github.com/chidiwilliams/buzz/discussions/866) discussion page for custom models in different languages.
-
-[](https://www.loom.com/share/cf263b099ac3481082bb56d19b7c87fe "Model preferences")
-
-## Advanced Preferences
-
-To keep preferences section simple for new users, some more advanced preferences are settable via OS environment variables. Set the necessary environment variables in your OS before starting Buzz or create a script to set them.
-
-On MacOS and Linux crete `run_buzz.sh` with the following content:
-
-```bash
-#!/bin/bash
-export VARIABLE=value
-export SOME_OTHER_VARIABLE=some_other_value
-buzz
-```
-
-On Windows crete `run_buzz.bat` with the following content:
-
-```bat
-@echo off
-set VARIABLE=value
-set SOME_OTHER_VARIABLE=some_other_value
-"C:\Program Files (x86)\Buzz\Buzz.exe"
-```
-
-Alternatively you can set environment variables in your OS settings. See [this guide](https://phoenixnap.com/kb/windows-set-environment-variable#ftoc-heading-4) or [this video](https://www.youtube.com/watch?v=bEroNNzqlF4) more information.
-
-### Available variables
-
-**BUZZ_WHISPERCPP_N_THREADS** - Number of threads to use for Whisper.cpp model. Default is half of available CPU cores.
-
-On a laptop with 16 threads setting `BUZZ_WHISPERCPP_N_THREADS=8` leads to some 15% speedup in transcription time.
-Increasing number of threads even more will lead in slower transcription time as results from parallel threads has to be
-combined to produce the final answer.
-
-**BUZZ_TRANSLATION_API_BASE_URL** - Base URL of OpenAI compatible API to use for translation.
-
-**BUZZ_TRANSLATION_API_KEY** - Api key of OpenAI compatible API to use for translation.
-
-**BUZZ_MODEL_ROOT** - Root directory to store model files. You may also want to set `HF_HOME` to the same folder as some libraries used in Buzz download their models independently.
-Defaults to [user_cache_dir](https://pypi.org/project/platformdirs/).
-
-**BUZZ_FAVORITE_LANGUAGES** - Coma separated list of supported language codes to show on top of language list.
-
-**BUZZ_DOWNLOAD_COOKIEFILE** - Location of a [cookiefile](https://github.com/yt-dlp/yt-dlp/wiki/FAQ#how-do-i-pass-cookies-to-yt-dlp) to use for downloading private videos or as workaround for anti-bot protection.
-
-**BUZZ_FORCE_CPU** - Will force Buzz to use CPU and not GPU, useful for setups with older GPU if that is slower than GPU or GPU has issues. Example usage `BUZZ_FORCE_CPU=true`. Available since `1.2.1`
-
-**BUZZ_REDUCE_GPU_MEMORY** - Will use 8bit quantization for Huggingface adn Faster Whisper transcriptions to reduce required GPU memory. Example usage `BUZZ_REDUCE_GPU_MEMORY=true`. Available since `1.4.0`
-
-**BUZZ_MERGE_REGROUP_RULE** - Custom regroup merge rule to use when combining transcripts with word-level timings. More information on available options [in stable-ts repo](https://github.com/jianfch/stable-ts?tab=readme-ov-file#regrouping-methods). Available since `1.3.0`
-
-**BUZZ_DISABLE_TELEMETRY** - Buzz collects basic OS name and architecture usage statistics to better focus development efforts. This variable lets disable collection of these statistics. Example usage `BUZZ_DISABLE_TELEMETRY=true`. Available since `1.3.0`
-
-**BUZZ_UPLOAD_URL** - Live recording transcripts and translations can be uploaded to a server for display on the web. Set this variable to the desired upload url. You can use [buzz-transcription-server](https://github.com/raivisdejus/buzz-transcription-server) as a server. Buzz will upload the following `json` via `POST` requests - `{"kind": "transcript", "text": "Sample transcript"}` or `{"kind": "translation", "text": "Sample translation"}`. Example usage `BUZZ_UPLOAD_URL=http://localhost:5000/upload`. Available since `1.3.0`
-
-Example of data collected by telemetry:
-```
-Buzz: 1.3.0, locale: ('lv_LV', 'UTF-8'), system: Linux, release: 6.14.0-27-generic, machine: x86_64, version: #27~24.04.1-Ubuntu SMP PREEMPT_DYNAMIC Tue Jul 22 17:38:49 UTC 2,
-```
-**BUZZ_PARAGRAPH_SPLIT_TIME** - Time in milliseconds of silence to split paragraphs in transcript and add two newlines when exporting the transcripts as text. Default is `2000` or 2 seconds. Available since `1.3.0`
\ No newline at end of file
diff --git a/docs/docs/usage/1_file_import.md b/docs/docs/usage/1_file_import.md
deleted file mode 100644
index 8b1086a8..00000000
--- a/docs/docs/usage/1_file_import.md
+++ /dev/null
@@ -1,29 +0,0 @@
----
-title: File Import
----
-
-**To import a file:**
-
-- Click Import Media File on the File menu (or the '+' icon on the toolbar, or **Command/Ctrl + O**).
-- Choose an audio or video file.
-- Select a task, language, and the model settings.
-- Click Run.
-- When the transcription status shows 'Completed', double-click on the row (or select the row and click the '⤢' icon) to
- open the transcription.
-
-**Available options:**
-
-To reduce misspellings you can pass some commonly misspelled words in an `Initial prompt` that is available under `Advanced...` button. See this [guide on prompting](https://cookbook.openai.com/examples/whisper_prompting_guide#pass-names-in-the-prompt-to-prevent-misspellings).
-
-
-| Field | Options | Default | Description |
-| ------------------ | ------------------- | ------- |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| Export As | "TXT", "SRT", "VTT" | "TXT" | Export file format |
-| Word-Level Timings | Off / On | Off | If checked, the transcription will generate a separate subtitle line for each word in the audio. Combine words into subtitles afterwards with the [resize option](https://chidiwilliams.github.io/buzz/docs/usage/edit_and_resize). |
-| Extract speech | Off / On | Off | If checked, speech will be extracted to a separate audio tack to improve accuracy. |
-
-(See the [Live Recording section](https://chidiwilliams.github.io/buzz/docs/usage/live_recording) for more information about the task, language, and quality settings.)
-
-[](https://www.loom.com/share/cf263b099ac3481082bb56d19b7c87fe "Media File Import on Buzz")
-
-**💡 Tip:** It is recommended to always select language to transcribe to as automatic language detection may result in unexpected results.
diff --git a/docs/docs/usage/2_live_recording.md b/docs/docs/usage/2_live_recording.md
deleted file mode 100644
index 750c5874..00000000
--- a/docs/docs/usage/2_live_recording.md
+++ /dev/null
@@ -1,81 +0,0 @@
----
-title: Live Recording
----
-
-To start a live recording:
-
-- Select a recording task, language, quality, and microphone.
-- Click Record.
-
-> **Note:** Transcribing audio using the default Whisper model is resource-intensive. Consider using the Whisper.cpp.
-> It supports GPU acceleration, if the model fits in GPU memory. Use smaller models for real-time performance.
-
-| Field | Options | Default | Description |
-|------------|------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| Task | "Transcribe", "Translate to English" | "Transcribe" | "Transcribe" converts the input audio into text in the selected language, while "Translate to English" converts it into text in English. |
-| Language | See [Whisper's documentation](https://github.com/openai/whisper#available-models-and-languages) for the full list of supported languages | "Detect Language" | "Detect Language" will try to detect the spoken language in the audio based on the first few seconds. However, selecting a language is recommended (if known) as it will improve transcription quality in many cases. |
-| Microphone | [Available system microphones] | [Default system microphone] | Microphone for recording input audio. |
-
-[](https://www.loom.com/share/564b753eb4d44b55b985b8abd26b55f7 "Live Recording on Buzz")
-
-#### Advanced preferences
-**Silence threshold** Set threshold to for transcriptions to be processed. If average volume level is under this setting the sentence will not be transcribed. Available since 1.4.4.
-
-**Line separator** Marking to add to the transcription and translation lines. Default value is two new lines (`\n\n`) that result in an empty space between translation or transcription lines. To have no empty line use `\n`. Available since 1.4.4.
-
-**Transcription step** If live recording mode is set to `Append and correct`, you can also set a transcription step. Shorter steps will reduce latency but cause larger load on the system. Monitor the `Queue` while transcribing in this mode, if it grows too much, increase the transcription step, to reduce load. Available since 1.4.4.
-
-**Hide unconfirmed** If live recording mode is set to `Append and correct`, you can also hide the unconfirmed part of the last transcript. This part may be incorrect as the Buzz has seen it only in one overlapping transcription segment. Hiding it will increase latency, but result will show only the correct transcripts. Available since 1.4.4.
-
-#### Presentation Window
-
-Buzz has an easy to use presentation window you can use to show live transcriptions during events and presentations. To open it start the recording and new options for the `Presentation window` will appear.
-
-### Record audio playing from computer (macOS)
-
-To record audio playing from an application on your computer, you may install an audio loopback driver (a program that
-lets you create virtual audio devices). The rest of this guide will
-use [BlackHole](https://github.com/ExistentialAudio/BlackHole) on Mac, but you can use other alternatives for your
-operating system (
-see [LoopBeAudio](https://nerds.de/en/loopbeaudio.html), [LoopBack](https://rogueamoeba.com/loopback/),
-and [Virtual Audio Cable](https://vac.muzychenko.net/en/)).
-
-1. Install [BlackHole via Homebrew](https://github.com/ExistentialAudio/BlackHole#option-2-install-via-homebrew)
-
- ```shell
- brew install blackhole-2ch
- ```
-
-2. Open Audio MIDI Setup from Spotlight or from `/Applications/Utilities/Audio Midi Setup.app`.
-
- 
-
-3. Click the '+' icon at the lower left corner and select 'Create Multi-Output Device'.
-
- 
-
-4. Add your default speaker and BlackHole to the multi-output device.
-
- 
-
-5. Select this multi-output device as your speaker (application or system-wide) to play audio into BlackHole.
-
-6. Open Buzz, select BlackHole as your microphone, and record as before to see transcriptions from the audio playing
- through BlackHole.
-
-### Record audio playing from computer (Windows)
-
-To transcribe system audio you need to configure virtual audio device and connect output from the applications you whant to transcribe to this virtual speaker. After that you can select it as source in the Buzz.
-
-1. Install [VB CABLE](https://vb-audio.com/Cable/) as virtual audio device.
-
-2. Configure using Windows Sound settings. Right-click on the speaker icon in the system tray and select "Open Sound settings". In the "Choose your output device" dropdown select "CABLE Input" to send all system sound to the virtual device or use "Advanced sound options" to select application that will output their sound to this device.
-
-### Record audio playing from computer (Linux)
-
-As described on [Ubuntu Wiki](https://wiki.ubuntu.com/record_system_sound) on any Linux with pulse audio you can redirect application audio to a virtual speaker. After that you can select it as source in Buzz.
-
-Overall steps:
-1. Launch application that will produce the sound you want to transcribe and start the playback. For example start a video in a media player.
-2. Launch Buzz and open Live recording screen, so you see the settings.
-3. Configure sound routing from the application you want to transcribe sound from to Buzz in `Recording tab` of the PulseAudio Volume Control (`pavucontrol`).
\ No newline at end of file
diff --git a/docs/docs/usage/3_translations.md b/docs/docs/usage/3_translations.md
deleted file mode 100644
index 44ed71f5..00000000
--- a/docs/docs/usage/3_translations.md
+++ /dev/null
@@ -1,17 +0,0 @@
----
-title: Translations
----
-
-Default `Translation` task uses Whisper model ability to translate to English, however `Large-V3-Turbo` is not compatible with this standard. Buzz supports additional AI translations to any other language.
-
-To use translation feature you will need to configure OpenAI API key and translation settings. Set OpenAI API ket in Preferences. Buzz also supports custom locally running translation AIs that support OpenAI API. For more information on locally running AIs see [ollama](https://ollama.com/blog/openai-compatibility) or [LM Studio](https://lmstudio.ai/). For information on available custom APIs see this [discussion thread](https://github.com/chidiwilliams/buzz/discussions/827).
-
-To configure translation for Live recordings enable it in Advances settings dialog of the Live Recording settings. Enter AI model to use and prompt with instructions for the AI on how to translate. Translation option is also available for files that already have speech recognised. Use Translate button on transcription viewer toolbar.
-
-For AI to know how to translate enter translation instructions in the "Instructions for AI" section. In your instructions you should describe to what language you want it to translate the text to. Also, you may need to add additional instructions to not add any notes or comments as AIs tend to add them. Example instructions to translate English subtitles to Spanish:
-
-> You are a professional translator, skilled in translating English to Spanish. You will only translate each sentence sent to you into Spanish and not add any notes or comments.
-
-If you enable "Enable live recording transcription export" in Preferences, Live text transcripts will be exported to a text file as they get generated and translated. This file can be used to further integrate Live transcripts with other applications like OBS Studio.
-
-Approximate cost of translation for 1 hour long audio with ChatGPT `gpt-4o` model is around $0.50.
diff --git a/docs/docs/usage/4_edit_and_resize.md b/docs/docs/usage/4_edit_and_resize.md
deleted file mode 100644
index 4231d8db..00000000
--- a/docs/docs/usage/4_edit_and_resize.md
+++ /dev/null
@@ -1,13 +0,0 @@
----
-title: Edit and Resize
----
-
-[](https://www.loom.com/share/cf263b099ac3481082bb56d19b7c87fe "Resize options")
-
-When transcript of some audio or video file is generated you can edit it and export to different subtitle formats or plain text. Double-click the transcript in the list of transcripts to see additional options for editing and exporting.
-
-Transcription view screen has option to resize the transcripts. Click on the "Resize" button so see available options. Transcripts that have been generated **with word-level timings** setting enabled can be combined into subtitles specifying different options, like maximum length of a subtitle and if subtitles should be split on punctuation. For transcripts that have been generated **without word-level timings** setting enabled can only be recombined specifying desired max length of a subtitle.
-
-If audio file is still present on the system word-level timing merge will also analyze the audio for silences to improve subtitle accuracy.
-
-The resize tool also has an option to extend end time of segments if you want the subtitles to be on the screen for longer. You can specify the amount of time in seconds to extend each subtitle segment. Buzz will add this amount of time to the end of each subtitle segment making sure that the end of a segment does not go over start of the next segment. This feature is available since 1.4.3.
\ No newline at end of file
diff --git a/docs/docs/usage/5_speaker_identification.md b/docs/docs/usage/5_speaker_identification.md
deleted file mode 100644
index a8c8c097..00000000
--- a/docs/docs/usage/5_speaker_identification.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-title: Speaker identification
----
-
-When transcript of some audio or video file is generated you can identify speakers in the transcript. Double-click the transcript in the list of transcripts to see additional options for editing and exporting.
-
-Transcription view screen has option to identify speakers. Click on the "Identify speakers" button so see available options.
-
-If audio file is still present on the system speaker identification will mark each speakers sentences with appropriate label. You can preview 10 seconds of some random sentence of the identified speaker and rename the automatically identified label to speakers real name. If "Merge speaker sentences" checkbox is selected when you save the speaker labels, all consecutive sentences of the same speaker will be merged into one segment. Speaker identification is not available on Intel macOS.
\ No newline at end of file
diff --git a/docs/docs/usage/5_transcription_viewer.md b/docs/docs/usage/5_transcription_viewer.md
deleted file mode 100644
index c075b9b3..00000000
--- a/docs/docs/usage/5_transcription_viewer.md
+++ /dev/null
@@ -1,123 +0,0 @@
-# Transcription Viewer
-
-The Buzz transcription viewer provides a powerful interface for reviewing, editing, and navigating through your transcriptions. This guide covers all the features available in the transcription viewer.
-
-## Overview
-
-The transcription viewer is organized into several key sections:
-
-- **Top Toolbar**: Contains view mode, export, translate, resize, and search
-- **Search Bar**: Find and navigate through transcript text
-- **Transcription Segments**: Table view of all transcription segments with timestamps
-- **Playback Controls**: Audio playback settings and speed controls
-- **Audio Player**: Standard media player with progress bar
-- **Current Segment Display**: Shows the currently selected or playing segment
-
-## Top Toolbar
-
-### View Mode Button
-- **Function**: Switch between different viewing modes
-- **Options**:
- - **Timestamps**: Shows segments in a table format with start/end times
- - **Text**: Shows combined text without timestamps
- - **Translation**: Shows translated text (if available)
-
-### Export Button
-- **Function**: Export transcription in various formats
-- **Formats**: SRT, VTT, TXT, JSON, and more
-- **Usage**: Click to open export menu and select desired format
-
-### Translate Button
-- **Function**: Translate transcription to different languages
-- **Usage**: Click to open translation settings and start translation
-
-### Resize Button
-- **Function**: Adjust transcription segment boundaries
-- **Usage**: Click to open resize dialog for fine-tuning timestamps
-- **More information**: See [Edit and Resize](https://chidiwilliams.github.io/buzz/docs/usage/edit_and_resize) section
-
-### Playback Controls Button
-- **Function**: Show/hide playback control panel
-- **Shortcut**: `Ctrl+Alt+P` (Windows/Linux) or `Cmd+Alt+P` (macOS)
-- **Behavior**: Toggle button that shows/hides the playback controls below
-
-### Find Button
-- **Function**: Show/hide search functionality
-- **Shortcut**: `Ctrl+F` (Windows/Linux) or `Cmd+F` (macOS)
-- **Behavior**: Toggle button that shows/hides the search bar
-
-### Scroll to Current Button
-- **Function**: Automatically scroll to the currently playing text
-- **Shortcut**: `Ctrl+G` (Windows/Linux) or `Cmd+G` (macOS)
-- **Usage**: Click to jump to the current audio position in the transcript
-
-## Search Functionality
-
-### Search Bar
-The search bar appears below the toolbar when activated and provides:
-
-- **Search Input**: Type text to find in the transcription (wider input field for better usability)
-- **Navigation**: Up/down arrows to move between matches
-- **Status**: Shows current match position and total matches (e.g., "3 of 15 matches")
-- **Clear**: Remove search text and results (larger button for better accessibility)
-- **Results**: Displays found text with context
-- **Consistent Button Sizing**: All navigation buttons have uniform height for better visual consistency
-
-### Search Shortcuts
-- **`Ctrl+F` / `Cmd+F`**: Toggle search bar on/off
-- **`Enter`**: Find next match
-- **`Shift+Enter`**: Find previous match
-- **`Escape`**: Close search bar
-
-### Search Features
-- **Real-time Search**: Results update as you type
-- **Case-insensitive**: Finds matches regardless of capitalization
-- **Word Boundaries**: Respects word boundaries for accurate matching
-- **Cross-view Search**: Works in all view modes (Timestamps, Text, Translation)
-
-## Playback Controls
-
-### Loop Segment
-- **Function**: Automatically loop playback of selected segments
-- **Usage**: Check the "Loop Segment" checkbox
-- **Behavior**: When enabled, clicking on a transcript segment will set a loop range
-- **Visual Feedback**: Loop range is highlighted in the audio player
-
-### Follow Audio
-- **Function**: Automatically scroll to current audio position
-- **Usage**: Check the "Follow Audio" checkbox
-- **Behavior**: Transcript automatically follows the audio playback
-- **Benefits**: Easy to follow along with long audio files
-
-### Speed Controls
-- **Function**: Adjust audio playback speed
-- **Range**: 0.5x to 2.0x speed
-- **Controls**:
- - **Speed Dropdown**: Select from preset speeds or enter custom value
- - **Decrease Button (-)**: Reduce speed by 0.05x increments
- - **Increase Button (+)**: Increase speed by 0.05x increments
-- **Persistence**: Speed setting is saved between sessions
-- **Button Sizing**: Speed control buttons match the size of search navigation buttons for visual consistency
-
-## Keyboard Shortcuts
-
-### Audio Playback
-- **`Ctrl+P` / `Cmd+P`**: Play/Pause audio
-- **`Ctrl+Shift+P` / `Cmd+Shift+P`**: Replay current segment from start
-
-### Timestamp Adjustment
-- **`Ctrl+←` / `Cmd+←`**: Decrease segment start time by 0.5s
-- **`Ctrl+→` / `Cmd+→`**: Increase segment start time by 0.5s
-- **`Ctrl+Shift+←` / `Cmd+Shift+←`**: Decrease segment end time by 0.5s
-- **`Ctrl+Shift+→` / `Cmd+Shift+→`**: Increase segment end time by 0.5s
-
-### Navigation
-- **`Ctrl+F` / `Cmd+F`**: Toggle search bar
-- **`Ctrl+Alt+P` / `Cmd+Alt+P`**: Toggle playback controls
-- **`Ctrl+G` / `Cmd+G`**: Scroll to current position
-- **`Ctrl+O` / `Cmd+O`**: Open file import dialog
-
-### Search
-- **`Enter`**: Find next match
-- **`Shift+Enter`**: Find previous match
-- **`Escape`**: Close search bar
diff --git a/docs/docs/usage/_category_.yml b/docs/docs/usage/_category_.yml
deleted file mode 100644
index 388ecf9d..00000000
--- a/docs/docs/usage/_category_.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-label: Usage
-position: 3
diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js
deleted file mode 100644
index e155d619..00000000
--- a/docs/docusaurus.config.js
+++ /dev/null
@@ -1,106 +0,0 @@
-// @ts-check
-// Note: type annotations allow type checking and IDEs autocompletion
-
-const lightCodeTheme = require("prism-react-renderer/themes/github");
-const darkCodeTheme = require("prism-react-renderer/themes/dracula");
-
-/** @type {import('@docusaurus/types').Config} */
-const config = {
- title: "Buzz",
- tagline: "Audio transcription and translation",
- favicon: "img/favicon.ico",
-
- // Set the production url of your site here
- url: "https://chidiwilliams.github.io",
- // Set the // pathname under which your site is served
- // For GitHub pages deployment, it is often '//'
- baseUrl: "/buzz/",
-
- // GitHub pages deployment config.
- // If you aren't using GitHub pages, you don't need these.
- organizationName: "chidiwilliams", // Usually your GitHub org/user name.
- projectName: "buzz", // Usually your repo name.
-
- onBrokenLinks: "throw",
- onBrokenMarkdownLinks: "warn",
-
- trailingSlash: false,
-
- // Even if you don't use internalization, you can use this field to set useful
- // metadata like html lang. For example, if your site is Chinese, you may want
- // to replace "en" with "zh-Hans".
- i18n: {
- defaultLocale: "en",
- locales: ["en", "zh"],
- path: "i18n",
- localeConfigs: {
- en: {
- label: "English",
- direction: "ltr",
- htmlLang: "en-US",
- path: "en",
- },
- zh: {
- label: "简体中文",
- direction: "ltr",
- htmlLang: "zh-CN",
- path: "zh",
- },
- },
- },
-
- presets: [
- [
- "classic",
- /** @type {import('@docusaurus/preset-classic').Options} */
- ({
- docs: {
- sidebarPath: require.resolve("./sidebars.js"),
- },
- blog: {
- showReadingTime: true,
- },
- theme: {
- customCss: require.resolve("./src/css/custom.css"),
- },
- }),
- ],
- ],
-
- themeConfig:
- /** @type {import('@docusaurus/preset-classic').ThemeConfig} */
- ({
- // Replace with your project's social card
- image: "img/favicon.ico",
- navbar: {
- title: "Buzz",
- logo: {
- alt: "Buzz",
- src: "img/favicon.ico",
- },
- items: [
- {
- type: "docSidebar",
- sidebarId: "tutorialSidebar",
- position: "left",
- label: "Docs",
- },
- {
- href: "https://github.com/chidiwilliams/buzz",
- label: "GitHub",
- position: "right",
- },
- {
- type: "localeDropdown",
- position: "left",
- },
- ],
- },
- prism: {
- theme: lightCodeTheme,
- darkTheme: darkCodeTheme,
- },
- }),
-};
-
-module.exports = config;
diff --git a/docs/i18n/zh/docusaurus-plugin-content-docs/current/cli.md b/docs/i18n/zh/docusaurus-plugin-content-docs/current/cli.md
deleted file mode 100644
index eca461b2..00000000
--- a/docs/i18n/zh/docusaurus-plugin-content-docs/current/cli.md
+++ /dev/null
@@ -1,87 +0,0 @@
----
-title: 命令行界面 (CLI)
-sidebar_position: 5
----
-
-## 命令
-
-### `增加`
-
-启动一个新的转录任务。
-
-```
-Usage: buzz add [options] [file url file...]
-
-Options:
- -t, --task The task to perform. Allowed: translate,
- transcribe. Default: transcribe.
- -m, --model-type Model type. Allowed: whisper, whispercpp,
- huggingface, fasterwhisper, openaiapi. Default:
- whisper.
- -s, --model-size Model size. Use only when --model-type is
- whisper, whispercpp, or fasterwhisper. Allowed:
- tiny, base, small, medium, large. Default:
- tiny.
- --hfid Hugging Face model ID. Use only when
- --model-type is huggingface. Example:
- "openai/whisper-tiny"
- -l, --language Language code. Allowed: af (Afrikaans), am
- (Amharic), ar (Arabic), as (Assamese), az
- (Azerbaijani), ba (Bashkir), be (Belarusian),
- bg (Bulgarian), bn (Bengali), bo (Tibetan), br
- (Breton), bs (Bosnian), ca (Catalan), cs
- (Czech), cy (Welsh), da (Danish), de (German),
- el (Greek), en (English), es (Spanish), et
- (Estonian), eu (Basque), fa (Persian), fi
- (Finnish), fo (Faroese), fr (French), gl
- (Galician), gu (Gujarati), ha (Hausa), haw
- (Hawaiian), he (Hebrew), hi (Hindi), hr
- (Croatian), ht (Haitian Creole), hu
- (Hungarian), hy (Armenian), id (Indonesian), is
- (Icelandic), it (Italian), ja (Japanese), jw
- (Javanese), ka (Georgian), kk (Kazakh), km
- (Khmer), kn (Kannada), ko (Korean), la (Latin),
- lb (Luxembourgish), ln (Lingala), lo (Lao), lt
- (Lithuanian), lv (Latvian), mg (Malagasy), mi
- (Maori), mk (Macedonian), ml (Malayalam), mn
- (Mongolian), mr (Marathi), ms (Malay), mt
- (Maltese), my (Myanmar), ne (Nepali), nl
- (Dutch), nn (Nynorsk), no (Norwegian), oc
- (Occitan), pa (Punjabi), pl (Polish), ps
- (Pashto), pt (Portuguese), ro (Romanian), ru
- (Russian), sa (Sanskrit), sd (Sindhi), si
- (Sinhala), sk (Slovak), sl (Slovenian), sn
- (Shona), so (Somali), sq (Albanian), sr
- (Serbian), su (Sundanese), sv (Swedish), sw
- (Swahili), ta (Tamil), te (Telugu), tg (Tajik),
- th (Thai), tk (Turkmen), tl (Tagalog), tr
- (Turkish), tt (Tatar), uk (Ukrainian), ur
- (Urdu), uz (Uzbek), vi (Vietnamese), yi
- (Yiddish), yo (Yoruba), zh (Chinese). Leave
- empty to detect language.
- -p, --prompt Initial prompt.
- -w, --word-timestamps Generate word-level timestamps. (available since 1.2.0)
- --openai-token OpenAI access token. Use only when
- --model-type is openaiapi. Defaults to your
- previously saved access token, if one exists.
- --srt Output result in an SRT file.
- --vtt Output result in a VTT file.
- --txt Output result in a TXT file.
- --hide-gui Hide the main application window. (available since 1.2.0)
- -h, --help Displays help on commandline options.
- --help-all Displays help including Qt specific options.
- -v, --version Displays version information.
-
-Arguments:
- files or urls Input file paths or urls. Url import availalbe since 1.2.0.
-```
-
-**示例**:
-
-```shell
-# 使用 OpenAI Whisper API 将两个 MP3 文件从法语翻译为英语
-buzz add --task translate --language fr --model-type openaiapi /Users/user/Downloads/1b3b03e4-8db5-ea2c-ace5-b71ff32e3304.mp3 /Users/user/Downloads/koaf9083k1lkpsfdi0.mp3
-
-# 使用 Whisper.cpp "small" 模型转录一个 MP4 文件,并立即导出为 SRT 和 VTT 文件
-buzz add --task transcribe --model-type whispercpp --model-size small --prompt "My initial prompt(我的初始提示)" --srt --vtt /Users/user/Downloads/buzz/1b3b03e4-8db5-ea2c-ace5-b71ff32e3304.mp4
-```
diff --git a/docs/i18n/zh/docusaurus-plugin-content-docs/current/faq.md b/docs/i18n/zh/docusaurus-plugin-content-docs/current/faq.md
deleted file mode 100644
index 48425cb3..00000000
--- a/docs/i18n/zh/docusaurus-plugin-content-docs/current/faq.md
+++ /dev/null
@@ -1,94 +0,0 @@
----
-title: 常见问题(FAQ)
-sidebar_position: 5
----
-
-### 1. 模型存储在哪里?
-
-模型存储在以下位置:
-
-- Linux: `~/.cache/Buzz`
-- Mac OS: `~/Library/Caches/Buzz`
-- Windows: `%USERPROFILE%\AppData\Local\Buzz\Buzz\Cache`
-
-将上述路径粘贴到文件管理器中即可访问模型。
-
-### 2. 如果转录速度太慢,我可以尝试什么?
-
-语音识别需要大量计算资源,您可以尝试使用较小的 Whisper 模型,或者使用 Whisper.cpp 模型在本地计算机上运行语音识别。如果您的计算机配备了至少 6GB VRAM 的 GPU,可以尝试使用 Faster Whisper 模型。
-
-Buzz 还支持使用 OpenAI API 在远程服务器上进行语音识别。要使用此功能,您需要在“偏好设置”中设置 OpenAI API 密钥。详情请参见 [偏好设置](https://chidiwilliams.github.io/buzz/docs/preferences) 部分。
-
-### 3. 如何录制系统音频?
-
-要转录系统音频,您需要配置虚拟音频设备,并将希望转录的应用程序输出连接到该虚拟扬声器。然后,您可以在 Buzz 中选择该设备作为音源。详情请参见 [使用指南](https://chidiwilliams.github.io/buzz/docs/usage/live_recording) 部分。
-
-相关工具:
-
-- Mac OS - [BlackHole](https://github.com/ExistentialAudio/BlackHole)
-- Windows - [VB CABLE](https://vb-audio.com/Cable/)
-- Linux - [PulseAudio Volume Control](https://wiki.ubuntu.com/record_system_sound)
-
-### 4. 我应该使用哪个模型?
-
-选择模型大小取决于您的硬件和使用场景。较小的模型运行速度更快,但准确性较低;较大的模型更准确,但需要更强的硬件或更长的转录时间。
-
-在选择大模型时,请参考以下信息:
-
-- **“Large”** 是最早发布的模型
-- **“Large-V2”** 是后续改进版,准确率更高,被认为是某些语言中最稳定的选择
-- **“Large-V3”** 是最新版本,在许多情况下准确性最佳,但有时可能会产生错误的单词
-- **“Turbo”** 模型在速度和准确性之间取得了良好平衡
-
-最好的方法是测试所有模型,以找到最适合您语言的选项。
-
-### 5. 如何使用 GPU 加速以提高转录速度?
-
-- 在 **Linux** 上,Nvidia GPU 受支持,可直接使用 GPU 加速。如果遇到问题,请安装 [CUDA 12](https://developer.nvidia.com/cuda-downloads)、[cuBLAS](https://developer.nvidia.com/cublas) 和 [cuDNN](https://developer.nvidia.com/cudnn)。
-- 在 **Windows** 上,请参阅[此说明](https://github.com/chidiwilliams/buzz/blob/main/CONTRIBUTING.md#gpu-support) 以启用 CUDA GPU 支持。
-- **Faster Whisper** 需要 CUDA 12,使用旧版 CUDA 的计算机将默认使用 CPU。
-
-### 6. 如何修复 `Unanticipated host error[PaErrorCode-9999]`?
-
-请检查系统设置,确保没有阻止应用访问麦克风。
-
-- **Windows** 用户请检查“设置 -> 隐私 -> 麦克风”,确保 Buzz 有权限使用麦克风。
-- 参考此视频的 [方法 1](https://www.youtube.com/watch?v=eRcCYgOuSYQ)。
-- **方法 2** 无需卸载防病毒软件,但可以尝试暂时禁用,或检查是否有相关设置阻止 Buzz 访问麦克风。
-
-### 7. 可以在没有互联网的计算机上使用 Buzz 吗?
-
-是的,您可以在离线计算机上使用 Buzz,但需要在另一台联网计算机上下载所需模型,并手动将其移动到离线计算机。
-
-最简单的方法是:
-
-1. 打开“帮助 -> 偏好设置 -> 模型”
-2. 下载所需的模型
-3. 点击“显示文件位置”按钮,打开存储模型的文件夹
-4. 将该模型文件夹复制到离线计算机的相同位置
-
-例如,在 Linux 上,模型存储在 `~/.cache/Buzz/models` 目录中。
-
-### 8. Buzz 崩溃了,怎么办?
-
-如果模型下载不完整或损坏,Buzz 可能会崩溃。尝试删除已下载的模型文件,然后重新下载。
-
-如果问题仍然存在,请检查日志文件并[报告问题](https://github.com/chidiwilliams/buzz/issues),以便我们修复。日志文件位置如下:
-
-- Mac OS: `~/Library/Logs/Buzz`
-- Windows: `%USERPROFILE%\AppData\Local\Buzz\Buzz\Logs`
-- Linux: 在终端运行 Buzz 查看相关错误信息。
-
-### 9. 哪里可以获取最新的开发版本?
-
-最新的开发版本包含最新的错误修复和新功能。如果您喜欢尝试新功能,可以下载最新的开发版本进行测试。
-
-- **Linux** 用户可以运行以下命令获取最新版本:
- ```sh
- sudo snap install buzz --edge
- ```
-- **其他平台** 请按以下步骤操作:
- 1. 访问 [构建页面](https://github.com/chidiwilliams/buzz/actions/workflows/ci.yml?query=branch%3Amain)
- 2. 点击最新构建的链接
- 3. 在构建页面向下滚动到“Artifacts”部分
- 4. 下载安装文件(请注意,您需要登录 GitHub 才能看到下载链接)
diff --git a/docs/i18n/zh/docusaurus-plugin-content-docs/current/index.md b/docs/i18n/zh/docusaurus-plugin-content-docs/current/index.md
deleted file mode 100644
index 86967bbb..00000000
--- a/docs/i18n/zh/docusaurus-plugin-content-docs/current/index.md
+++ /dev/null
@@ -1,23 +0,0 @@
----
-title: 介绍
-sidebar_position: 1
----
-
-在您的个人电脑上离线转录和翻译音频。由 OpenAI 的 [Whisper](https://github.com/openai/whisper) 提供支持。
-
-
-[](https://github.com/chidiwilliams/buzz/actions/workflows/ci.yml)
-[](https://codecov.io/github/chidiwilliams/buzz)
-
-[](https://GitHub.com/chidiwilliams/buzz/releases/)
-
-## 功能
-
-- 导入音频和视频文件,并将转录内容导出为 TXT、SRT 和 VTT 格式([演示](https://www.loom.com/share/cf263b099ac3481082bb56d19b7c87fe))
-- 从电脑麦克风转录和翻译为文本(资源密集型,可能无法实时完成,[演示](https://www.loom.com/share/564b753eb4d44b55b985b8abd26b55f7))
-- 支持 [Whisper](https://github.com/openai/whisper#available-models-and-languages)、
- [Whisper.cpp](https://github.com/ggerganov/whisper.cpp)、[Faster Whisper](https://github.com/guillaumekln/faster-whisper)、
- [Whisper 兼容的 Hugging Face 模型](https://huggingface.co/models?other=whisper) 和
- [OpenAI Whisper API](https://platform.openai.com/docs/api-reference/introduction)
-- [命令行界面](#命令行界面)
-- 支持 Mac、Windows 和 Linux
diff --git a/docs/i18n/zh/docusaurus-plugin-content-docs/current/installation.md b/docs/i18n/zh/docusaurus-plugin-content-docs/current/installation.md
deleted file mode 100644
index b3bbc7c1..00000000
--- a/docs/i18n/zh/docusaurus-plugin-content-docs/current/installation.md
+++ /dev/null
@@ -1,47 +0,0 @@
----
-title: 安装
-sidebar_position: 2
----
-
-要安装 Buzz,请下载适用于您操作系统的[最新版本](https://github.com/chidiwilliams/buzz/releases/latest)。Buzz 支持 **Mac**(Intel)、**Windows** 和 **Linux** 系统。
-
-## macOS(Intel,macOS 11.7 及更高版本)
-
-通过 [brew](https://brew.sh/) 安装:
-
-```shell
-brew install --cask buzz
-```
-
-或者,下载并运行 `Buzz-x.y.z.dmg` 文件。
-
-对于 Mac Silicon 用户(以及希望在 Mac Intel 上获得更好体验的用户)。
-
-## Windows(Windows 10 及更高版本)
-
-下载并运行 `Buzz-x.y.z.exe` 文件。
-
-## Linux
-
-```shell
-sudo apt-get install libportaudio2 libcanberra-gtk-module libcanberra-gtk3-module
-sudo snap install buzz
-sudo snap connect buzz:password-manager-service
-```
-
-[](https://snapcraft.io/buzz)
-
-或者,在 Ubuntu 20.04 及更高版本上,安装依赖项:
-
-```shell
-sudo apt-get install libportaudio2
-```
-
-然后,下载并解压 `Buzz-x.y.z-unix.tar.gz` 文件。
-
-## PyPI
-
-```shell
-pip install buzz-captions
-python -m buzz
-```
diff --git a/docs/i18n/zh/docusaurus-plugin-content-docs/current/preferences.md b/docs/i18n/zh/docusaurus-plugin-content-docs/current/preferences.md
deleted file mode 100644
index b699039c..00000000
--- a/docs/i18n/zh/docusaurus-plugin-content-docs/current/preferences.md
+++ /dev/null
@@ -1,92 +0,0 @@
----
-title: 偏好设置
-sidebar_position: 4
----
-
-从菜单栏打开偏好设置窗口,或点击 `Ctrl/Cmd + ,`。
-
-## 常规偏好设置
-
-### OpenAI API 偏好设置
-
-**API 密钥** - 用于验证 OpenAI API 请求的密钥。要获取 OpenAI 的 API 密钥,请参阅 [此文章](https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key)。
-
-**基础 URL** - 默认情况下,所有请求都会发送到 OpenAI 公司提供的 API。他们的 API URL 是 `https://api.openai.com/v1/`。其他公司也提供了兼容的 API。你可以在 [讨论页面](https://github.com/chidiwilliams/buzz/discussions/827) 找到可用的 API URL 列表。
-
-### 默认导出文件名
-
-设置文件识别的默认导出文件名。例如,值为 `{{ input_file_name }} ({{ task }}d on {{ date_time }})` 时,TXT 导出文件将默认保存为`Input Filename (transcribed on 19-Sep-2023 20-39-25).txt`(输入文件名 (转录于 19-Sep-2023 20-39-25).txt)。
-
-可用变量:
-
-| 键 | 描述 | 示例 |
-| ----------------- | ------------------------------------- | ---------------------------------------------------------- |
-| `input_file_name` | 导入文件的文件名 | `audio`(例如,如果导入的文件路径是 `/path/to/audio.wav`) |
-| `task` | 转录任务 | `transcribe`, `translate` |
-| `language` | 语言代码 | `en`, `fr`, `yo` 等 |
-| `model_type` | 模型类型 | `Whisper`, `Whisper.cpp`, `Faster Whisper` 等 |
-| `model_size` | 模型大小 | `tiny`, `base`, `small`, `medium`, `large` 等 |
-| `date_time` | 导出时间(格式:`%d-%b-%Y %H-%M-%S`) | `19-Sep-2023 20-39-25` |
-
-### 实时识别导出
-
-实时识别导出可用于将 Buzz 与其他应用程序(如 OBS Studio)集成。
-启用后,实时文本识别将在生成和翻译时导出到文本文件。
-
-如果为实时录音启用了 AI 翻译,翻译后的文本也将导出到文本文件。
-翻译文本的文件名将以 `.translated.txt` 结尾。
-
-### 实时识别模式
-
-有三种转识别式可用:
-
-**下方追加** - 新句子将在现有内容下方添加,并在它们之间留有空行。最后一句话将位于底部。
-
-**上方追加** - 新句子将在现有内容上方添加,并在它们之间留有空行。最后一句话将位于顶部。
-
-**追加并修正** - 新句子将在现有转录内容的末尾添加,中间不留空行。此模式还会尝试修正之前转录句子末尾的错误。此模式需要更多的处理能力和更强大的硬件支持。
-
-## 高级偏好设置
-
-为了简化新用户的偏好设置部分,一些更高级的设置可以通过操作系统环境变量进行配置。在启动 Buzz 之前,请在操作系统中设置必要的环境变量,或创建一个脚本来设置它们。
-
-在 MacOS 和 Linux 上,创建 `run_buzz.sh`,内容如下:
-
-```bash
-#!/bin/bash
-export VARIABLE=value
-export SOME_OTHER_VARIABLE=some_other_value
-buzz
-```
-
-在 Windows 上,创建 `run_buzz.bat`,内容如下:
-
-```bat
-@echo off
-set VARIABLE=value
-set SOME_OTHER_VARIABLE=some_other_value
-"C:\Program Files (x86)\Buzz\Buzz.exe"
-```
-
-或者,你可以在操作系统设置中设置环境变量。更多信息请参阅 [此指南](https://phoenixnap.com/kb/windows-set-environment-variable#ftoc-heading-4) 或 [此视频](https://www.youtube.com/watch?v=bEroNNzqlF4)。
-
-### 可用变量
-
-**BUZZ_WHISPERCPP_N_THREADS** - Whisper.cpp 模型使用的线程数。默认为 `4`。
-在具有 16 线程的笔记本电脑上,设置 `BUZZ_WHISPERCPP_N_THREADS=8` 可以使转录时间加快约 15%。
-进一步增加线程数会导致转录时间变慢,因为并行线程的结果需要合并以生成最终答案。
-
-**BUZZ_TRANSLATION_API_BASE_URl** - 用于翻译的 OpenAI 兼容 API 的基础 URL。
-
-**BUZZ_TRANSLATION_API_KEY** - 用于翻译的 OpenAI 兼容 API 的密钥。
-
-**BUZZ_MODEL_ROOT** - 存储模型文件的根目录。
-默认为 [user_cache_dir](https://pypi.org/project/platformdirs/)。
-
-**BUZZ_FAVORITE_LANGUAGES** - 以逗号分隔的支持语言代码列表,显示在语言列表顶部。
-
-**BUZZ_DOWNLOAD_COOKIEFILE** - 用于下载私有视频或绕过反机器人保护的 [cookiefile](https://github.com/yt-dlp/yt-dlp/wiki/FAQ#how-do-i-pass-cookies-to-yt-dlp) 的位置。
-
-**BUZZ_FORCE_CPU** - 强制 Buzz 使用 CPU 而不是 GPU,适用于旧 GPU 较慢或 GPU 有问题的设置。示例用法:`BUZZ_FORCE_CPU=true`。自 `1.2.1` 版本起可用。
-
-**BUZZ_MERGE_REGROUP_RULE** - 合并带有单词级时间戳的转录时使用的自定义重新分组规则。更多可用选项的信息请参阅 [stable-ts 仓库](https://github.com/jianfch/stable-ts?tab=readme-ov-file#regrouping-methods)。自 `1.3.0` 版本起可用。
diff --git a/docs/i18n/zh/docusaurus-plugin-content-docs/current/usage/1_file_import.md b/docs/i18n/zh/docusaurus-plugin-content-docs/current/usage/1_file_import.md
deleted file mode 100644
index 5f81620f..00000000
--- a/docs/i18n/zh/docusaurus-plugin-content-docs/current/usage/1_file_import.md
+++ /dev/null
@@ -1,21 +0,0 @@
----
-title: 文件导入
----
-
-若要导入文件:
-
-- 点击“文件”菜单中的“导入媒体文件”(或者点击工具栏上的“+”图标,也可以使用快捷键 **Command/Ctrl + O**)。
-- 选择一个音频或视频文件。
-- 选择任务、语言和模型设置。
-- 点击“运行”。
-- 当转录状态显示为“已完成”时,双击该行(或者选中该行后点击“⤢”图标)即可打开转录内容。
-
-| 字段 | 选项 | 默认值 | 描述 |
-| ------------ | ------------------- | ------ | -------------------------------------------------------------------------------------------------------- |
-| 导出格式 | "TXT"、"SRT"、"VTT" | "TXT" | 导出文件的格式 |
-| 单词级时间戳 | 关闭 / 开启 | 关闭 | 若勾选此项,转录内容将为音频中的每个单词生成单独的字幕行。仅当“导出格式”设置为“SRT”或“VTT”时此选项可用。 |
-| 提取语音 | 关闭 / 开启 | 关闭 | 若勾选此项,语音将被提取到单独的音轨中以提高转录准确性。此功能自 1.3.0 版本起可用。 |
-
-(有关任务、语言和质量设置的更多信息,请参阅[实时录制部分](https://chidiwilliams.github.io/buzz/zh/docs/usage/live_recording)。)
-
-[](https://www.loom.com/share/cf263b099ac3481082bb56d19b7c87fe "Buzz 中的媒体文件导入")
diff --git a/docs/i18n/zh/docusaurus-plugin-content-docs/current/usage/2_live_recording.md b/docs/i18n/zh/docusaurus-plugin-content-docs/current/usage/2_live_recording.md
deleted file mode 100644
index b75b5068..00000000
--- a/docs/i18n/zh/docusaurus-plugin-content-docs/current/usage/2_live_recording.md
+++ /dev/null
@@ -1,62 +0,0 @@
----
-title: 实时录制
----
-
-若要开始实时录制,请按以下步骤操作:
-
-- 选择录制任务、语言、质量和麦克风。
-- 点击“录制”。
-
-> **注意:** 使用默认的 Whisper 模型转录音频会占用大量系统资源。若想实现实时性能,可考虑使用 Whisper.cpp Tiny 模型。
-
-| 字段 | 选项 | 默认值 | 描述 |
-| ------ | --------------------------------------------------------------------------------------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| 任务 | "转录"、"翻译" | "转录" | "转录"会将输入音频转换为所选语言的文本,而"翻译"则会将其转换为英文文本。 |
-| 语言 | 完整的支持语言列表请参阅 [Whisper 文档](https://github.com/openai/whisper#available-models-and-languages) | "自动检测语言" | "自动检测语言"会根据音频的前几秒尝试检测其中的语言。不过,如果已知音频语言,建议手动选择,因为在很多情况下这可以提高转录质量。 |
-| 质量 | "极低"、"低"、"中"、"高" | "极低" | 转录质量决定了用于转录的 Whisper 模型。"极低"使用"tiny"模型;"低"使用"base"模型;"中"使用"small"模型;"高"使用"medium"模型。模型越大,转录质量越高,但所需的系统资源也越多。更多关于模型的信息请参阅 [Whisper 文档](https://github.com/openai/whisper#available-models-and-languages)。 |
-| 麦克风 | [系统可用麦克风] | [系统默认麦克风] | 用于录制输入音频的麦克风。 |
-
-[](https://www.loom.com/share/564b753eb4d44b55b985b8abd26b55f7 "在Buzz 上实时转录")
-
-### 录制电脑播放的音频(macOS)
-
-若要录制电脑应用程序播放的音频,你可以安装一个音频回环驱动程序(一种可让你创建虚拟音频设备的程序)。本指南后续将介绍在 Mac 上使用 [BlackHole](https://github.com/ExistentialAudio/BlackHole) 的方法,但你也可以根据自己的操作系统选择其他替代方案(例如 [LoopBeAudio](https://nerds.de/en/loopbeaudio.html)、[LoopBack](https://rogueamoeba.com/loopback/) 和 [Virtual Audio Cable](https://vac.muzychenko.net/en/))。
-
-1. [通过 Homebrew 安装 BlackHole](https://github.com/ExistentialAudio/BlackHole#option-2-install-via-homebrew)
-
- ```shell
- brew install blackhole-2ch
- ```
-
-2. 通过聚焦搜索(Spotlight)或直接打开 `/Applications/Utilities/Audio Midi Setup.app` 来启动“音频 MIDI 设置”。
-
-
-
-3. 点击窗口左下角的“+”图标,然后选择“创建多输出设备”。
-
-
-
-4. 将你的默认扬声器和 BlackHole 添加到这个多输出设备中。
-
-
-
-5. 将此多输出设备设置为你的扬声器(可在应用程序内或系统全局进行设置),这样音频就会被输送到 BlackHole 中。
-
-6. 打开 Buzz 软件,选择 BlackHole 作为录音的麦克风,接着像平常一样进行录制,你就能看到通过 BlackHole 播放的音频的转录文本了。
-
-### 录制电脑播放的音频(Windows)
-
-若要转录系统音频,你需要配置虚拟音频设备,并将你想要转录的应用程序的音频输出连接到该虚拟扬声器。之后,你就可以在 Buzz 中选择它作为音频源。
-
-1. 安装 [VB CABLE](https://vb - audio.com/Cable/) 作为虚拟音频设备。
-2. 使用 Windows 声音设置进行配置。右键单击系统托盘里的扬声器图标,然后选择“打开声音设置”。在“选择你的输出设备”下拉菜单中,选择“CABLE Input”,将所有系统声音发送到虚拟设备;或者使用“高级声音选项”,选择要将声音输出到该设备的应用程序。
-
-### 录制电脑播放的音频(Linux)
-
-正如 [Ubuntu 维基](https://wiki.ubuntu.com/record_system_sound?uselang=zh) 中所述,在任何使用 PulseAudio 的 Linux 系统上,你可以将应用程序的音频重定向到虚拟扬声器。之后,你可以在 Buzz 中选择它作为音频源。
-
-总体步骤如下:
-
-1. 启动会产生你想要转录的声音的应用程序,并开始播放。例如,在媒体播放器中播放视频。
-2. 启动 Buzz 并打开实时录制界面,以便查看设置。
-3. 在 PulseAudio 音量控制(`pavucontrol`)的“录制”选项卡中,配置从你想要转录声音的应用程序到 Buzz 的声音路由。
diff --git a/docs/i18n/zh/docusaurus-plugin-content-docs/current/usage/3_translations.md b/docs/i18n/zh/docusaurus-plugin-content-docs/current/usage/3_translations.md
deleted file mode 100644
index 2bde40db..00000000
--- a/docs/i18n/zh/docusaurus-plugin-content-docs/current/usage/3_translations.md
+++ /dev/null
@@ -1,17 +0,0 @@
----
-title: 翻译功能
----
-
-默认的“翻译”任务借助 Whisper 模型将内容翻译成英语。从 `1.0.0` 版本开始,Buzz 支持使用其他人工智能将内容翻译成任意语言。
-
-若要使用翻译功能,你需要配置 OpenAI API 密钥和翻译设置。在“偏好设置”中设置 OpenAI API 密钥。Buzz 也支持本地运行的、兼容 OpenAI API 的自定义翻译人工智能。有关本地运行人工智能的更多信息,请参阅 [ollama](https://ollama.com/blog/openai-compatibility) 或 [LM Studio](https://lmstudio.ai/)。有关可用自定义 API 的信息,请查看这个 [讨论线程](https://github.com/chidiwilliams/buzz/discussions/827)。
-
-若要为实时录制配置翻译功能,可在实时录制设置的“高级设置”对话框中启用该功能。输入要使用的人工智能模型,并提供给人工智能的翻译指令提示。对于已经完成语音识别的文件,也可以使用翻译功能。在转录查看器工具栏上点击“翻译”按钮即可。
-
-为了让人工智能知道如何进行翻译,请在“给人工智能的指令”部分输入翻译说明。在说明中,你应该明确指出要将文本翻译成何种语言。此外,由于人工智能往往会添加一些注释或备注,你可能需要额外添加指令禁止其这么做。以下是一个将英语字幕翻译成西班牙语的指令示例:
-
-> 你是一位专业翻译人员,擅长将英语翻译成西班牙语。你只需将发给你的每一句话翻译成西班牙语,不要添加任何注释或备注。
-
-如果你在“偏好设置”中启用了“启用实时录制转录导出”功能,实时文本转录内容在生成和翻译后将被导出到一个文本文件中。这个文件可用于将实时转录内容与其他应用程序(如 OBS Studio)进行进一步集成。
-
-使用 ChatGPT `gpt - 4o` 模型对一小时长的音频进行翻译,大致费用约为 0.50 美元。
diff --git a/docs/i18n/zh/docusaurus-plugin-content-docs/current/usage/4_edit_and_resize.md b/docs/i18n/zh/docusaurus-plugin-content-docs/current/usage/4_edit_and_resize.md
deleted file mode 100644
index 18ebb5ff..00000000
--- a/docs/i18n/zh/docusaurus-plugin-content-docs/current/usage/4_edit_and_resize.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-title: 编辑与调整
----
-
-当某个音频或视频文件完成转录后,你可以对其进行编辑,并将其导出为不同的字幕格式或纯文本。在转录列表中双击转录内容,即可查看用于编辑和导出的其他选项。
-
-转录视图界面提供了调整转录内容的选项。点击“调整”按钮,可查看可用的选项。对于在 **启用单词级时间戳** 设置下生成的转录内容,可以通过指定不同选项(如字幕的最大长度以及是否应在标点处拆分字幕)将其合并成字幕。而对于在 **未启用单词级时间戳** 设置下生成的转录内容,仅能通过指定所需的字幕最大长度来重新组合。
-
-如果系统中仍存在音频文件,单词级时间戳合并操作还会分析音频中的静音部分,以提高字幕的准确性。从带有单词级时间戳的转录内容生成字幕的功能自 1.3.0 版本起可用。
diff --git a/docs/i18n/zh/docusaurus-plugin-content-docs/current/usage/_category_.yml b/docs/i18n/zh/docusaurus-plugin-content-docs/current/usage/_category_.yml
deleted file mode 100644
index 19fa6741..00000000
--- a/docs/i18n/zh/docusaurus-plugin-content-docs/current/usage/_category_.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-label: 使用方法
-position: 3
diff --git a/docs/package-lock.json b/docs/package-lock.json
deleted file mode 100644
index 7ac8a131..00000000
--- a/docs/package-lock.json
+++ /dev/null
@@ -1,12662 +0,0 @@
-{
- "name": "docs",
- "version": "0.0.0",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "": {
- "name": "docs",
- "version": "0.0.0",
- "dependencies": {
- "@docusaurus/core": "2.4.1",
- "@docusaurus/preset-classic": "2.4.1",
- "@mdx-js/react": "^1.6.22",
- "clsx": "^1.2.1",
- "prism-react-renderer": "^1.3.5",
- "react": "^17.0.2",
- "react-dom": "^17.0.2"
- },
- "devDependencies": {
- "@docusaurus/module-type-aliases": "2.4.1",
- "@tsconfig/docusaurus": "^1.0.5",
- "typescript": "^4.7.4"
- },
- "engines": {
- "node": ">=16.14"
- }
- },
- "node_modules/@algolia/autocomplete-core": {
- "version": "1.9.3",
- "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.9.3.tgz",
- "integrity": "sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw==",
- "dependencies": {
- "@algolia/autocomplete-plugin-algolia-insights": "1.9.3",
- "@algolia/autocomplete-shared": "1.9.3"
- }
- },
- "node_modules/@algolia/autocomplete-plugin-algolia-insights": {
- "version": "1.9.3",
- "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.9.3.tgz",
- "integrity": "sha512-a/yTUkcO/Vyy+JffmAnTWbr4/90cLzw+CC3bRbhnULr/EM0fGNvM13oQQ14f2moLMcVDyAx/leczLlAOovhSZg==",
- "dependencies": {
- "@algolia/autocomplete-shared": "1.9.3"
- },
- "peerDependencies": {
- "search-insights": ">= 1 < 3"
- }
- },
- "node_modules/@algolia/autocomplete-preset-algolia": {
- "version": "1.9.3",
- "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.9.3.tgz",
- "integrity": "sha512-d4qlt6YmrLMYy95n5TB52wtNDr6EgAIPH81dvvvW8UmuWRgxEtY0NJiPwl/h95JtG2vmRM804M0DSwMCNZlzRA==",
- "dependencies": {
- "@algolia/autocomplete-shared": "1.9.3"
- },
- "peerDependencies": {
- "@algolia/client-search": ">= 4.9.1 < 6",
- "algoliasearch": ">= 4.9.1 < 6"
- }
- },
- "node_modules/@algolia/autocomplete-shared": {
- "version": "1.9.3",
- "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.9.3.tgz",
- "integrity": "sha512-Wnm9E4Ye6Rl6sTTqjoymD+l8DjSTHsHboVRYrKgEt8Q7UHm9nYbqhN/i0fhUYA3OAEH7WA8x3jfpnmJm3rKvaQ==",
- "peerDependencies": {
- "@algolia/client-search": ">= 4.9.1 < 6",
- "algoliasearch": ">= 4.9.1 < 6"
- }
- },
- "node_modules/@algolia/cache-browser-local-storage": {
- "version": "4.17.2",
- "resolved": "https://registry.npmjs.org/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.17.2.tgz",
- "integrity": "sha512-ZkVN7K/JE+qMQbpR6h3gQOGR6yCJpmucSBCmH5YDxnrYbp2CbrVCu0Nr+FGVoWzMJNznj1waShkfQ9awERulLw==",
- "dependencies": {
- "@algolia/cache-common": "4.17.2"
- }
- },
- "node_modules/@algolia/cache-common": {
- "version": "4.17.2",
- "resolved": "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.17.2.tgz",
- "integrity": "sha512-fojbhYIS8ovfYs6hwZpy1O4mBfVRxNgAaZRqsdVQd54hU4MxYDYFCxagYX28lOBz7btcDHld6BMoWXvjzkx6iQ=="
- },
- "node_modules/@algolia/cache-in-memory": {
- "version": "4.17.2",
- "resolved": "https://registry.npmjs.org/@algolia/cache-in-memory/-/cache-in-memory-4.17.2.tgz",
- "integrity": "sha512-UYQcMzPurNi+cPYkuPemTZkjKAjdgAS1hagC5irujKbrYnN4yscK4TkOI5tX+O8/KegtJt3kOK07OIrJ2QDAAw==",
- "dependencies": {
- "@algolia/cache-common": "4.17.2"
- }
- },
- "node_modules/@algolia/client-account": {
- "version": "4.17.2",
- "resolved": "https://registry.npmjs.org/@algolia/client-account/-/client-account-4.17.2.tgz",
- "integrity": "sha512-doSk89pBPDpDyKJSHFADIGa2XSGrBCj3QwPvqtRJXDADpN+OjW+eTR8r4hEs/7X4GGfjfAOAES8JgDx+fZntYw==",
- "dependencies": {
- "@algolia/client-common": "4.17.2",
- "@algolia/client-search": "4.17.2",
- "@algolia/transporter": "4.17.2"
- }
- },
- "node_modules/@algolia/client-analytics": {
- "version": "4.17.2",
- "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-4.17.2.tgz",
- "integrity": "sha512-V+DcXbOtD/hKwAR3qGQrtlrJ3q2f9OKfx843q744o4m3xHv5ueCAvGXB1znPsdaUrVDNAImcgEgqwI9x7EJbDw==",
- "dependencies": {
- "@algolia/client-common": "4.17.2",
- "@algolia/client-search": "4.17.2",
- "@algolia/requester-common": "4.17.2",
- "@algolia/transporter": "4.17.2"
- }
- },
- "node_modules/@algolia/client-common": {
- "version": "4.17.2",
- "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.17.2.tgz",
- "integrity": "sha512-gKBUnjxi0ukJYIJxVREYGt1Dmj1B3RBYbfGWi0dIPp1BC1VvQm+BOuNwsIwmq/x3MPO+sGuK978eKiP3tZDvag==",
- "dependencies": {
- "@algolia/requester-common": "4.17.2",
- "@algolia/transporter": "4.17.2"
- }
- },
- "node_modules/@algolia/client-personalization": {
- "version": "4.17.2",
- "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-4.17.2.tgz",
- "integrity": "sha512-wc4UgOWxSYWz5wpuelNmlt895jA9twjZWM2ms17Ws8qCvBHF7OVGdMGgbysPB8790YnfvvDnSsWOv3CEj26Eow==",
- "dependencies": {
- "@algolia/client-common": "4.17.2",
- "@algolia/requester-common": "4.17.2",
- "@algolia/transporter": "4.17.2"
- }
- },
- "node_modules/@algolia/client-search": {
- "version": "4.17.2",
- "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.17.2.tgz",
- "integrity": "sha512-FUjIs+gRe0upJC++uVs4sdxMw15JxfkT86Gr/kqVwi9kcqaZhXntSbW/Fw959bIYXczjmeVQsilYvBWW4YvSZA==",
- "dependencies": {
- "@algolia/client-common": "4.17.2",
- "@algolia/requester-common": "4.17.2",
- "@algolia/transporter": "4.17.2"
- }
- },
- "node_modules/@algolia/events": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/@algolia/events/-/events-4.0.1.tgz",
- "integrity": "sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ=="
- },
- "node_modules/@algolia/logger-common": {
- "version": "4.17.2",
- "resolved": "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.17.2.tgz",
- "integrity": "sha512-EfXuweUE+1HiSMsQidaDWA5Lv4NnStYIlh7PO5pLkI+sdhbMX0e5AO5nUAMIFM1VkEANes70RA8fzhP6OqCqQQ=="
- },
- "node_modules/@algolia/logger-console": {
- "version": "4.17.2",
- "resolved": "https://registry.npmjs.org/@algolia/logger-console/-/logger-console-4.17.2.tgz",
- "integrity": "sha512-JuG8HGVlJ+l/UEDK4h2Y8q/IEmRjQz1J0aS9tf6GPNbGYiSvMr1DDdZ+hqV3bb1XE6wU8Ypex56HisWMSpnG0A==",
- "dependencies": {
- "@algolia/logger-common": "4.17.2"
- }
- },
- "node_modules/@algolia/requester-browser-xhr": {
- "version": "4.17.2",
- "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.17.2.tgz",
- "integrity": "sha512-FKI2lYWwksALfRt2OETFmGb5+P7WVc4py2Ai3H7k8FSfTLwVvs9WVVmtlx6oANQ8RFEK4B85h8DQJTJ29TDfmA==",
- "dependencies": {
- "@algolia/requester-common": "4.17.2"
- }
- },
- "node_modules/@algolia/requester-common": {
- "version": "4.17.2",
- "resolved": "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.17.2.tgz",
- "integrity": "sha512-Rfim23ztAhYpE9qm+KCfCRo+YLJCjiiTG+IpDdzUjMpYPhUtirQT0A35YEd/gKn86YNyydxS9w8iRSjwKh+L0A=="
- },
- "node_modules/@algolia/requester-node-http": {
- "version": "4.17.2",
- "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.17.2.tgz",
- "integrity": "sha512-E0b0kyCDMvUIhQmDNd/mH4fsKJdEEX6PkMKrYJjzm6moo+rP22tqpq4Rfe7DZD8OB6/LsDD3zs3Kvd+L+M5wwQ==",
- "dependencies": {
- "@algolia/requester-common": "4.17.2"
- }
- },
- "node_modules/@algolia/transporter": {
- "version": "4.17.2",
- "resolved": "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.17.2.tgz",
- "integrity": "sha512-m8pXlz5OnNzjD1rcw+duCN4jG4yEzkJBsvKYMoN22Oq6rQwy1AY5muZ+IQUs4dL+A364CYkRMLRWhvXpCZ1x+g==",
- "dependencies": {
- "@algolia/cache-common": "4.17.2",
- "@algolia/logger-common": "4.17.2",
- "@algolia/requester-common": "4.17.2"
- }
- },
- "node_modules/@ampproject/remapping": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz",
- "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==",
- "dependencies": {
- "@jridgewell/gen-mapping": "^0.3.0",
- "@jridgewell/trace-mapping": "^0.3.9"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@babel/code-frame": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz",
- "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==",
- "dependencies": {
- "@babel/highlight": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/compat-data": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.5.tgz",
- "integrity": "sha512-4Jc/YuIaYqKnDDz892kPIledykKg12Aw1PYX5i/TY28anJtacvM1Rrr8wbieB9GfEJwlzqT0hUEao0CxEebiDA==",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/core": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.5.tgz",
- "integrity": "sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg==",
- "dependencies": {
- "@ampproject/remapping": "^2.2.0",
- "@babel/code-frame": "^7.22.5",
- "@babel/generator": "^7.22.5",
- "@babel/helper-compilation-targets": "^7.22.5",
- "@babel/helper-module-transforms": "^7.22.5",
- "@babel/helpers": "^7.22.5",
- "@babel/parser": "^7.22.5",
- "@babel/template": "^7.22.5",
- "@babel/traverse": "^7.22.5",
- "@babel/types": "^7.22.5",
- "convert-source-map": "^1.7.0",
- "debug": "^4.1.0",
- "gensync": "^1.0.0-beta.2",
- "json5": "^2.2.2",
- "semver": "^6.3.0"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/babel"
- }
- },
- "node_modules/@babel/core/node_modules/semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
- "node_modules/@babel/generator": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.5.tgz",
- "integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==",
- "dependencies": {
- "@babel/types": "^7.22.5",
- "@jridgewell/gen-mapping": "^0.3.2",
- "@jridgewell/trace-mapping": "^0.3.17",
- "jsesc": "^2.5.1"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-annotate-as-pure": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz",
- "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==",
- "dependencies": {
- "@babel/types": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.5.tgz",
- "integrity": "sha512-m1EP3lVOPptR+2DwD125gziZNcmoNSHGmJROKoy87loWUQyJaVXDgpmruWqDARZSmtYQ+Dl25okU8+qhVzuykw==",
- "dependencies": {
- "@babel/types": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-compilation-targets": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.5.tgz",
- "integrity": "sha512-Ji+ywpHeuqxB8WDxraCiqR0xfhYjiDE/e6k7FuIaANnoOFxAHskHChz4vA1mJC9Lbm01s1PVAGhQY4FUKSkGZw==",
- "dependencies": {
- "@babel/compat-data": "^7.22.5",
- "@babel/helper-validator-option": "^7.22.5",
- "browserslist": "^4.21.3",
- "lru-cache": "^5.1.1",
- "semver": "^6.3.0"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0"
- }
- },
- "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
- "node_modules/@babel/helper-create-class-features-plugin": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.5.tgz",
- "integrity": "sha512-xkb58MyOYIslxu3gKmVXmjTtUPvBU4odYzbiIQbWwLKIHCsx6UGZGX6F1IznMFVnDdirseUZopzN+ZRt8Xb33Q==",
- "dependencies": {
- "@babel/helper-annotate-as-pure": "^7.22.5",
- "@babel/helper-environment-visitor": "^7.22.5",
- "@babel/helper-function-name": "^7.22.5",
- "@babel/helper-member-expression-to-functions": "^7.22.5",
- "@babel/helper-optimise-call-expression": "^7.22.5",
- "@babel/helper-replace-supers": "^7.22.5",
- "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5",
- "@babel/helper-split-export-declaration": "^7.22.5",
- "semver": "^6.3.0"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0"
- }
- },
- "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
- "node_modules/@babel/helper-create-regexp-features-plugin": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.5.tgz",
- "integrity": "sha512-1VpEFOIbMRaXyDeUwUfmTIxExLwQ+zkW+Bh5zXpApA3oQedBx9v/updixWxnx/bZpKw7u8VxWjb/qWpIcmPq8A==",
- "dependencies": {
- "@babel/helper-annotate-as-pure": "^7.22.5",
- "regexpu-core": "^5.3.1",
- "semver": "^6.3.0"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0"
- }
- },
- "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
- "node_modules/@babel/helper-define-polyfill-provider": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.0.tgz",
- "integrity": "sha512-RnanLx5ETe6aybRi1cO/edaRH+bNYWaryCEmjDDYyNr4wnSzyOp8T0dWipmqVHKEY3AbVKUom50AKSlj1zmKbg==",
- "dependencies": {
- "@babel/helper-compilation-targets": "^7.17.7",
- "@babel/helper-plugin-utils": "^7.16.7",
- "debug": "^4.1.1",
- "lodash.debounce": "^4.0.8",
- "resolve": "^1.14.2",
- "semver": "^6.1.2"
- },
- "peerDependencies": {
- "@babel/core": "^7.4.0-0"
- }
- },
- "node_modules/@babel/helper-define-polyfill-provider/node_modules/semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
- "node_modules/@babel/helper-environment-visitor": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz",
- "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-function-name": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz",
- "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==",
- "dependencies": {
- "@babel/template": "^7.22.5",
- "@babel/types": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-hoist-variables": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz",
- "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==",
- "dependencies": {
- "@babel/types": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-member-expression-to-functions": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz",
- "integrity": "sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ==",
- "dependencies": {
- "@babel/types": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-module-imports": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz",
- "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==",
- "dependencies": {
- "@babel/types": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-module-transforms": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz",
- "integrity": "sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==",
- "dependencies": {
- "@babel/helper-environment-visitor": "^7.22.5",
- "@babel/helper-module-imports": "^7.22.5",
- "@babel/helper-simple-access": "^7.22.5",
- "@babel/helper-split-export-declaration": "^7.22.5",
- "@babel/helper-validator-identifier": "^7.22.5",
- "@babel/template": "^7.22.5",
- "@babel/traverse": "^7.22.5",
- "@babel/types": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-optimise-call-expression": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz",
- "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==",
- "dependencies": {
- "@babel/types": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-plugin-utils": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz",
- "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-remap-async-to-generator": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.5.tgz",
- "integrity": "sha512-cU0Sq1Rf4Z55fgz7haOakIyM7+x/uCFwXpLPaeRzfoUtAEAuUZjZvFPjL/rk5rW693dIgn2hng1W7xbT7lWT4g==",
- "dependencies": {
- "@babel/helper-annotate-as-pure": "^7.22.5",
- "@babel/helper-environment-visitor": "^7.22.5",
- "@babel/helper-wrap-function": "^7.22.5",
- "@babel/types": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0"
- }
- },
- "node_modules/@babel/helper-replace-supers": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.5.tgz",
- "integrity": "sha512-aLdNM5I3kdI/V9xGNyKSF3X/gTyMUBohTZ+/3QdQKAA9vxIiy12E+8E2HoOP1/DjeqU+g6as35QHJNMDDYpuCg==",
- "dependencies": {
- "@babel/helper-environment-visitor": "^7.22.5",
- "@babel/helper-member-expression-to-functions": "^7.22.5",
- "@babel/helper-optimise-call-expression": "^7.22.5",
- "@babel/template": "^7.22.5",
- "@babel/traverse": "^7.22.5",
- "@babel/types": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-simple-access": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz",
- "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==",
- "dependencies": {
- "@babel/types": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-skip-transparent-expression-wrappers": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz",
- "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==",
- "dependencies": {
- "@babel/types": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-split-export-declaration": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz",
- "integrity": "sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==",
- "dependencies": {
- "@babel/types": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-string-parser": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
- "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-validator-identifier": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz",
- "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-validator-option": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz",
- "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-wrap-function": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.5.tgz",
- "integrity": "sha512-bYqLIBSEshYcYQyfks8ewYA8S30yaGSeRslcvKMvoUk6HHPySbxHq9YRi6ghhzEU+yhQv9bP/jXnygkStOcqZw==",
- "dependencies": {
- "@babel/helper-function-name": "^7.22.5",
- "@babel/template": "^7.22.5",
- "@babel/traverse": "^7.22.5",
- "@babel/types": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helpers": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.5.tgz",
- "integrity": "sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q==",
- "dependencies": {
- "@babel/template": "^7.22.5",
- "@babel/traverse": "^7.22.5",
- "@babel/types": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/highlight": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz",
- "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==",
- "dependencies": {
- "@babel/helper-validator-identifier": "^7.22.5",
- "chalk": "^2.0.0",
- "js-tokens": "^4.0.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/highlight/node_modules/ansi-styles": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
- "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
- "dependencies": {
- "color-convert": "^1.9.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/@babel/highlight/node_modules/chalk": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
- "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
- "dependencies": {
- "ansi-styles": "^3.2.1",
- "escape-string-regexp": "^1.0.5",
- "supports-color": "^5.3.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/@babel/highlight/node_modules/color-convert": {
- "version": "1.9.3",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
- "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
- "dependencies": {
- "color-name": "1.1.3"
- }
- },
- "node_modules/@babel/highlight/node_modules/color-name": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
- "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
- },
- "node_modules/@babel/highlight/node_modules/escape-string-regexp": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
- "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
- "engines": {
- "node": ">=0.8.0"
- }
- },
- "node_modules/@babel/highlight/node_modules/has-flag": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
- "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/@babel/highlight/node_modules/supports-color": {
- "version": "5.5.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
- "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
- "dependencies": {
- "has-flag": "^3.0.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/@babel/parser": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz",
- "integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==",
- "bin": {
- "parser": "bin/babel-parser.js"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz",
- "integrity": "sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0"
- }
- },
- "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.5.tgz",
- "integrity": "sha512-31Bb65aZaUwqCbWMnZPduIZxCBngHFlzyN6Dq6KAJjtx+lx6ohKHubc61OomYi7XwVD4Ol0XCVz4h+pYFR048g==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5",
- "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5",
- "@babel/plugin-transform-optional-chaining": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.13.0"
- }
- },
- "node_modules/@babel/plugin-proposal-object-rest-spread": {
- "version": "7.12.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.12.1.tgz",
- "integrity": "sha512-s6SowJIjzlhx8o7lsFx5zmY4At6CTtDvgNQDdPzkBQucle58A6b/TTeEBYtyDgmcXjUTM+vE8YOGHZzzbc/ioA==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.10.4",
- "@babel/plugin-syntax-object-rest-spread": "^7.8.0",
- "@babel/plugin-transform-parameters": "^7.12.1"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-proposal-private-property-in-object": {
- "version": "7.21.0-placeholder-for-preset-env.2",
- "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
- "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==",
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-proposal-unicode-property-regex": {
- "version": "7.18.6",
- "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz",
- "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==",
- "dependencies": {
- "@babel/helper-create-regexp-features-plugin": "^7.18.6",
- "@babel/helper-plugin-utils": "^7.18.6"
- },
- "engines": {
- "node": ">=4"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-async-generators": {
- "version": "7.8.4",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz",
- "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.8.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-class-properties": {
- "version": "7.12.13",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz",
- "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.12.13"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-class-static-block": {
- "version": "7.14.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz",
- "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.14.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-dynamic-import": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz",
- "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.8.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-export-namespace-from": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz",
- "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.8.3"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-import-assertions": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz",
- "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-import-attributes": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz",
- "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-import-meta": {
- "version": "7.10.4",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz",
- "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.10.4"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-json-strings": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz",
- "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.8.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-jsx": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz",
- "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-logical-assignment-operators": {
- "version": "7.10.4",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz",
- "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.10.4"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz",
- "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.8.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-numeric-separator": {
- "version": "7.10.4",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz",
- "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.10.4"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-object-rest-spread": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz",
- "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.8.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-optional-catch-binding": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz",
- "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.8.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-optional-chaining": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz",
- "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.8.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-private-property-in-object": {
- "version": "7.14.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz",
- "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.14.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-top-level-await": {
- "version": "7.14.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz",
- "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.14.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-typescript": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz",
- "integrity": "sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-syntax-unicode-sets-regex": {
- "version": "7.18.6",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz",
- "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==",
- "dependencies": {
- "@babel/helper-create-regexp-features-plugin": "^7.18.6",
- "@babel/helper-plugin-utils": "^7.18.6"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0"
- }
- },
- "node_modules/@babel/plugin-transform-arrow-functions": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz",
- "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-async-generator-functions": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.5.tgz",
- "integrity": "sha512-gGOEvFzm3fWoyD5uZq7vVTD57pPJ3PczPUD/xCFGjzBpUosnklmXyKnGQbbbGs1NPNPskFex0j93yKbHt0cHyg==",
- "dependencies": {
- "@babel/helper-environment-visitor": "^7.22.5",
- "@babel/helper-plugin-utils": "^7.22.5",
- "@babel/helper-remap-async-to-generator": "^7.22.5",
- "@babel/plugin-syntax-async-generators": "^7.8.4"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-async-to-generator": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz",
- "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==",
- "dependencies": {
- "@babel/helper-module-imports": "^7.22.5",
- "@babel/helper-plugin-utils": "^7.22.5",
- "@babel/helper-remap-async-to-generator": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-block-scoped-functions": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz",
- "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-block-scoping": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.5.tgz",
- "integrity": "sha512-EcACl1i5fSQ6bt+YGuU/XGCeZKStLmyVGytWkpyhCLeQVA0eu6Wtiw92V+I1T/hnezUv7j74dA/Ro69gWcU+hg==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-class-properties": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz",
- "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==",
- "dependencies": {
- "@babel/helper-create-class-features-plugin": "^7.22.5",
- "@babel/helper-plugin-utils": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-class-static-block": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.5.tgz",
- "integrity": "sha512-SPToJ5eYZLxlnp1UzdARpOGeC2GbHvr9d/UV0EukuVx8atktg194oe+C5BqQ8jRTkgLRVOPYeXRSBg1IlMoVRA==",
- "dependencies": {
- "@babel/helper-create-class-features-plugin": "^7.22.5",
- "@babel/helper-plugin-utils": "^7.22.5",
- "@babel/plugin-syntax-class-static-block": "^7.14.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.12.0"
- }
- },
- "node_modules/@babel/plugin-transform-classes": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.5.tgz",
- "integrity": "sha512-2edQhLfibpWpsVBx2n/GKOz6JdGQvLruZQfGr9l1qes2KQaWswjBzhQF7UDUZMNaMMQeYnQzxwOMPsbYF7wqPQ==",
- "dependencies": {
- "@babel/helper-annotate-as-pure": "^7.22.5",
- "@babel/helper-compilation-targets": "^7.22.5",
- "@babel/helper-environment-visitor": "^7.22.5",
- "@babel/helper-function-name": "^7.22.5",
- "@babel/helper-optimise-call-expression": "^7.22.5",
- "@babel/helper-plugin-utils": "^7.22.5",
- "@babel/helper-replace-supers": "^7.22.5",
- "@babel/helper-split-export-declaration": "^7.22.5",
- "globals": "^11.1.0"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-computed-properties": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz",
- "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5",
- "@babel/template": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-destructuring": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.5.tgz",
- "integrity": "sha512-GfqcFuGW8vnEqTUBM7UtPd5A4q797LTvvwKxXTgRsFjoqaJiEg9deBG6kWeQYkVEL569NpnmpC0Pkr/8BLKGnQ==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-dotall-regex": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz",
- "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==",
- "dependencies": {
- "@babel/helper-create-regexp-features-plugin": "^7.22.5",
- "@babel/helper-plugin-utils": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-duplicate-keys": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz",
- "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-dynamic-import": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.5.tgz",
- "integrity": "sha512-0MC3ppTB1AMxd8fXjSrbPa7LT9hrImt+/fcj+Pg5YMD7UQyWp/02+JWpdnCymmsXwIx5Z+sYn1bwCn4ZJNvhqQ==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5",
- "@babel/plugin-syntax-dynamic-import": "^7.8.3"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-exponentiation-operator": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz",
- "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==",
- "dependencies": {
- "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5",
- "@babel/helper-plugin-utils": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-export-namespace-from": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.5.tgz",
- "integrity": "sha512-X4hhm7FRnPgd4nDA4b/5V280xCx6oL7Oob5+9qVS5C13Zq4bh1qq7LU0GgRU6b5dBWBvhGaXYVB4AcN6+ol6vg==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5",
- "@babel/plugin-syntax-export-namespace-from": "^7.8.3"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-for-of": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.5.tgz",
- "integrity": "sha512-3kxQjX1dU9uudwSshyLeEipvrLjBCVthCgeTp6CzE/9JYrlAIaeekVxRpCWsDDfYTfRZRoCeZatCQvwo+wvK8A==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-function-name": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz",
- "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==",
- "dependencies": {
- "@babel/helper-compilation-targets": "^7.22.5",
- "@babel/helper-function-name": "^7.22.5",
- "@babel/helper-plugin-utils": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-json-strings": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.5.tgz",
- "integrity": "sha512-DuCRB7fu8MyTLbEQd1ew3R85nx/88yMoqo2uPSjevMj3yoN7CDM8jkgrY0wmVxfJZyJ/B9fE1iq7EQppWQmR5A==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5",
- "@babel/plugin-syntax-json-strings": "^7.8.3"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-literals": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz",
- "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-logical-assignment-operators": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.5.tgz",
- "integrity": "sha512-MQQOUW1KL8X0cDWfbwYP+TbVbZm16QmQXJQ+vndPtH/BoO0lOKpVoEDMI7+PskYxH+IiE0tS8xZye0qr1lGzSA==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5",
- "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-member-expression-literals": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz",
- "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-modules-amd": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz",
- "integrity": "sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ==",
- "dependencies": {
- "@babel/helper-module-transforms": "^7.22.5",
- "@babel/helper-plugin-utils": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-modules-commonjs": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.5.tgz",
- "integrity": "sha512-B4pzOXj+ONRmuaQTg05b3y/4DuFz3WcCNAXPLb2Q0GT0TrGKGxNKV4jwsXts+StaM0LQczZbOpj8o1DLPDJIiA==",
- "dependencies": {
- "@babel/helper-module-transforms": "^7.22.5",
- "@babel/helper-plugin-utils": "^7.22.5",
- "@babel/helper-simple-access": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-modules-systemjs": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.5.tgz",
- "integrity": "sha512-emtEpoaTMsOs6Tzz+nbmcePl6AKVtS1yC4YNAeMun9U8YCsgadPNxnOPQ8GhHFB2qdx+LZu9LgoC0Lthuu05DQ==",
- "dependencies": {
- "@babel/helper-hoist-variables": "^7.22.5",
- "@babel/helper-module-transforms": "^7.22.5",
- "@babel/helper-plugin-utils": "^7.22.5",
- "@babel/helper-validator-identifier": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-modules-umd": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz",
- "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==",
- "dependencies": {
- "@babel/helper-module-transforms": "^7.22.5",
- "@babel/helper-plugin-utils": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-named-capturing-groups-regex": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz",
- "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==",
- "dependencies": {
- "@babel/helper-create-regexp-features-plugin": "^7.22.5",
- "@babel/helper-plugin-utils": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0"
- }
- },
- "node_modules/@babel/plugin-transform-new-target": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz",
- "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-nullish-coalescing-operator": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.5.tgz",
- "integrity": "sha512-6CF8g6z1dNYZ/VXok5uYkkBBICHZPiGEl7oDnAx2Mt1hlHVHOSIKWJaXHjQJA5VB43KZnXZDIexMchY4y2PGdA==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5",
- "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-numeric-separator": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.5.tgz",
- "integrity": "sha512-NbslED1/6M+sXiwwtcAB/nieypGw02Ejf4KtDeMkCEpP6gWFMX1wI9WKYua+4oBneCCEmulOkRpwywypVZzs/g==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5",
- "@babel/plugin-syntax-numeric-separator": "^7.10.4"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-object-rest-spread": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.5.tgz",
- "integrity": "sha512-Kk3lyDmEslH9DnvCDA1s1kkd3YWQITiBOHngOtDL9Pt6BZjzqb6hiOlb8VfjiiQJ2unmegBqZu0rx5RxJb5vmQ==",
- "dependencies": {
- "@babel/compat-data": "^7.22.5",
- "@babel/helper-compilation-targets": "^7.22.5",
- "@babel/helper-plugin-utils": "^7.22.5",
- "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
- "@babel/plugin-transform-parameters": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-object-super": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz",
- "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5",
- "@babel/helper-replace-supers": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-optional-catch-binding": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.5.tgz",
- "integrity": "sha512-pH8orJahy+hzZje5b8e2QIlBWQvGpelS76C63Z+jhZKsmzfNaPQ+LaW6dcJ9bxTpo1mtXbgHwy765Ro3jftmUg==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5",
- "@babel/plugin-syntax-optional-catch-binding": "^7.8.3"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-optional-chaining": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.5.tgz",
- "integrity": "sha512-AconbMKOMkyG+xCng2JogMCDcqW8wedQAqpVIL4cOSescZ7+iW8utC6YDZLMCSUIReEA733gzRSaOSXMAt/4WQ==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5",
- "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5",
- "@babel/plugin-syntax-optional-chaining": "^7.8.3"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-parameters": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.5.tgz",
- "integrity": "sha512-AVkFUBurORBREOmHRKo06FjHYgjrabpdqRSwq6+C7R5iTCZOsM4QbcB27St0a4U6fffyAOqh3s/qEfybAhfivg==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-private-methods": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz",
- "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==",
- "dependencies": {
- "@babel/helper-create-class-features-plugin": "^7.22.5",
- "@babel/helper-plugin-utils": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-private-property-in-object": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.5.tgz",
- "integrity": "sha512-/9xnaTTJcVoBtSSmrVyhtSvO3kbqS2ODoh2juEU72c3aYonNF0OMGiaz2gjukyKM2wBBYJP38S4JiE0Wfb5VMQ==",
- "dependencies": {
- "@babel/helper-annotate-as-pure": "^7.22.5",
- "@babel/helper-create-class-features-plugin": "^7.22.5",
- "@babel/helper-plugin-utils": "^7.22.5",
- "@babel/plugin-syntax-private-property-in-object": "^7.14.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-property-literals": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz",
- "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-react-constant-elements": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.22.5.tgz",
- "integrity": "sha512-BF5SXoO+nX3h5OhlN78XbbDrBOffv+AxPP2ENaJOVqjWCgBDeOY3WcaUcddutGSfoap+5NEQ/q/4I3WZIvgkXA==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-react-display-name": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.22.5.tgz",
- "integrity": "sha512-PVk3WPYudRF5z4GKMEYUrLjPl38fJSKNaEOkFuoprioowGuWN6w2RKznuFNSlJx7pzzXXStPUnNSOEO0jL5EVw==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-react-jsx": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.22.5.tgz",
- "integrity": "sha512-rog5gZaVbUip5iWDMTYbVM15XQq+RkUKhET/IHR6oizR+JEoN6CAfTTuHcK4vwUyzca30qqHqEpzBOnaRMWYMA==",
- "dependencies": {
- "@babel/helper-annotate-as-pure": "^7.22.5",
- "@babel/helper-module-imports": "^7.22.5",
- "@babel/helper-plugin-utils": "^7.22.5",
- "@babel/plugin-syntax-jsx": "^7.22.5",
- "@babel/types": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-react-jsx-development": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz",
- "integrity": "sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A==",
- "dependencies": {
- "@babel/plugin-transform-react-jsx": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-react-pure-annotations": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.22.5.tgz",
- "integrity": "sha512-gP4k85wx09q+brArVinTXhWiyzLl9UpmGva0+mWyKxk6JZequ05x3eUcIUE+FyttPKJFRRVtAvQaJ6YF9h1ZpA==",
- "dependencies": {
- "@babel/helper-annotate-as-pure": "^7.22.5",
- "@babel/helper-plugin-utils": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-regenerator": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.5.tgz",
- "integrity": "sha512-rR7KePOE7gfEtNTh9Qw+iO3Q/e4DEsoQ+hdvM6QUDH7JRJ5qxq5AA52ZzBWbI5i9lfNuvySgOGP8ZN7LAmaiPw==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5",
- "regenerator-transform": "^0.15.1"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-reserved-words": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz",
- "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-runtime": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.22.5.tgz",
- "integrity": "sha512-bg4Wxd1FWeFx3daHFTWk1pkSWK/AyQuiyAoeZAOkAOUBjnZPH6KT7eMxouV47tQ6hl6ax2zyAWBdWZXbrvXlaw==",
- "dependencies": {
- "@babel/helper-module-imports": "^7.22.5",
- "@babel/helper-plugin-utils": "^7.22.5",
- "babel-plugin-polyfill-corejs2": "^0.4.3",
- "babel-plugin-polyfill-corejs3": "^0.8.1",
- "babel-plugin-polyfill-regenerator": "^0.5.0",
- "semver": "^6.3.0"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-runtime/node_modules/semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
- "node_modules/@babel/plugin-transform-shorthand-properties": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz",
- "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-spread": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz",
- "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5",
- "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-sticky-regex": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz",
- "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-template-literals": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz",
- "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-typeof-symbol": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz",
- "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-typescript": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.22.5.tgz",
- "integrity": "sha512-SMubA9S7Cb5sGSFFUlqxyClTA9zWJ8qGQrppNUm05LtFuN1ELRFNndkix4zUJrC9F+YivWwa1dHMSyo0e0N9dA==",
- "dependencies": {
- "@babel/helper-annotate-as-pure": "^7.22.5",
- "@babel/helper-create-class-features-plugin": "^7.22.5",
- "@babel/helper-plugin-utils": "^7.22.5",
- "@babel/plugin-syntax-typescript": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-unicode-escapes": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.5.tgz",
- "integrity": "sha512-biEmVg1IYB/raUO5wT1tgfacCef15Fbzhkx493D3urBI++6hpJ+RFG4SrWMn0NEZLfvilqKf3QDrRVZHo08FYg==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-unicode-property-regex": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz",
- "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==",
- "dependencies": {
- "@babel/helper-create-regexp-features-plugin": "^7.22.5",
- "@babel/helper-plugin-utils": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-unicode-regex": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz",
- "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==",
- "dependencies": {
- "@babel/helper-create-regexp-features-plugin": "^7.22.5",
- "@babel/helper-plugin-utils": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-unicode-sets-regex": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz",
- "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==",
- "dependencies": {
- "@babel/helper-create-regexp-features-plugin": "^7.22.5",
- "@babel/helper-plugin-utils": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0"
- }
- },
- "node_modules/@babel/preset-env": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.5.tgz",
- "integrity": "sha512-fj06hw89dpiZzGZtxn+QybifF07nNiZjZ7sazs2aVDcysAZVGjW7+7iFYxg6GLNM47R/thYfLdrXc+2f11Vi9A==",
- "dependencies": {
- "@babel/compat-data": "^7.22.5",
- "@babel/helper-compilation-targets": "^7.22.5",
- "@babel/helper-plugin-utils": "^7.22.5",
- "@babel/helper-validator-option": "^7.22.5",
- "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.5",
- "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.5",
- "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2",
- "@babel/plugin-syntax-async-generators": "^7.8.4",
- "@babel/plugin-syntax-class-properties": "^7.12.13",
- "@babel/plugin-syntax-class-static-block": "^7.14.5",
- "@babel/plugin-syntax-dynamic-import": "^7.8.3",
- "@babel/plugin-syntax-export-namespace-from": "^7.8.3",
- "@babel/plugin-syntax-import-assertions": "^7.22.5",
- "@babel/plugin-syntax-import-attributes": "^7.22.5",
- "@babel/plugin-syntax-import-meta": "^7.10.4",
- "@babel/plugin-syntax-json-strings": "^7.8.3",
- "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4",
- "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3",
- "@babel/plugin-syntax-numeric-separator": "^7.10.4",
- "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
- "@babel/plugin-syntax-optional-catch-binding": "^7.8.3",
- "@babel/plugin-syntax-optional-chaining": "^7.8.3",
- "@babel/plugin-syntax-private-property-in-object": "^7.14.5",
- "@babel/plugin-syntax-top-level-await": "^7.14.5",
- "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6",
- "@babel/plugin-transform-arrow-functions": "^7.22.5",
- "@babel/plugin-transform-async-generator-functions": "^7.22.5",
- "@babel/plugin-transform-async-to-generator": "^7.22.5",
- "@babel/plugin-transform-block-scoped-functions": "^7.22.5",
- "@babel/plugin-transform-block-scoping": "^7.22.5",
- "@babel/plugin-transform-class-properties": "^7.22.5",
- "@babel/plugin-transform-class-static-block": "^7.22.5",
- "@babel/plugin-transform-classes": "^7.22.5",
- "@babel/plugin-transform-computed-properties": "^7.22.5",
- "@babel/plugin-transform-destructuring": "^7.22.5",
- "@babel/plugin-transform-dotall-regex": "^7.22.5",
- "@babel/plugin-transform-duplicate-keys": "^7.22.5",
- "@babel/plugin-transform-dynamic-import": "^7.22.5",
- "@babel/plugin-transform-exponentiation-operator": "^7.22.5",
- "@babel/plugin-transform-export-namespace-from": "^7.22.5",
- "@babel/plugin-transform-for-of": "^7.22.5",
- "@babel/plugin-transform-function-name": "^7.22.5",
- "@babel/plugin-transform-json-strings": "^7.22.5",
- "@babel/plugin-transform-literals": "^7.22.5",
- "@babel/plugin-transform-logical-assignment-operators": "^7.22.5",
- "@babel/plugin-transform-member-expression-literals": "^7.22.5",
- "@babel/plugin-transform-modules-amd": "^7.22.5",
- "@babel/plugin-transform-modules-commonjs": "^7.22.5",
- "@babel/plugin-transform-modules-systemjs": "^7.22.5",
- "@babel/plugin-transform-modules-umd": "^7.22.5",
- "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5",
- "@babel/plugin-transform-new-target": "^7.22.5",
- "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.5",
- "@babel/plugin-transform-numeric-separator": "^7.22.5",
- "@babel/plugin-transform-object-rest-spread": "^7.22.5",
- "@babel/plugin-transform-object-super": "^7.22.5",
- "@babel/plugin-transform-optional-catch-binding": "^7.22.5",
- "@babel/plugin-transform-optional-chaining": "^7.22.5",
- "@babel/plugin-transform-parameters": "^7.22.5",
- "@babel/plugin-transform-private-methods": "^7.22.5",
- "@babel/plugin-transform-private-property-in-object": "^7.22.5",
- "@babel/plugin-transform-property-literals": "^7.22.5",
- "@babel/plugin-transform-regenerator": "^7.22.5",
- "@babel/plugin-transform-reserved-words": "^7.22.5",
- "@babel/plugin-transform-shorthand-properties": "^7.22.5",
- "@babel/plugin-transform-spread": "^7.22.5",
- "@babel/plugin-transform-sticky-regex": "^7.22.5",
- "@babel/plugin-transform-template-literals": "^7.22.5",
- "@babel/plugin-transform-typeof-symbol": "^7.22.5",
- "@babel/plugin-transform-unicode-escapes": "^7.22.5",
- "@babel/plugin-transform-unicode-property-regex": "^7.22.5",
- "@babel/plugin-transform-unicode-regex": "^7.22.5",
- "@babel/plugin-transform-unicode-sets-regex": "^7.22.5",
- "@babel/preset-modules": "^0.1.5",
- "@babel/types": "^7.22.5",
- "babel-plugin-polyfill-corejs2": "^0.4.3",
- "babel-plugin-polyfill-corejs3": "^0.8.1",
- "babel-plugin-polyfill-regenerator": "^0.5.0",
- "core-js-compat": "^3.30.2",
- "semver": "^6.3.0"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/preset-env/node_modules/semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
- "node_modules/@babel/preset-modules": {
- "version": "0.1.5",
- "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz",
- "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.0.0",
- "@babel/plugin-proposal-unicode-property-regex": "^7.4.4",
- "@babel/plugin-transform-dotall-regex": "^7.4.4",
- "@babel/types": "^7.4.4",
- "esutils": "^2.0.2"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/preset-react": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.22.5.tgz",
- "integrity": "sha512-M+Is3WikOpEJHgR385HbuCITPTaPRaNkibTEa9oiofmJvIsrceb4yp9RL9Kb+TE8LznmeyZqpP+Lopwcx59xPQ==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5",
- "@babel/helper-validator-option": "^7.22.5",
- "@babel/plugin-transform-react-display-name": "^7.22.5",
- "@babel/plugin-transform-react-jsx": "^7.22.5",
- "@babel/plugin-transform-react-jsx-development": "^7.22.5",
- "@babel/plugin-transform-react-pure-annotations": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/preset-typescript": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.22.5.tgz",
- "integrity": "sha512-YbPaal9LxztSGhmndR46FmAbkJ/1fAsw293tSU+I5E5h+cnJ3d4GTwyUgGYmOXJYdGA+uNePle4qbaRzj2NISQ==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5",
- "@babel/helper-validator-option": "^7.22.5",
- "@babel/plugin-syntax-jsx": "^7.22.5",
- "@babel/plugin-transform-modules-commonjs": "^7.22.5",
- "@babel/plugin-transform-typescript": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/regjsgen": {
- "version": "0.8.0",
- "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz",
- "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA=="
- },
- "node_modules/@babel/runtime": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz",
- "integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==",
- "dependencies": {
- "regenerator-runtime": "^0.13.11"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/runtime-corejs3": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.22.5.tgz",
- "integrity": "sha512-TNPDN6aBFaUox2Lu+H/Y1dKKQgr4ucz/FGyCz67RVYLsBpVpUFf1dDngzg+Od8aqbrqwyztkaZjtWCZEUOT8zA==",
- "dependencies": {
- "core-js-pure": "^3.30.2",
- "regenerator-runtime": "^0.13.11"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/template": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz",
- "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==",
- "dependencies": {
- "@babel/code-frame": "^7.22.5",
- "@babel/parser": "^7.22.5",
- "@babel/types": "^7.22.5"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/traverse": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.5.tgz",
- "integrity": "sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==",
- "dependencies": {
- "@babel/code-frame": "^7.22.5",
- "@babel/generator": "^7.22.5",
- "@babel/helper-environment-visitor": "^7.22.5",
- "@babel/helper-function-name": "^7.22.5",
- "@babel/helper-hoist-variables": "^7.22.5",
- "@babel/helper-split-export-declaration": "^7.22.5",
- "@babel/parser": "^7.22.5",
- "@babel/types": "^7.22.5",
- "debug": "^4.1.0",
- "globals": "^11.1.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/types": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz",
- "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==",
- "dependencies": {
- "@babel/helper-string-parser": "^7.22.5",
- "@babel/helper-validator-identifier": "^7.22.5",
- "to-fast-properties": "^2.0.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@colors/colors": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
- "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==",
- "optional": true,
- "engines": {
- "node": ">=0.1.90"
- }
- },
- "node_modules/@discoveryjs/json-ext": {
- "version": "0.5.7",
- "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz",
- "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==",
- "engines": {
- "node": ">=10.0.0"
- }
- },
- "node_modules/@docsearch/css": {
- "version": "3.5.1",
- "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.5.1.tgz",
- "integrity": "sha512-2Pu9HDg/uP/IT10rbQ+4OrTQuxIWdKVUEdcw9/w7kZJv9NeHS6skJx1xuRiFyoGKwAzcHXnLp7csE99sj+O1YA=="
- },
- "node_modules/@docsearch/react": {
- "version": "3.5.1",
- "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.5.1.tgz",
- "integrity": "sha512-t5mEODdLzZq4PTFAm/dvqcvZFdPDMdfPE5rJS5SC8OUq9mPzxEy6b+9THIqNM9P0ocCb4UC5jqBrxKclnuIbzQ==",
- "dependencies": {
- "@algolia/autocomplete-core": "1.9.3",
- "@algolia/autocomplete-preset-algolia": "1.9.3",
- "@docsearch/css": "3.5.1",
- "algoliasearch": "^4.0.0"
- },
- "peerDependencies": {
- "@types/react": ">= 16.8.0 < 19.0.0",
- "react": ">= 16.8.0 < 19.0.0",
- "react-dom": ">= 16.8.0 < 19.0.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "react": {
- "optional": true
- },
- "react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@docusaurus/core": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-2.4.1.tgz",
- "integrity": "sha512-SNsY7PshK3Ri7vtsLXVeAJGS50nJN3RgF836zkyUfAD01Fq+sAk5EwWgLw+nnm5KVNGDu7PRR2kRGDsWvqpo0g==",
- "dependencies": {
- "@babel/core": "^7.18.6",
- "@babel/generator": "^7.18.7",
- "@babel/plugin-syntax-dynamic-import": "^7.8.3",
- "@babel/plugin-transform-runtime": "^7.18.6",
- "@babel/preset-env": "^7.18.6",
- "@babel/preset-react": "^7.18.6",
- "@babel/preset-typescript": "^7.18.6",
- "@babel/runtime": "^7.18.6",
- "@babel/runtime-corejs3": "^7.18.6",
- "@babel/traverse": "^7.18.8",
- "@docusaurus/cssnano-preset": "2.4.1",
- "@docusaurus/logger": "2.4.1",
- "@docusaurus/mdx-loader": "2.4.1",
- "@docusaurus/react-loadable": "5.5.2",
- "@docusaurus/utils": "2.4.1",
- "@docusaurus/utils-common": "2.4.1",
- "@docusaurus/utils-validation": "2.4.1",
- "@slorber/static-site-generator-webpack-plugin": "^4.0.7",
- "@svgr/webpack": "^6.2.1",
- "autoprefixer": "^10.4.7",
- "babel-loader": "^8.2.5",
- "babel-plugin-dynamic-import-node": "^2.3.3",
- "boxen": "^6.2.1",
- "chalk": "^4.1.2",
- "chokidar": "^3.5.3",
- "clean-css": "^5.3.0",
- "cli-table3": "^0.6.2",
- "combine-promises": "^1.1.0",
- "commander": "^5.1.0",
- "copy-webpack-plugin": "^11.0.0",
- "core-js": "^3.23.3",
- "css-loader": "^6.7.1",
- "css-minimizer-webpack-plugin": "^4.0.0",
- "cssnano": "^5.1.12",
- "del": "^6.1.1",
- "detect-port": "^1.3.0",
- "escape-html": "^1.0.3",
- "eta": "^2.0.0",
- "file-loader": "^6.2.0",
- "fs-extra": "^10.1.0",
- "html-minifier-terser": "^6.1.0",
- "html-tags": "^3.2.0",
- "html-webpack-plugin": "^5.5.0",
- "import-fresh": "^3.3.0",
- "leven": "^3.1.0",
- "lodash": "^4.17.21",
- "mini-css-extract-plugin": "^2.6.1",
- "postcss": "^8.4.14",
- "postcss-loader": "^7.0.0",
- "prompts": "^2.4.2",
- "react-dev-utils": "^12.0.1",
- "react-helmet-async": "^1.3.0",
- "react-loadable": "npm:@docusaurus/react-loadable@5.5.2",
- "react-loadable-ssr-addon-v5-slorber": "^1.0.1",
- "react-router": "^5.3.3",
- "react-router-config": "^5.1.1",
- "react-router-dom": "^5.3.3",
- "rtl-detect": "^1.0.4",
- "semver": "^7.3.7",
- "serve-handler": "^6.1.3",
- "shelljs": "^0.8.5",
- "terser-webpack-plugin": "^5.3.3",
- "tslib": "^2.4.0",
- "update-notifier": "^5.1.0",
- "url-loader": "^4.1.1",
- "wait-on": "^6.0.1",
- "webpack": "^5.73.0",
- "webpack-bundle-analyzer": "^4.5.0",
- "webpack-dev-server": "^4.9.3",
- "webpack-merge": "^5.8.0",
- "webpackbar": "^5.0.2"
- },
- "bin": {
- "docusaurus": "bin/docusaurus.mjs"
- },
- "engines": {
- "node": ">=16.14"
- },
- "peerDependencies": {
- "react": "^16.8.4 || ^17.0.0",
- "react-dom": "^16.8.4 || ^17.0.0"
- }
- },
- "node_modules/@docusaurus/cssnano-preset": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-2.4.1.tgz",
- "integrity": "sha512-ka+vqXwtcW1NbXxWsh6yA1Ckii1klY9E53cJ4O9J09nkMBgrNX3iEFED1fWdv8wf4mJjvGi5RLZ2p9hJNjsLyQ==",
- "dependencies": {
- "cssnano-preset-advanced": "^5.3.8",
- "postcss": "^8.4.14",
- "postcss-sort-media-queries": "^4.2.1",
- "tslib": "^2.4.0"
- },
- "engines": {
- "node": ">=16.14"
- }
- },
- "node_modules/@docusaurus/logger": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-2.4.1.tgz",
- "integrity": "sha512-5h5ysIIWYIDHyTVd8BjheZmQZmEgWDR54aQ1BX9pjFfpyzFo5puKXKYrYJXbjEHGyVhEzmB9UXwbxGfaZhOjcg==",
- "dependencies": {
- "chalk": "^4.1.2",
- "tslib": "^2.4.0"
- },
- "engines": {
- "node": ">=16.14"
- }
- },
- "node_modules/@docusaurus/mdx-loader": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-2.4.1.tgz",
- "integrity": "sha512-4KhUhEavteIAmbBj7LVFnrVYDiU51H5YWW1zY6SmBSte/YLhDutztLTBE0PQl1Grux1jzUJeaSvAzHpTn6JJDQ==",
- "dependencies": {
- "@babel/parser": "^7.18.8",
- "@babel/traverse": "^7.18.8",
- "@docusaurus/logger": "2.4.1",
- "@docusaurus/utils": "2.4.1",
- "@mdx-js/mdx": "^1.6.22",
- "escape-html": "^1.0.3",
- "file-loader": "^6.2.0",
- "fs-extra": "^10.1.0",
- "image-size": "^1.0.1",
- "mdast-util-to-string": "^2.0.0",
- "remark-emoji": "^2.2.0",
- "stringify-object": "^3.3.0",
- "tslib": "^2.4.0",
- "unified": "^9.2.2",
- "unist-util-visit": "^2.0.3",
- "url-loader": "^4.1.1",
- "webpack": "^5.73.0"
- },
- "engines": {
- "node": ">=16.14"
- },
- "peerDependencies": {
- "react": "^16.8.4 || ^17.0.0",
- "react-dom": "^16.8.4 || ^17.0.0"
- }
- },
- "node_modules/@docusaurus/module-type-aliases": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-2.4.1.tgz",
- "integrity": "sha512-gLBuIFM8Dp2XOCWffUDSjtxY7jQgKvYujt7Mx5s4FCTfoL5dN1EVbnrn+O2Wvh8b0a77D57qoIDY7ghgmatR1A==",
- "dependencies": {
- "@docusaurus/react-loadable": "5.5.2",
- "@docusaurus/types": "2.4.1",
- "@types/history": "^4.7.11",
- "@types/react": "*",
- "@types/react-router-config": "*",
- "@types/react-router-dom": "*",
- "react-helmet-async": "*",
- "react-loadable": "npm:@docusaurus/react-loadable@5.5.2"
- },
- "peerDependencies": {
- "react": "*",
- "react-dom": "*"
- }
- },
- "node_modules/@docusaurus/plugin-content-blog": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-2.4.1.tgz",
- "integrity": "sha512-E2i7Knz5YIbE1XELI6RlTnZnGgS52cUO4BlCiCUCvQHbR+s1xeIWz4C6BtaVnlug0Ccz7nFSksfwDpVlkujg5Q==",
- "dependencies": {
- "@docusaurus/core": "2.4.1",
- "@docusaurus/logger": "2.4.1",
- "@docusaurus/mdx-loader": "2.4.1",
- "@docusaurus/types": "2.4.1",
- "@docusaurus/utils": "2.4.1",
- "@docusaurus/utils-common": "2.4.1",
- "@docusaurus/utils-validation": "2.4.1",
- "cheerio": "^1.0.0-rc.12",
- "feed": "^4.2.2",
- "fs-extra": "^10.1.0",
- "lodash": "^4.17.21",
- "reading-time": "^1.5.0",
- "tslib": "^2.4.0",
- "unist-util-visit": "^2.0.3",
- "utility-types": "^3.10.0",
- "webpack": "^5.73.0"
- },
- "engines": {
- "node": ">=16.14"
- },
- "peerDependencies": {
- "react": "^16.8.4 || ^17.0.0",
- "react-dom": "^16.8.4 || ^17.0.0"
- }
- },
- "node_modules/@docusaurus/plugin-content-docs": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-2.4.1.tgz",
- "integrity": "sha512-Lo7lSIcpswa2Kv4HEeUcGYqaasMUQNpjTXpV0N8G6jXgZaQurqp7E8NGYeGbDXnb48czmHWbzDL4S3+BbK0VzA==",
- "dependencies": {
- "@docusaurus/core": "2.4.1",
- "@docusaurus/logger": "2.4.1",
- "@docusaurus/mdx-loader": "2.4.1",
- "@docusaurus/module-type-aliases": "2.4.1",
- "@docusaurus/types": "2.4.1",
- "@docusaurus/utils": "2.4.1",
- "@docusaurus/utils-validation": "2.4.1",
- "@types/react-router-config": "^5.0.6",
- "combine-promises": "^1.1.0",
- "fs-extra": "^10.1.0",
- "import-fresh": "^3.3.0",
- "js-yaml": "^4.1.0",
- "lodash": "^4.17.21",
- "tslib": "^2.4.0",
- "utility-types": "^3.10.0",
- "webpack": "^5.73.0"
- },
- "engines": {
- "node": ">=16.14"
- },
- "peerDependencies": {
- "react": "^16.8.4 || ^17.0.0",
- "react-dom": "^16.8.4 || ^17.0.0"
- }
- },
- "node_modules/@docusaurus/plugin-content-pages": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-2.4.1.tgz",
- "integrity": "sha512-/UjuH/76KLaUlL+o1OvyORynv6FURzjurSjvn2lbWTFc4tpYY2qLYTlKpTCBVPhlLUQsfyFnshEJDLmPneq2oA==",
- "dependencies": {
- "@docusaurus/core": "2.4.1",
- "@docusaurus/mdx-loader": "2.4.1",
- "@docusaurus/types": "2.4.1",
- "@docusaurus/utils": "2.4.1",
- "@docusaurus/utils-validation": "2.4.1",
- "fs-extra": "^10.1.0",
- "tslib": "^2.4.0",
- "webpack": "^5.73.0"
- },
- "engines": {
- "node": ">=16.14"
- },
- "peerDependencies": {
- "react": "^16.8.4 || ^17.0.0",
- "react-dom": "^16.8.4 || ^17.0.0"
- }
- },
- "node_modules/@docusaurus/plugin-debug": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-2.4.1.tgz",
- "integrity": "sha512-7Yu9UPzRShlrH/G8btOpR0e6INFZr0EegWplMjOqelIwAcx3PKyR8mgPTxGTxcqiYj6hxSCRN0D8R7YrzImwNA==",
- "dependencies": {
- "@docusaurus/core": "2.4.1",
- "@docusaurus/types": "2.4.1",
- "@docusaurus/utils": "2.4.1",
- "fs-extra": "^10.1.0",
- "react-json-view": "^1.21.3",
- "tslib": "^2.4.0"
- },
- "engines": {
- "node": ">=16.14"
- },
- "peerDependencies": {
- "react": "^16.8.4 || ^17.0.0",
- "react-dom": "^16.8.4 || ^17.0.0"
- }
- },
- "node_modules/@docusaurus/plugin-google-analytics": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-2.4.1.tgz",
- "integrity": "sha512-dyZJdJiCoL+rcfnm0RPkLt/o732HvLiEwmtoNzOoz9MSZz117UH2J6U2vUDtzUzwtFLIf32KkeyzisbwUCgcaQ==",
- "dependencies": {
- "@docusaurus/core": "2.4.1",
- "@docusaurus/types": "2.4.1",
- "@docusaurus/utils-validation": "2.4.1",
- "tslib": "^2.4.0"
- },
- "engines": {
- "node": ">=16.14"
- },
- "peerDependencies": {
- "react": "^16.8.4 || ^17.0.0",
- "react-dom": "^16.8.4 || ^17.0.0"
- }
- },
- "node_modules/@docusaurus/plugin-google-gtag": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-2.4.1.tgz",
- "integrity": "sha512-mKIefK+2kGTQBYvloNEKtDmnRD7bxHLsBcxgnbt4oZwzi2nxCGjPX6+9SQO2KCN5HZbNrYmGo5GJfMgoRvy6uA==",
- "dependencies": {
- "@docusaurus/core": "2.4.1",
- "@docusaurus/types": "2.4.1",
- "@docusaurus/utils-validation": "2.4.1",
- "tslib": "^2.4.0"
- },
- "engines": {
- "node": ">=16.14"
- },
- "peerDependencies": {
- "react": "^16.8.4 || ^17.0.0",
- "react-dom": "^16.8.4 || ^17.0.0"
- }
- },
- "node_modules/@docusaurus/plugin-google-tag-manager": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-2.4.1.tgz",
- "integrity": "sha512-Zg4Ii9CMOLfpeV2nG74lVTWNtisFaH9QNtEw48R5QE1KIwDBdTVaiSA18G1EujZjrzJJzXN79VhINSbOJO/r3g==",
- "dependencies": {
- "@docusaurus/core": "2.4.1",
- "@docusaurus/types": "2.4.1",
- "@docusaurus/utils-validation": "2.4.1",
- "tslib": "^2.4.0"
- },
- "engines": {
- "node": ">=16.14"
- },
- "peerDependencies": {
- "react": "^16.8.4 || ^17.0.0",
- "react-dom": "^16.8.4 || ^17.0.0"
- }
- },
- "node_modules/@docusaurus/plugin-sitemap": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-2.4.1.tgz",
- "integrity": "sha512-lZx+ijt/+atQ3FVE8FOHV/+X3kuok688OydDXrqKRJyXBJZKgGjA2Qa8RjQ4f27V2woaXhtnyrdPop/+OjVMRg==",
- "dependencies": {
- "@docusaurus/core": "2.4.1",
- "@docusaurus/logger": "2.4.1",
- "@docusaurus/types": "2.4.1",
- "@docusaurus/utils": "2.4.1",
- "@docusaurus/utils-common": "2.4.1",
- "@docusaurus/utils-validation": "2.4.1",
- "fs-extra": "^10.1.0",
- "sitemap": "^7.1.1",
- "tslib": "^2.4.0"
- },
- "engines": {
- "node": ">=16.14"
- },
- "peerDependencies": {
- "react": "^16.8.4 || ^17.0.0",
- "react-dom": "^16.8.4 || ^17.0.0"
- }
- },
- "node_modules/@docusaurus/preset-classic": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-2.4.1.tgz",
- "integrity": "sha512-P4//+I4zDqQJ+UDgoFrjIFaQ1MeS9UD1cvxVQaI6O7iBmiHQm0MGROP1TbE7HlxlDPXFJjZUK3x3cAoK63smGQ==",
- "dependencies": {
- "@docusaurus/core": "2.4.1",
- "@docusaurus/plugin-content-blog": "2.4.1",
- "@docusaurus/plugin-content-docs": "2.4.1",
- "@docusaurus/plugin-content-pages": "2.4.1",
- "@docusaurus/plugin-debug": "2.4.1",
- "@docusaurus/plugin-google-analytics": "2.4.1",
- "@docusaurus/plugin-google-gtag": "2.4.1",
- "@docusaurus/plugin-google-tag-manager": "2.4.1",
- "@docusaurus/plugin-sitemap": "2.4.1",
- "@docusaurus/theme-classic": "2.4.1",
- "@docusaurus/theme-common": "2.4.1",
- "@docusaurus/theme-search-algolia": "2.4.1",
- "@docusaurus/types": "2.4.1"
- },
- "engines": {
- "node": ">=16.14"
- },
- "peerDependencies": {
- "react": "^16.8.4 || ^17.0.0",
- "react-dom": "^16.8.4 || ^17.0.0"
- }
- },
- "node_modules/@docusaurus/react-loadable": {
- "version": "5.5.2",
- "resolved": "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-5.5.2.tgz",
- "integrity": "sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ==",
- "dependencies": {
- "@types/react": "*",
- "prop-types": "^15.6.2"
- },
- "peerDependencies": {
- "react": "*"
- }
- },
- "node_modules/@docusaurus/theme-classic": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-2.4.1.tgz",
- "integrity": "sha512-Rz0wKUa+LTW1PLXmwnf8mn85EBzaGSt6qamqtmnh9Hflkc+EqiYMhtUJeLdV+wsgYq4aG0ANc+bpUDpsUhdnwg==",
- "dependencies": {
- "@docusaurus/core": "2.4.1",
- "@docusaurus/mdx-loader": "2.4.1",
- "@docusaurus/module-type-aliases": "2.4.1",
- "@docusaurus/plugin-content-blog": "2.4.1",
- "@docusaurus/plugin-content-docs": "2.4.1",
- "@docusaurus/plugin-content-pages": "2.4.1",
- "@docusaurus/theme-common": "2.4.1",
- "@docusaurus/theme-translations": "2.4.1",
- "@docusaurus/types": "2.4.1",
- "@docusaurus/utils": "2.4.1",
- "@docusaurus/utils-common": "2.4.1",
- "@docusaurus/utils-validation": "2.4.1",
- "@mdx-js/react": "^1.6.22",
- "clsx": "^1.2.1",
- "copy-text-to-clipboard": "^3.0.1",
- "infima": "0.2.0-alpha.43",
- "lodash": "^4.17.21",
- "nprogress": "^0.2.0",
- "postcss": "^8.4.14",
- "prism-react-renderer": "^1.3.5",
- "prismjs": "^1.28.0",
- "react-router-dom": "^5.3.3",
- "rtlcss": "^3.5.0",
- "tslib": "^2.4.0",
- "utility-types": "^3.10.0"
- },
- "engines": {
- "node": ">=16.14"
- },
- "peerDependencies": {
- "react": "^16.8.4 || ^17.0.0",
- "react-dom": "^16.8.4 || ^17.0.0"
- }
- },
- "node_modules/@docusaurus/theme-common": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-2.4.1.tgz",
- "integrity": "sha512-G7Zau1W5rQTaFFB3x3soQoZpkgMbl/SYNG8PfMFIjKa3M3q8n0m/GRf5/H/e5BqOvt8c+ZWIXGCiz+kUCSHovA==",
- "dependencies": {
- "@docusaurus/mdx-loader": "2.4.1",
- "@docusaurus/module-type-aliases": "2.4.1",
- "@docusaurus/plugin-content-blog": "2.4.1",
- "@docusaurus/plugin-content-docs": "2.4.1",
- "@docusaurus/plugin-content-pages": "2.4.1",
- "@docusaurus/utils": "2.4.1",
- "@docusaurus/utils-common": "2.4.1",
- "@types/history": "^4.7.11",
- "@types/react": "*",
- "@types/react-router-config": "*",
- "clsx": "^1.2.1",
- "parse-numeric-range": "^1.3.0",
- "prism-react-renderer": "^1.3.5",
- "tslib": "^2.4.0",
- "use-sync-external-store": "^1.2.0",
- "utility-types": "^3.10.0"
- },
- "engines": {
- "node": ">=16.14"
- },
- "peerDependencies": {
- "react": "^16.8.4 || ^17.0.0",
- "react-dom": "^16.8.4 || ^17.0.0"
- }
- },
- "node_modules/@docusaurus/theme-search-algolia": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-2.4.1.tgz",
- "integrity": "sha512-6BcqW2lnLhZCXuMAvPRezFs1DpmEKzXFKlYjruuas+Xy3AQeFzDJKTJFIm49N77WFCTyxff8d3E4Q9pi/+5McQ==",
- "dependencies": {
- "@docsearch/react": "^3.1.1",
- "@docusaurus/core": "2.4.1",
- "@docusaurus/logger": "2.4.1",
- "@docusaurus/plugin-content-docs": "2.4.1",
- "@docusaurus/theme-common": "2.4.1",
- "@docusaurus/theme-translations": "2.4.1",
- "@docusaurus/utils": "2.4.1",
- "@docusaurus/utils-validation": "2.4.1",
- "algoliasearch": "^4.13.1",
- "algoliasearch-helper": "^3.10.0",
- "clsx": "^1.2.1",
- "eta": "^2.0.0",
- "fs-extra": "^10.1.0",
- "lodash": "^4.17.21",
- "tslib": "^2.4.0",
- "utility-types": "^3.10.0"
- },
- "engines": {
- "node": ">=16.14"
- },
- "peerDependencies": {
- "react": "^16.8.4 || ^17.0.0",
- "react-dom": "^16.8.4 || ^17.0.0"
- }
- },
- "node_modules/@docusaurus/theme-translations": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-2.4.1.tgz",
- "integrity": "sha512-T1RAGP+f86CA1kfE8ejZ3T3pUU3XcyvrGMfC/zxCtc2BsnoexuNI9Vk2CmuKCb+Tacvhxjv5unhxXce0+NKyvA==",
- "dependencies": {
- "fs-extra": "^10.1.0",
- "tslib": "^2.4.0"
- },
- "engines": {
- "node": ">=16.14"
- }
- },
- "node_modules/@docusaurus/types": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-2.4.1.tgz",
- "integrity": "sha512-0R+cbhpMkhbRXX138UOc/2XZFF8hiZa6ooZAEEJFp5scytzCw4tC1gChMFXrpa3d2tYE6AX8IrOEpSonLmfQuQ==",
- "dependencies": {
- "@types/history": "^4.7.11",
- "@types/react": "*",
- "commander": "^5.1.0",
- "joi": "^17.6.0",
- "react-helmet-async": "^1.3.0",
- "utility-types": "^3.10.0",
- "webpack": "^5.73.0",
- "webpack-merge": "^5.8.0"
- },
- "peerDependencies": {
- "react": "^16.8.4 || ^17.0.0",
- "react-dom": "^16.8.4 || ^17.0.0"
- }
- },
- "node_modules/@docusaurus/utils": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-2.4.1.tgz",
- "integrity": "sha512-1lvEZdAQhKNht9aPXPoh69eeKnV0/62ROhQeFKKxmzd0zkcuE/Oc5Gpnt00y/f5bIsmOsYMY7Pqfm/5rteT5GA==",
- "dependencies": {
- "@docusaurus/logger": "2.4.1",
- "@svgr/webpack": "^6.2.1",
- "escape-string-regexp": "^4.0.0",
- "file-loader": "^6.2.0",
- "fs-extra": "^10.1.0",
- "github-slugger": "^1.4.0",
- "globby": "^11.1.0",
- "gray-matter": "^4.0.3",
- "js-yaml": "^4.1.0",
- "lodash": "^4.17.21",
- "micromatch": "^4.0.5",
- "resolve-pathname": "^3.0.0",
- "shelljs": "^0.8.5",
- "tslib": "^2.4.0",
- "url-loader": "^4.1.1",
- "webpack": "^5.73.0"
- },
- "engines": {
- "node": ">=16.14"
- },
- "peerDependencies": {
- "@docusaurus/types": "*"
- },
- "peerDependenciesMeta": {
- "@docusaurus/types": {
- "optional": true
- }
- }
- },
- "node_modules/@docusaurus/utils-common": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-2.4.1.tgz",
- "integrity": "sha512-bCVGdZU+z/qVcIiEQdyx0K13OC5mYwxhSuDUR95oFbKVuXYRrTVrwZIqQljuo1fyJvFTKHiL9L9skQOPokuFNQ==",
- "dependencies": {
- "tslib": "^2.4.0"
- },
- "engines": {
- "node": ">=16.14"
- },
- "peerDependencies": {
- "@docusaurus/types": "*"
- },
- "peerDependenciesMeta": {
- "@docusaurus/types": {
- "optional": true
- }
- }
- },
- "node_modules/@docusaurus/utils-validation": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-2.4.1.tgz",
- "integrity": "sha512-unII3hlJlDwZ3w8U+pMO3Lx3RhI4YEbY3YNsQj4yzrkZzlpqZOLuAiZK2JyULnD+TKbceKU0WyWkQXtYbLNDFA==",
- "dependencies": {
- "@docusaurus/logger": "2.4.1",
- "@docusaurus/utils": "2.4.1",
- "joi": "^17.6.0",
- "js-yaml": "^4.1.0",
- "tslib": "^2.4.0"
- },
- "engines": {
- "node": ">=16.14"
- }
- },
- "node_modules/@hapi/hoek": {
- "version": "9.3.0",
- "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
- "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ=="
- },
- "node_modules/@hapi/topo": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz",
- "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
- "dependencies": {
- "@hapi/hoek": "^9.0.0"
- }
- },
- "node_modules/@jest/schemas": {
- "version": "29.4.3",
- "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz",
- "integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==",
- "dependencies": {
- "@sinclair/typebox": "^0.25.16"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/@jest/types": {
- "version": "29.5.0",
- "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.5.0.tgz",
- "integrity": "sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==",
- "dependencies": {
- "@jest/schemas": "^29.4.3",
- "@types/istanbul-lib-coverage": "^2.0.0",
- "@types/istanbul-reports": "^3.0.0",
- "@types/node": "*",
- "@types/yargs": "^17.0.8",
- "chalk": "^4.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/@jridgewell/gen-mapping": {
- "version": "0.3.3",
- "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
- "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
- "dependencies": {
- "@jridgewell/set-array": "^1.0.1",
- "@jridgewell/sourcemap-codec": "^1.4.10",
- "@jridgewell/trace-mapping": "^0.3.9"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@jridgewell/resolve-uri": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
- "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@jridgewell/set-array": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
- "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@jridgewell/source-map": {
- "version": "0.3.3",
- "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz",
- "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==",
- "dependencies": {
- "@jridgewell/gen-mapping": "^0.3.0",
- "@jridgewell/trace-mapping": "^0.3.9"
- }
- },
- "node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.4.15",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
- "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
- },
- "node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.18",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz",
- "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==",
- "dependencies": {
- "@jridgewell/resolve-uri": "3.1.0",
- "@jridgewell/sourcemap-codec": "1.4.14"
- }
- },
- "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.4.14",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
- "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="
- },
- "node_modules/@leichtgewicht/ip-codec": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
- "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A=="
- },
- "node_modules/@mdx-js/mdx": {
- "version": "1.6.22",
- "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-1.6.22.tgz",
- "integrity": "sha512-AMxuLxPz2j5/6TpF/XSdKpQP1NlG0z11dFOlq+2IP/lSgl11GY8ji6S/rgsViN/L0BDvHvUMruRb7ub+24LUYA==",
- "dependencies": {
- "@babel/core": "7.12.9",
- "@babel/plugin-syntax-jsx": "7.12.1",
- "@babel/plugin-syntax-object-rest-spread": "7.8.3",
- "@mdx-js/util": "1.6.22",
- "babel-plugin-apply-mdx-type-prop": "1.6.22",
- "babel-plugin-extract-import-names": "1.6.22",
- "camelcase-css": "2.0.1",
- "detab": "2.0.4",
- "hast-util-raw": "6.0.1",
- "lodash.uniq": "4.5.0",
- "mdast-util-to-hast": "10.0.1",
- "remark-footnotes": "2.0.0",
- "remark-mdx": "1.6.22",
- "remark-parse": "8.0.3",
- "remark-squeeze-paragraphs": "4.0.0",
- "style-to-object": "0.3.0",
- "unified": "9.2.0",
- "unist-builder": "2.0.3",
- "unist-util-visit": "2.0.3"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/@mdx-js/mdx/node_modules/@babel/core": {
- "version": "7.12.9",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.9.tgz",
- "integrity": "sha512-gTXYh3M5wb7FRXQy+FErKFAv90BnlOuNn1QkCK2lREoPAjrQCO49+HVSrFoe5uakFAF5eenS75KbO2vQiLrTMQ==",
- "dependencies": {
- "@babel/code-frame": "^7.10.4",
- "@babel/generator": "^7.12.5",
- "@babel/helper-module-transforms": "^7.12.1",
- "@babel/helpers": "^7.12.5",
- "@babel/parser": "^7.12.7",
- "@babel/template": "^7.12.7",
- "@babel/traverse": "^7.12.9",
- "@babel/types": "^7.12.7",
- "convert-source-map": "^1.7.0",
- "debug": "^4.1.0",
- "gensync": "^1.0.0-beta.1",
- "json5": "^2.1.2",
- "lodash": "^4.17.19",
- "resolve": "^1.3.2",
- "semver": "^5.4.1",
- "source-map": "^0.5.0"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/babel"
- }
- },
- "node_modules/@mdx-js/mdx/node_modules/@babel/plugin-syntax-jsx": {
- "version": "7.12.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.1.tgz",
- "integrity": "sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.10.4"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@mdx-js/mdx/node_modules/semver": {
- "version": "5.7.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
- "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
- "bin": {
- "semver": "bin/semver"
- }
- },
- "node_modules/@mdx-js/mdx/node_modules/source-map": {
- "version": "0.5.7",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
- "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/@mdx-js/mdx/node_modules/unified": {
- "version": "9.2.0",
- "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.0.tgz",
- "integrity": "sha512-vx2Z0vY+a3YoTj8+pttM3tiJHCwY5UFbYdiWrwBEbHmK8pvsPj2rtAX2BFfgXen8T39CJWblWRDT4L5WGXtDdg==",
- "dependencies": {
- "bail": "^1.0.0",
- "extend": "^3.0.0",
- "is-buffer": "^2.0.0",
- "is-plain-obj": "^2.0.0",
- "trough": "^1.0.0",
- "vfile": "^4.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/@mdx-js/react": {
- "version": "1.6.22",
- "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-1.6.22.tgz",
- "integrity": "sha512-TDoPum4SHdfPiGSAaRBw7ECyI8VaHpK8GJugbJIJuqyh6kzw9ZLJZW3HGL3NNrJGxcAixUvqROm+YuQOo5eXtg==",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- },
- "peerDependencies": {
- "react": "^16.13.1 || ^17.0.0"
- }
- },
- "node_modules/@mdx-js/util": {
- "version": "1.6.22",
- "resolved": "https://registry.npmjs.org/@mdx-js/util/-/util-1.6.22.tgz",
- "integrity": "sha512-H1rQc1ZOHANWBvPcW+JpGwr+juXSxM8Q8YCkm3GhZd8REu1fHR3z99CErO1p9pkcfcxZnMdIZdIsXkOHY0NilA==",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/@nodelib/fs.scandir": {
- "version": "2.1.5",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
- "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
- "dependencies": {
- "@nodelib/fs.stat": "2.0.5",
- "run-parallel": "^1.1.9"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/@nodelib/fs.stat": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
- "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/@nodelib/fs.walk": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
- "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
- "dependencies": {
- "@nodelib/fs.scandir": "2.1.5",
- "fastq": "^1.6.0"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/@polka/url": {
- "version": "1.0.0-next.21",
- "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz",
- "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g=="
- },
- "node_modules/@sideway/address": {
- "version": "4.1.4",
- "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz",
- "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==",
- "dependencies": {
- "@hapi/hoek": "^9.0.0"
- }
- },
- "node_modules/@sideway/formula": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
- "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg=="
- },
- "node_modules/@sideway/pinpoint": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
- "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="
- },
- "node_modules/@sinclair/typebox": {
- "version": "0.25.24",
- "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz",
- "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ=="
- },
- "node_modules/@sindresorhus/is": {
- "version": "0.14.0",
- "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
- "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/@slorber/static-site-generator-webpack-plugin": {
- "version": "4.0.7",
- "resolved": "https://registry.npmjs.org/@slorber/static-site-generator-webpack-plugin/-/static-site-generator-webpack-plugin-4.0.7.tgz",
- "integrity": "sha512-Ug7x6z5lwrz0WqdnNFOMYrDQNTPAprvHLSh6+/fmml3qUiz6l5eq+2MzLKWtn/q5K5NpSiFsZTP/fck/3vjSxA==",
- "dependencies": {
- "eval": "^0.1.8",
- "p-map": "^4.0.0",
- "webpack-sources": "^3.2.2"
- },
- "engines": {
- "node": ">=14"
- }
- },
- "node_modules/@svgr/babel-plugin-add-jsx-attribute": {
- "version": "6.5.1",
- "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-6.5.1.tgz",
- "integrity": "sha512-9PYGcXrAxitycIjRmZB+Q0JaN07GZIWaTBIGQzfaZv+qr1n8X1XUEJ5rZ/vx6OVD9RRYlrNnXWExQXcmZeD/BQ==",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/gregberge"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@svgr/babel-plugin-remove-jsx-attribute": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz",
- "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==",
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/gregberge"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz",
- "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==",
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/gregberge"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": {
- "version": "6.5.1",
- "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-6.5.1.tgz",
- "integrity": "sha512-8DPaVVE3fd5JKuIC29dqyMB54sA6mfgki2H2+swh+zNJoynC8pMPzOkidqHOSc6Wj032fhl8Z0TVn1GiPpAiJg==",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/gregberge"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@svgr/babel-plugin-svg-dynamic-title": {
- "version": "6.5.1",
- "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-6.5.1.tgz",
- "integrity": "sha512-FwOEi0Il72iAzlkaHrlemVurgSQRDFbk0OC8dSvD5fSBPHltNh7JtLsxmZUhjYBZo2PpcU/RJvvi6Q0l7O7ogw==",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/gregberge"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@svgr/babel-plugin-svg-em-dimensions": {
- "version": "6.5.1",
- "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-6.5.1.tgz",
- "integrity": "sha512-gWGsiwjb4tw+ITOJ86ndY/DZZ6cuXMNE/SjcDRg+HLuCmwpcjOktwRF9WgAiycTqJD/QXqL2f8IzE2Rzh7aVXA==",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/gregberge"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@svgr/babel-plugin-transform-react-native-svg": {
- "version": "6.5.1",
- "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-6.5.1.tgz",
- "integrity": "sha512-2jT3nTayyYP7kI6aGutkyfJ7UMGtuguD72OjeGLwVNyfPRBD8zQthlvL+fAbAKk5n9ZNcvFkp/b1lZ7VsYqVJg==",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/gregberge"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@svgr/babel-plugin-transform-svg-component": {
- "version": "6.5.1",
- "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-6.5.1.tgz",
- "integrity": "sha512-a1p6LF5Jt33O3rZoVRBqdxL350oge54iZWHNI6LJB5tQ7EelvD/Mb1mfBiZNAan0dt4i3VArkFRjA4iObuNykQ==",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/gregberge"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@svgr/babel-preset": {
- "version": "6.5.1",
- "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-6.5.1.tgz",
- "integrity": "sha512-6127fvO/FF2oi5EzSQOAjo1LE3OtNVh11R+/8FXa+mHx1ptAaS4cknIjnUA7e6j6fwGGJ17NzaTJFUwOV2zwCw==",
- "dependencies": {
- "@svgr/babel-plugin-add-jsx-attribute": "^6.5.1",
- "@svgr/babel-plugin-remove-jsx-attribute": "*",
- "@svgr/babel-plugin-remove-jsx-empty-expression": "*",
- "@svgr/babel-plugin-replace-jsx-attribute-value": "^6.5.1",
- "@svgr/babel-plugin-svg-dynamic-title": "^6.5.1",
- "@svgr/babel-plugin-svg-em-dimensions": "^6.5.1",
- "@svgr/babel-plugin-transform-react-native-svg": "^6.5.1",
- "@svgr/babel-plugin-transform-svg-component": "^6.5.1"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/gregberge"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@svgr/core": {
- "version": "6.5.1",
- "resolved": "https://registry.npmjs.org/@svgr/core/-/core-6.5.1.tgz",
- "integrity": "sha512-/xdLSWxK5QkqG524ONSjvg3V/FkNyCv538OIBdQqPNaAta3AsXj/Bd2FbvR87yMbXO2hFSWiAe/Q6IkVPDw+mw==",
- "dependencies": {
- "@babel/core": "^7.19.6",
- "@svgr/babel-preset": "^6.5.1",
- "@svgr/plugin-jsx": "^6.5.1",
- "camelcase": "^6.2.0",
- "cosmiconfig": "^7.0.1"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/gregberge"
- }
- },
- "node_modules/@svgr/hast-util-to-babel-ast": {
- "version": "6.5.1",
- "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-6.5.1.tgz",
- "integrity": "sha512-1hnUxxjd83EAxbL4a0JDJoD3Dao3hmjvyvyEV8PzWmLK3B9m9NPlW7GKjFyoWE8nM7HnXzPcmmSyOW8yOddSXw==",
- "dependencies": {
- "@babel/types": "^7.20.0",
- "entities": "^4.4.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/gregberge"
- }
- },
- "node_modules/@svgr/plugin-jsx": {
- "version": "6.5.1",
- "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-6.5.1.tgz",
- "integrity": "sha512-+UdQxI3jgtSjCykNSlEMuy1jSRQlGC7pqBCPvkG/2dATdWo082zHTTK3uhnAju2/6XpE6B5mZ3z4Z8Ns01S8Gw==",
- "dependencies": {
- "@babel/core": "^7.19.6",
- "@svgr/babel-preset": "^6.5.1",
- "@svgr/hast-util-to-babel-ast": "^6.5.1",
- "svg-parser": "^2.0.4"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/gregberge"
- },
- "peerDependencies": {
- "@svgr/core": "^6.0.0"
- }
- },
- "node_modules/@svgr/plugin-svgo": {
- "version": "6.5.1",
- "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-6.5.1.tgz",
- "integrity": "sha512-omvZKf8ixP9z6GWgwbtmP9qQMPX4ODXi+wzbVZgomNFsUIlHA1sf4fThdwTWSsZGgvGAG6yE+b/F5gWUkcZ/iQ==",
- "dependencies": {
- "cosmiconfig": "^7.0.1",
- "deepmerge": "^4.2.2",
- "svgo": "^2.8.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/gregberge"
- },
- "peerDependencies": {
- "@svgr/core": "*"
- }
- },
- "node_modules/@svgr/webpack": {
- "version": "6.5.1",
- "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-6.5.1.tgz",
- "integrity": "sha512-cQ/AsnBkXPkEK8cLbv4Dm7JGXq2XrumKnL1dRpJD9rIO2fTIlJI9a1uCciYG1F2aUsox/hJQyNGbt3soDxSRkA==",
- "dependencies": {
- "@babel/core": "^7.19.6",
- "@babel/plugin-transform-react-constant-elements": "^7.18.12",
- "@babel/preset-env": "^7.19.4",
- "@babel/preset-react": "^7.18.6",
- "@babel/preset-typescript": "^7.18.6",
- "@svgr/core": "^6.5.1",
- "@svgr/plugin-jsx": "^6.5.1",
- "@svgr/plugin-svgo": "^6.5.1"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/gregberge"
- }
- },
- "node_modules/@szmarczak/http-timer": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz",
- "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==",
- "dependencies": {
- "defer-to-connect": "^1.0.1"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/@trysound/sax": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
- "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==",
- "engines": {
- "node": ">=10.13.0"
- }
- },
- "node_modules/@tsconfig/docusaurus": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/@tsconfig/docusaurus/-/docusaurus-1.0.7.tgz",
- "integrity": "sha512-ffTXxGIP/IRMCjuzHd6M4/HdIrw1bMfC7Bv8hMkTadnePkpe0lG0oDSdbRpSDZb2rQMAgpbWiR10BvxvNYwYrg==",
- "dev": true
- },
- "node_modules/@types/body-parser": {
- "version": "1.19.2",
- "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
- "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==",
- "dependencies": {
- "@types/connect": "*",
- "@types/node": "*"
- }
- },
- "node_modules/@types/bonjour": {
- "version": "3.5.10",
- "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz",
- "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==",
- "dependencies": {
- "@types/node": "*"
- }
- },
- "node_modules/@types/connect": {
- "version": "3.4.35",
- "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
- "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==",
- "dependencies": {
- "@types/node": "*"
- }
- },
- "node_modules/@types/connect-history-api-fallback": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz",
- "integrity": "sha512-4x5FkPpLipqwthjPsF7ZRbOv3uoLUFkTA9G9v583qi4pACvq0uTELrB8OLUzPWUI4IJIyvM85vzkV1nyiI2Lig==",
- "dependencies": {
- "@types/express-serve-static-core": "*",
- "@types/node": "*"
- }
- },
- "node_modules/@types/eslint": {
- "version": "8.40.2",
- "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.40.2.tgz",
- "integrity": "sha512-PRVjQ4Eh9z9pmmtaq8nTjZjQwKFk7YIHIud3lRoKRBgUQjgjRmoGxxGEPXQkF+lH7QkHJRNr5F4aBgYCW0lqpQ==",
- "dependencies": {
- "@types/estree": "*",
- "@types/json-schema": "*"
- }
- },
- "node_modules/@types/eslint-scope": {
- "version": "3.7.4",
- "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz",
- "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==",
- "dependencies": {
- "@types/eslint": "*",
- "@types/estree": "*"
- }
- },
- "node_modules/@types/estree": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz",
- "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA=="
- },
- "node_modules/@types/express": {
- "version": "4.17.17",
- "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz",
- "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==",
- "dependencies": {
- "@types/body-parser": "*",
- "@types/express-serve-static-core": "^4.17.33",
- "@types/qs": "*",
- "@types/serve-static": "*"
- }
- },
- "node_modules/@types/express-serve-static-core": {
- "version": "4.17.35",
- "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz",
- "integrity": "sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==",
- "dependencies": {
- "@types/node": "*",
- "@types/qs": "*",
- "@types/range-parser": "*",
- "@types/send": "*"
- }
- },
- "node_modules/@types/hast": {
- "version": "2.3.4",
- "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz",
- "integrity": "sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==",
- "dependencies": {
- "@types/unist": "*"
- }
- },
- "node_modules/@types/history": {
- "version": "4.7.11",
- "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz",
- "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA=="
- },
- "node_modules/@types/html-minifier-terser": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
- "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg=="
- },
- "node_modules/@types/http-proxy": {
- "version": "1.17.11",
- "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.11.tgz",
- "integrity": "sha512-HC8G7c1WmaF2ekqpnFq626xd3Zz0uvaqFmBJNRZCGEZCXkvSdJoNFn/8Ygbd9fKNQj8UzLdCETaI0UWPAjK7IA==",
- "dependencies": {
- "@types/node": "*"
- }
- },
- "node_modules/@types/istanbul-lib-coverage": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
- "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g=="
- },
- "node_modules/@types/istanbul-lib-report": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz",
- "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==",
- "dependencies": {
- "@types/istanbul-lib-coverage": "*"
- }
- },
- "node_modules/@types/istanbul-reports": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz",
- "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==",
- "dependencies": {
- "@types/istanbul-lib-report": "*"
- }
- },
- "node_modules/@types/json-schema": {
- "version": "7.0.12",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz",
- "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA=="
- },
- "node_modules/@types/mdast": {
- "version": "3.0.11",
- "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.11.tgz",
- "integrity": "sha512-Y/uImid8aAwrEA24/1tcRZwpxX3pIFTSilcNDKSPn+Y2iDywSEachzRuvgAYYLR3wpGXAsMbv5lvKLDZLeYPAw==",
- "dependencies": {
- "@types/unist": "*"
- }
- },
- "node_modules/@types/mime": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
- "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw=="
- },
- "node_modules/@types/node": {
- "version": "20.3.1",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.1.tgz",
- "integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg=="
- },
- "node_modules/@types/parse-json": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
- "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
- },
- "node_modules/@types/parse5": {
- "version": "5.0.3",
- "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-5.0.3.tgz",
- "integrity": "sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw=="
- },
- "node_modules/@types/prop-types": {
- "version": "15.7.5",
- "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
- "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w=="
- },
- "node_modules/@types/qs": {
- "version": "6.9.7",
- "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
- "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw=="
- },
- "node_modules/@types/range-parser": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
- "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw=="
- },
- "node_modules/@types/react": {
- "version": "18.2.13",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.13.tgz",
- "integrity": "sha512-vJ+zElvi/Zn9cVXB5slX2xL8PZodPCwPRDpittQdw43JR2AJ5k3vKdgJJyneV/cYgIbLQUwXa9JVDvUZXGba+Q==",
- "dependencies": {
- "@types/prop-types": "*",
- "@types/scheduler": "*",
- "csstype": "^3.0.2"
- }
- },
- "node_modules/@types/react-router": {
- "version": "5.1.20",
- "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz",
- "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==",
- "dependencies": {
- "@types/history": "^4.7.11",
- "@types/react": "*"
- }
- },
- "node_modules/@types/react-router-config": {
- "version": "5.0.7",
- "resolved": "https://registry.npmjs.org/@types/react-router-config/-/react-router-config-5.0.7.tgz",
- "integrity": "sha512-pFFVXUIydHlcJP6wJm7sDii5mD/bCmmAY0wQzq+M+uX7bqS95AQqHZWP1iNMKrWVQSuHIzj5qi9BvrtLX2/T4w==",
- "dependencies": {
- "@types/history": "^4.7.11",
- "@types/react": "*",
- "@types/react-router": "^5.1.0"
- }
- },
- "node_modules/@types/react-router-dom": {
- "version": "5.3.3",
- "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz",
- "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==",
- "dependencies": {
- "@types/history": "^4.7.11",
- "@types/react": "*",
- "@types/react-router": "*"
- }
- },
- "node_modules/@types/retry": {
- "version": "0.12.0",
- "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
- "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="
- },
- "node_modules/@types/sax": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.4.tgz",
- "integrity": "sha512-pSAff4IAxJjfAXUG6tFkO7dsSbTmf8CtUpfhhZ5VhkRpC4628tJhh3+V6H1E+/Gs9piSzYKT5yzHO5M4GG9jkw==",
- "dependencies": {
- "@types/node": "*"
- }
- },
- "node_modules/@types/scheduler": {
- "version": "0.16.3",
- "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz",
- "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ=="
- },
- "node_modules/@types/send": {
- "version": "0.17.1",
- "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.1.tgz",
- "integrity": "sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==",
- "dependencies": {
- "@types/mime": "^1",
- "@types/node": "*"
- }
- },
- "node_modules/@types/serve-index": {
- "version": "1.9.1",
- "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz",
- "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==",
- "dependencies": {
- "@types/express": "*"
- }
- },
- "node_modules/@types/serve-static": {
- "version": "1.15.1",
- "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.1.tgz",
- "integrity": "sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==",
- "dependencies": {
- "@types/mime": "*",
- "@types/node": "*"
- }
- },
- "node_modules/@types/sockjs": {
- "version": "0.3.33",
- "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz",
- "integrity": "sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==",
- "dependencies": {
- "@types/node": "*"
- }
- },
- "node_modules/@types/unist": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz",
- "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="
- },
- "node_modules/@types/ws": {
- "version": "8.5.5",
- "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz",
- "integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==",
- "dependencies": {
- "@types/node": "*"
- }
- },
- "node_modules/@types/yargs": {
- "version": "17.0.24",
- "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz",
- "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==",
- "dependencies": {
- "@types/yargs-parser": "*"
- }
- },
- "node_modules/@types/yargs-parser": {
- "version": "21.0.0",
- "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz",
- "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA=="
- },
- "node_modules/@webassemblyjs/ast": {
- "version": "1.11.6",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz",
- "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==",
- "dependencies": {
- "@webassemblyjs/helper-numbers": "1.11.6",
- "@webassemblyjs/helper-wasm-bytecode": "1.11.6"
- }
- },
- "node_modules/@webassemblyjs/floating-point-hex-parser": {
- "version": "1.11.6",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz",
- "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw=="
- },
- "node_modules/@webassemblyjs/helper-api-error": {
- "version": "1.11.6",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz",
- "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q=="
- },
- "node_modules/@webassemblyjs/helper-buffer": {
- "version": "1.11.6",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz",
- "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA=="
- },
- "node_modules/@webassemblyjs/helper-numbers": {
- "version": "1.11.6",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz",
- "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==",
- "dependencies": {
- "@webassemblyjs/floating-point-hex-parser": "1.11.6",
- "@webassemblyjs/helper-api-error": "1.11.6",
- "@xtuc/long": "4.2.2"
- }
- },
- "node_modules/@webassemblyjs/helper-wasm-bytecode": {
- "version": "1.11.6",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz",
- "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA=="
- },
- "node_modules/@webassemblyjs/helper-wasm-section": {
- "version": "1.11.6",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz",
- "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==",
- "dependencies": {
- "@webassemblyjs/ast": "1.11.6",
- "@webassemblyjs/helper-buffer": "1.11.6",
- "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
- "@webassemblyjs/wasm-gen": "1.11.6"
- }
- },
- "node_modules/@webassemblyjs/ieee754": {
- "version": "1.11.6",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz",
- "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==",
- "dependencies": {
- "@xtuc/ieee754": "^1.2.0"
- }
- },
- "node_modules/@webassemblyjs/leb128": {
- "version": "1.11.6",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz",
- "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==",
- "dependencies": {
- "@xtuc/long": "4.2.2"
- }
- },
- "node_modules/@webassemblyjs/utf8": {
- "version": "1.11.6",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz",
- "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA=="
- },
- "node_modules/@webassemblyjs/wasm-edit": {
- "version": "1.11.6",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz",
- "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==",
- "dependencies": {
- "@webassemblyjs/ast": "1.11.6",
- "@webassemblyjs/helper-buffer": "1.11.6",
- "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
- "@webassemblyjs/helper-wasm-section": "1.11.6",
- "@webassemblyjs/wasm-gen": "1.11.6",
- "@webassemblyjs/wasm-opt": "1.11.6",
- "@webassemblyjs/wasm-parser": "1.11.6",
- "@webassemblyjs/wast-printer": "1.11.6"
- }
- },
- "node_modules/@webassemblyjs/wasm-gen": {
- "version": "1.11.6",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz",
- "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==",
- "dependencies": {
- "@webassemblyjs/ast": "1.11.6",
- "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
- "@webassemblyjs/ieee754": "1.11.6",
- "@webassemblyjs/leb128": "1.11.6",
- "@webassemblyjs/utf8": "1.11.6"
- }
- },
- "node_modules/@webassemblyjs/wasm-opt": {
- "version": "1.11.6",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz",
- "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==",
- "dependencies": {
- "@webassemblyjs/ast": "1.11.6",
- "@webassemblyjs/helper-buffer": "1.11.6",
- "@webassemblyjs/wasm-gen": "1.11.6",
- "@webassemblyjs/wasm-parser": "1.11.6"
- }
- },
- "node_modules/@webassemblyjs/wasm-parser": {
- "version": "1.11.6",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz",
- "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==",
- "dependencies": {
- "@webassemblyjs/ast": "1.11.6",
- "@webassemblyjs/helper-api-error": "1.11.6",
- "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
- "@webassemblyjs/ieee754": "1.11.6",
- "@webassemblyjs/leb128": "1.11.6",
- "@webassemblyjs/utf8": "1.11.6"
- }
- },
- "node_modules/@webassemblyjs/wast-printer": {
- "version": "1.11.6",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz",
- "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==",
- "dependencies": {
- "@webassemblyjs/ast": "1.11.6",
- "@xtuc/long": "4.2.2"
- }
- },
- "node_modules/@xtuc/ieee754": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
- "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="
- },
- "node_modules/@xtuc/long": {
- "version": "4.2.2",
- "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
- "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="
- },
- "node_modules/accepts": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
- "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
- "dependencies": {
- "mime-types": "~2.1.34",
- "negotiator": "0.6.3"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/accepts/node_modules/mime-db": {
- "version": "1.52.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
- "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/accepts/node_modules/mime-types": {
- "version": "2.1.35",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
- "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
- "dependencies": {
- "mime-db": "1.52.0"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/acorn": {
- "version": "8.9.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz",
- "integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==",
- "bin": {
- "acorn": "bin/acorn"
- },
- "engines": {
- "node": ">=0.4.0"
- }
- },
- "node_modules/acorn-import-assertions": {
- "version": "1.9.0",
- "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz",
- "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==",
- "peerDependencies": {
- "acorn": "^8"
- }
- },
- "node_modules/acorn-walk": {
- "version": "8.2.0",
- "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
- "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
- "engines": {
- "node": ">=0.4.0"
- }
- },
- "node_modules/address": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz",
- "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==",
- "engines": {
- "node": ">= 10.0.0"
- }
- },
- "node_modules/aggregate-error": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
- "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
- "dependencies": {
- "clean-stack": "^2.0.0",
- "indent-string": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/ajv": {
- "version": "6.12.6",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
- "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
- "dependencies": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/ajv-formats": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
- "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
- "dependencies": {
- "ajv": "^8.0.0"
- },
- "peerDependencies": {
- "ajv": "^8.0.0"
- },
- "peerDependenciesMeta": {
- "ajv": {
- "optional": true
- }
- }
- },
- "node_modules/ajv-formats/node_modules/ajv": {
- "version": "8.12.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
- "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
- "dependencies": {
- "fast-deep-equal": "^3.1.1",
- "json-schema-traverse": "^1.0.0",
- "require-from-string": "^2.0.2",
- "uri-js": "^4.2.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/ajv-formats/node_modules/json-schema-traverse": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
- "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
- },
- "node_modules/ajv-keywords": {
- "version": "3.5.2",
- "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
- "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
- "peerDependencies": {
- "ajv": "^6.9.1"
- }
- },
- "node_modules/algoliasearch": {
- "version": "4.17.2",
- "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.17.2.tgz",
- "integrity": "sha512-VFu43JJNYIW74awp7oeQcQsPcxOhd8psqBDTfyNO2Zt6L1NqnNMTVnaIdQ+8dtKqUDBqQZp0szPxECvX8CK2Fg==",
- "dependencies": {
- "@algolia/cache-browser-local-storage": "4.17.2",
- "@algolia/cache-common": "4.17.2",
- "@algolia/cache-in-memory": "4.17.2",
- "@algolia/client-account": "4.17.2",
- "@algolia/client-analytics": "4.17.2",
- "@algolia/client-common": "4.17.2",
- "@algolia/client-personalization": "4.17.2",
- "@algolia/client-search": "4.17.2",
- "@algolia/logger-common": "4.17.2",
- "@algolia/logger-console": "4.17.2",
- "@algolia/requester-browser-xhr": "4.17.2",
- "@algolia/requester-common": "4.17.2",
- "@algolia/requester-node-http": "4.17.2",
- "@algolia/transporter": "4.17.2"
- }
- },
- "node_modules/algoliasearch-helper": {
- "version": "3.13.2",
- "resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.13.2.tgz",
- "integrity": "sha512-1bZjtHuqCBYw7Eu3Qh0Jfq4s63UcbOs6VvLPdt7kxn5+zMgs46xiXgc65YhZBNM3hDGrudhAX9hDhE9OP+rKUw==",
- "dependencies": {
- "@algolia/events": "^4.0.1"
- },
- "peerDependencies": {
- "algoliasearch": ">= 3.1 < 6"
- }
- },
- "node_modules/ansi-align": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
- "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==",
- "dependencies": {
- "string-width": "^4.1.0"
- }
- },
- "node_modules/ansi-align/node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
- },
- "node_modules/ansi-align/node_modules/string-width": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/ansi-html-community": {
- "version": "0.0.8",
- "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz",
- "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==",
- "engines": [
- "node >= 0.8.0"
- ],
- "bin": {
- "ansi-html": "bin/ansi-html"
- }
- },
- "node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/ansi-styles": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/anymatch": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
- "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
- "dependencies": {
- "normalize-path": "^3.0.0",
- "picomatch": "^2.0.4"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/arg": {
- "version": "5.0.2",
- "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
- "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="
- },
- "node_modules/argparse": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
- "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
- },
- "node_modules/array-flatten": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz",
- "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ=="
- },
- "node_modules/array-union": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
- "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/asap": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
- "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="
- },
- "node_modules/at-least-node": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
- "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
- "engines": {
- "node": ">= 4.0.0"
- }
- },
- "node_modules/autoprefixer": {
- "version": "10.4.14",
- "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz",
- "integrity": "sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==",
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/autoprefixer"
- }
- ],
- "dependencies": {
- "browserslist": "^4.21.5",
- "caniuse-lite": "^1.0.30001464",
- "fraction.js": "^4.2.0",
- "normalize-range": "^0.1.2",
- "picocolors": "^1.0.0",
- "postcss-value-parser": "^4.2.0"
- },
- "bin": {
- "autoprefixer": "bin/autoprefixer"
- },
- "engines": {
- "node": "^10 || ^12 || >=14"
- },
- "peerDependencies": {
- "postcss": "^8.1.0"
- }
- },
- "node_modules/axios": {
- "version": "0.25.0",
- "resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz",
- "integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==",
- "dependencies": {
- "follow-redirects": "^1.14.7"
- }
- },
- "node_modules/babel-loader": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.3.0.tgz",
- "integrity": "sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==",
- "dependencies": {
- "find-cache-dir": "^3.3.1",
- "loader-utils": "^2.0.0",
- "make-dir": "^3.1.0",
- "schema-utils": "^2.6.5"
- },
- "engines": {
- "node": ">= 8.9"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0",
- "webpack": ">=2"
- }
- },
- "node_modules/babel-plugin-apply-mdx-type-prop": {
- "version": "1.6.22",
- "resolved": "https://registry.npmjs.org/babel-plugin-apply-mdx-type-prop/-/babel-plugin-apply-mdx-type-prop-1.6.22.tgz",
- "integrity": "sha512-VefL+8o+F/DfK24lPZMtJctrCVOfgbqLAGZSkxwhazQv4VxPg3Za/i40fu22KR2m8eEda+IfSOlPLUSIiLcnCQ==",
- "dependencies": {
- "@babel/helper-plugin-utils": "7.10.4",
- "@mdx-js/util": "1.6.22"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- },
- "peerDependencies": {
- "@babel/core": "^7.11.6"
- }
- },
- "node_modules/babel-plugin-apply-mdx-type-prop/node_modules/@babel/helper-plugin-utils": {
- "version": "7.10.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz",
- "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg=="
- },
- "node_modules/babel-plugin-dynamic-import-node": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz",
- "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==",
- "dependencies": {
- "object.assign": "^4.1.0"
- }
- },
- "node_modules/babel-plugin-extract-import-names": {
- "version": "1.6.22",
- "resolved": "https://registry.npmjs.org/babel-plugin-extract-import-names/-/babel-plugin-extract-import-names-1.6.22.tgz",
- "integrity": "sha512-yJ9BsJaISua7d8zNT7oRG1ZLBJCIdZ4PZqmH8qa9N5AK01ifk3fnkc98AXhtzE7UkfCsEumvoQWgoYLhOnJ7jQ==",
- "dependencies": {
- "@babel/helper-plugin-utils": "7.10.4"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/babel-plugin-extract-import-names/node_modules/@babel/helper-plugin-utils": {
- "version": "7.10.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz",
- "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg=="
- },
- "node_modules/babel-plugin-polyfill-corejs2": {
- "version": "0.4.3",
- "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.3.tgz",
- "integrity": "sha512-bM3gHc337Dta490gg+/AseNB9L4YLHxq1nGKZZSHbhXv4aTYU2MD2cjza1Ru4S6975YLTaL1K8uJf6ukJhhmtw==",
- "dependencies": {
- "@babel/compat-data": "^7.17.7",
- "@babel/helper-define-polyfill-provider": "^0.4.0",
- "semver": "^6.1.1"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
- "node_modules/babel-plugin-polyfill-corejs3": {
- "version": "0.8.1",
- "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.1.tgz",
- "integrity": "sha512-ikFrZITKg1xH6pLND8zT14UPgjKHiGLqex7rGEZCH2EvhsneJaJPemmpQaIZV5AL03II+lXylw3UmddDK8RU5Q==",
- "dependencies": {
- "@babel/helper-define-polyfill-provider": "^0.4.0",
- "core-js-compat": "^3.30.1"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/babel-plugin-polyfill-regenerator": {
- "version": "0.5.0",
- "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.0.tgz",
- "integrity": "sha512-hDJtKjMLVa7Z+LwnTCxoDLQj6wdc+B8dun7ayF2fYieI6OzfuvcLMB32ihJZ4UhCBwNYGl5bg/x/P9cMdnkc2g==",
- "dependencies": {
- "@babel/helper-define-polyfill-provider": "^0.4.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/bail": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz",
- "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/balanced-match": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
- },
- "node_modules/base16": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/base16/-/base16-1.0.0.tgz",
- "integrity": "sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ=="
- },
- "node_modules/batch": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
- "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw=="
- },
- "node_modules/big.js": {
- "version": "5.2.2",
- "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
- "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==",
- "engines": {
- "node": "*"
- }
- },
- "node_modules/binary-extensions": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
- "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/body-parser": {
- "version": "1.20.1",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
- "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
- "dependencies": {
- "bytes": "3.1.2",
- "content-type": "~1.0.4",
- "debug": "2.6.9",
- "depd": "2.0.0",
- "destroy": "1.2.0",
- "http-errors": "2.0.0",
- "iconv-lite": "0.4.24",
- "on-finished": "2.4.1",
- "qs": "6.11.0",
- "raw-body": "2.5.1",
- "type-is": "~1.6.18",
- "unpipe": "1.0.0"
- },
- "engines": {
- "node": ">= 0.8",
- "npm": "1.2.8000 || >= 1.4.16"
- }
- },
- "node_modules/body-parser/node_modules/bytes": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
- "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/body-parser/node_modules/debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "dependencies": {
- "ms": "2.0.0"
- }
- },
- "node_modules/body-parser/node_modules/ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
- },
- "node_modules/bonjour-service": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.1.1.tgz",
- "integrity": "sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg==",
- "dependencies": {
- "array-flatten": "^2.1.2",
- "dns-equal": "^1.0.0",
- "fast-deep-equal": "^3.1.3",
- "multicast-dns": "^7.2.5"
- }
- },
- "node_modules/boolbase": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
- "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
- },
- "node_modules/boxen": {
- "version": "6.2.1",
- "resolved": "https://registry.npmjs.org/boxen/-/boxen-6.2.1.tgz",
- "integrity": "sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw==",
- "dependencies": {
- "ansi-align": "^3.0.1",
- "camelcase": "^6.2.0",
- "chalk": "^4.1.2",
- "cli-boxes": "^3.0.0",
- "string-width": "^5.0.1",
- "type-fest": "^2.5.0",
- "widest-line": "^4.0.1",
- "wrap-ansi": "^8.0.1"
- },
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "node_modules/braces": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
- "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
- "dependencies": {
- "fill-range": "^7.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/browserslist": {
- "version": "4.21.9",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz",
- "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==",
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/browserslist"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "dependencies": {
- "caniuse-lite": "^1.0.30001503",
- "electron-to-chromium": "^1.4.431",
- "node-releases": "^2.0.12",
- "update-browserslist-db": "^1.0.11"
- },
- "bin": {
- "browserslist": "cli.js"
- },
- "engines": {
- "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
- }
- },
- "node_modules/buffer-from": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
- "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
- },
- "node_modules/bytes": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
- "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/cacheable-request": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz",
- "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==",
- "dependencies": {
- "clone-response": "^1.0.2",
- "get-stream": "^5.1.0",
- "http-cache-semantics": "^4.0.0",
- "keyv": "^3.0.0",
- "lowercase-keys": "^2.0.0",
- "normalize-url": "^4.1.0",
- "responselike": "^1.0.2"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/cacheable-request/node_modules/get-stream": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
- "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
- "dependencies": {
- "pump": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/cacheable-request/node_modules/lowercase-keys": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
- "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/cacheable-request/node_modules/normalize-url": {
- "version": "4.5.1",
- "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz",
- "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/call-bind": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
- "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
- "dependencies": {
- "function-bind": "^1.1.1",
- "get-intrinsic": "^1.0.2"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/callsites": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
- "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/camel-case": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz",
- "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==",
- "dependencies": {
- "pascal-case": "^3.1.2",
- "tslib": "^2.0.3"
- }
- },
- "node_modules/camelcase": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
- "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/camelcase-css": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
- "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/caniuse-api": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
- "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==",
- "dependencies": {
- "browserslist": "^4.0.0",
- "caniuse-lite": "^1.0.0",
- "lodash.memoize": "^4.1.2",
- "lodash.uniq": "^4.5.0"
- }
- },
- "node_modules/caniuse-lite": {
- "version": "1.0.30001505",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001505.tgz",
- "integrity": "sha512-jaAOR5zVtxHfL0NjZyflVTtXm3D3J9P15zSJ7HmQF8dSKGA6tqzQq+0ZI3xkjyQj46I4/M0K2GbMpcAFOcbr3A==",
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ]
- },
- "node_modules/ccount": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/ccount/-/ccount-1.1.0.tgz",
- "integrity": "sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/chalk": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
- "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
- "dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
- }
- },
- "node_modules/character-entities": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz",
- "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/character-entities-legacy": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz",
- "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/character-reference-invalid": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz",
- "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/cheerio": {
- "version": "1.0.0-rc.12",
- "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz",
- "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==",
- "dependencies": {
- "cheerio-select": "^2.1.0",
- "dom-serializer": "^2.0.0",
- "domhandler": "^5.0.3",
- "domutils": "^3.0.1",
- "htmlparser2": "^8.0.1",
- "parse5": "^7.0.0",
- "parse5-htmlparser2-tree-adapter": "^7.0.0"
- },
- "engines": {
- "node": ">= 6"
- },
- "funding": {
- "url": "https://github.com/cheeriojs/cheerio?sponsor=1"
- }
- },
- "node_modules/cheerio-select": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz",
- "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
- "dependencies": {
- "boolbase": "^1.0.0",
- "css-select": "^5.1.0",
- "css-what": "^6.1.0",
- "domelementtype": "^2.3.0",
- "domhandler": "^5.0.3",
- "domutils": "^3.0.1"
- },
- "funding": {
- "url": "https://github.com/sponsors/fb55"
- }
- },
- "node_modules/chokidar": {
- "version": "3.5.3",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
- "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
- "funding": [
- {
- "type": "individual",
- "url": "https://paulmillr.com/funding/"
- }
- ],
- "dependencies": {
- "anymatch": "~3.1.2",
- "braces": "~3.0.2",
- "glob-parent": "~5.1.2",
- "is-binary-path": "~2.1.0",
- "is-glob": "~4.0.1",
- "normalize-path": "~3.0.0",
- "readdirp": "~3.6.0"
- },
- "engines": {
- "node": ">= 8.10.0"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.2"
- }
- },
- "node_modules/chrome-trace-event": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz",
- "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==",
- "engines": {
- "node": ">=6.0"
- }
- },
- "node_modules/ci-info": {
- "version": "3.8.0",
- "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz",
- "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/sibiraj-s"
- }
- ],
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/clean-css": {
- "version": "5.3.2",
- "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz",
- "integrity": "sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==",
- "dependencies": {
- "source-map": "~0.6.0"
- },
- "engines": {
- "node": ">= 10.0"
- }
- },
- "node_modules/clean-stack": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
- "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/cli-boxes": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz",
- "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/cli-table3": {
- "version": "0.6.3",
- "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz",
- "integrity": "sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==",
- "dependencies": {
- "string-width": "^4.2.0"
- },
- "engines": {
- "node": "10.* || >= 12.*"
- },
- "optionalDependencies": {
- "@colors/colors": "1.5.0"
- }
- },
- "node_modules/cli-table3/node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
- },
- "node_modules/cli-table3/node_modules/string-width": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/clone-deep": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
- "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==",
- "dependencies": {
- "is-plain-object": "^2.0.4",
- "kind-of": "^6.0.2",
- "shallow-clone": "^3.0.0"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/clone-response": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
- "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==",
- "dependencies": {
- "mimic-response": "^1.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/clsx": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
- "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/collapse-white-space": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.6.tgz",
- "integrity": "sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
- },
- "node_modules/colord": {
- "version": "2.9.3",
- "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
- "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw=="
- },
- "node_modules/colorette": {
- "version": "2.0.20",
- "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
- "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="
- },
- "node_modules/combine-promises": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/combine-promises/-/combine-promises-1.1.0.tgz",
- "integrity": "sha512-ZI9jvcLDxqwaXEixOhArm3r7ReIivsXkpbyEWyeOhzz1QS0iSgBPnWvEqvIQtYyamGCYA88gFhmUrs9hrrQ0pg==",
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/comma-separated-tokens": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz",
- "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/commander": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
- "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==",
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/commondir": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
- "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="
- },
- "node_modules/compressible": {
- "version": "2.0.18",
- "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
- "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
- "dependencies": {
- "mime-db": ">= 1.43.0 < 2"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/compressible/node_modules/mime-db": {
- "version": "1.52.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
- "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/compression": {
- "version": "1.7.4",
- "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz",
- "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==",
- "dependencies": {
- "accepts": "~1.3.5",
- "bytes": "3.0.0",
- "compressible": "~2.0.16",
- "debug": "2.6.9",
- "on-headers": "~1.0.2",
- "safe-buffer": "5.1.2",
- "vary": "~1.1.2"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/compression/node_modules/debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "dependencies": {
- "ms": "2.0.0"
- }
- },
- "node_modules/compression/node_modules/ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
- },
- "node_modules/compression/node_modules/safe-buffer": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
- "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
- },
- "node_modules/concat-map": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
- },
- "node_modules/configstore": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz",
- "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==",
- "dependencies": {
- "dot-prop": "^5.2.0",
- "graceful-fs": "^4.1.2",
- "make-dir": "^3.0.0",
- "unique-string": "^2.0.0",
- "write-file-atomic": "^3.0.0",
- "xdg-basedir": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/connect-history-api-fallback": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz",
- "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==",
- "engines": {
- "node": ">=0.8"
- }
- },
- "node_modules/consola": {
- "version": "2.15.3",
- "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz",
- "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw=="
- },
- "node_modules/content-disposition": {
- "version": "0.5.2",
- "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
- "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/content-type": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
- "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/convert-source-map": {
- "version": "1.9.0",
- "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
- "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
- },
- "node_modules/cookie": {
- "version": "0.5.0",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
- "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/cookie-signature": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
- "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
- },
- "node_modules/copy-text-to-clipboard": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/copy-text-to-clipboard/-/copy-text-to-clipboard-3.1.0.tgz",
- "integrity": "sha512-PFM6BnjLnOON/lB3ta/Jg7Ywsv+l9kQGD4TWDCSlRBGmqnnTM5MrDkhAFgw+8HZt0wW6Q2BBE4cmy9sq+s9Qng==",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/copy-webpack-plugin": {
- "version": "11.0.0",
- "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz",
- "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==",
- "dependencies": {
- "fast-glob": "^3.2.11",
- "glob-parent": "^6.0.1",
- "globby": "^13.1.1",
- "normalize-path": "^3.0.0",
- "schema-utils": "^4.0.0",
- "serialize-javascript": "^6.0.0"
- },
- "engines": {
- "node": ">= 14.15.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- },
- "peerDependencies": {
- "webpack": "^5.1.0"
- }
- },
- "node_modules/copy-webpack-plugin/node_modules/ajv": {
- "version": "8.12.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
- "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
- "dependencies": {
- "fast-deep-equal": "^3.1.1",
- "json-schema-traverse": "^1.0.0",
- "require-from-string": "^2.0.2",
- "uri-js": "^4.2.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/copy-webpack-plugin/node_modules/ajv-keywords": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
- "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
- "dependencies": {
- "fast-deep-equal": "^3.1.3"
- },
- "peerDependencies": {
- "ajv": "^8.8.2"
- }
- },
- "node_modules/copy-webpack-plugin/node_modules/glob-parent": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
- "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
- "dependencies": {
- "is-glob": "^4.0.3"
- },
- "engines": {
- "node": ">=10.13.0"
- }
- },
- "node_modules/copy-webpack-plugin/node_modules/globby": {
- "version": "13.2.0",
- "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.0.tgz",
- "integrity": "sha512-jWsQfayf13NvqKUIL3Ta+CIqMnvlaIDFveWE/dpOZ9+3AMEJozsxDvKA02zync9UuvOM8rOXzsD5GqKP4OnWPQ==",
- "dependencies": {
- "dir-glob": "^3.0.1",
- "fast-glob": "^3.2.11",
- "ignore": "^5.2.0",
- "merge2": "^1.4.1",
- "slash": "^4.0.0"
- },
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/copy-webpack-plugin/node_modules/json-schema-traverse": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
- "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
- },
- "node_modules/copy-webpack-plugin/node_modules/schema-utils": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz",
- "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==",
- "dependencies": {
- "@types/json-schema": "^7.0.9",
- "ajv": "^8.9.0",
- "ajv-formats": "^2.1.1",
- "ajv-keywords": "^5.1.0"
- },
- "engines": {
- "node": ">= 12.13.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- }
- },
- "node_modules/copy-webpack-plugin/node_modules/slash": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz",
- "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/core-js": {
- "version": "3.31.0",
- "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.31.0.tgz",
- "integrity": "sha512-NIp2TQSGfR6ba5aalZD+ZQ1fSxGhDo/s1w0nx3RYzf2pnJxt7YynxFlFScP6eV7+GZsKO95NSjGxyJsU3DZgeQ==",
- "hasInstallScript": true,
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/core-js"
- }
- },
- "node_modules/core-js-compat": {
- "version": "3.31.0",
- "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.31.0.tgz",
- "integrity": "sha512-hM7YCu1cU6Opx7MXNu0NuumM0ezNeAeRKadixyiQELWY3vT3De9S4J5ZBMraWV2vZnrE1Cirl0GtFtDtMUXzPw==",
- "dependencies": {
- "browserslist": "^4.21.5"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/core-js"
- }
- },
- "node_modules/core-js-pure": {
- "version": "3.31.0",
- "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.31.0.tgz",
- "integrity": "sha512-/AnE9Y4OsJZicCzIe97JP5XoPKQJfTuEG43aEVLFJGOJpyqELod+pE6LEl63DfG1Mp8wX97LDaDpy1GmLEUxlg==",
- "hasInstallScript": true,
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/core-js"
- }
- },
- "node_modules/core-util-is": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
- "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
- },
- "node_modules/cosmiconfig": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
- "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
- "dependencies": {
- "@types/parse-json": "^4.0.0",
- "import-fresh": "^3.2.1",
- "parse-json": "^5.0.0",
- "path-type": "^4.0.0",
- "yaml": "^1.10.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/cross-fetch": {
- "version": "3.1.6",
- "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.6.tgz",
- "integrity": "sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g==",
- "dependencies": {
- "node-fetch": "^2.6.11"
- }
- },
- "node_modules/cross-spawn": {
- "version": "7.0.3",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
- "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
- "dependencies": {
- "path-key": "^3.1.0",
- "shebang-command": "^2.0.0",
- "which": "^2.0.1"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/crypto-random-string": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
- "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/css-declaration-sorter": {
- "version": "6.4.0",
- "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.0.tgz",
- "integrity": "sha512-jDfsatwWMWN0MODAFuHszfjphEXfNw9JUAhmY4pLu3TyTU+ohUpsbVtbU+1MZn4a47D9kqh03i4eyOm+74+zew==",
- "engines": {
- "node": "^10 || ^12 || >=14"
- },
- "peerDependencies": {
- "postcss": "^8.0.9"
- }
- },
- "node_modules/css-loader": {
- "version": "6.8.1",
- "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.8.1.tgz",
- "integrity": "sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g==",
- "dependencies": {
- "icss-utils": "^5.1.0",
- "postcss": "^8.4.21",
- "postcss-modules-extract-imports": "^3.0.0",
- "postcss-modules-local-by-default": "^4.0.3",
- "postcss-modules-scope": "^3.0.0",
- "postcss-modules-values": "^4.0.0",
- "postcss-value-parser": "^4.2.0",
- "semver": "^7.3.8"
- },
- "engines": {
- "node": ">= 12.13.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- },
- "peerDependencies": {
- "webpack": "^5.0.0"
- }
- },
- "node_modules/css-minimizer-webpack-plugin": {
- "version": "4.2.2",
- "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-4.2.2.tgz",
- "integrity": "sha512-s3Of/4jKfw1Hj9CxEO1E5oXhQAxlayuHO2y/ML+C6I9sQ7FdzfEV6QgMLN3vI+qFsjJGIAFLKtQK7t8BOXAIyA==",
- "dependencies": {
- "cssnano": "^5.1.8",
- "jest-worker": "^29.1.2",
- "postcss": "^8.4.17",
- "schema-utils": "^4.0.0",
- "serialize-javascript": "^6.0.0",
- "source-map": "^0.6.1"
- },
- "engines": {
- "node": ">= 14.15.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- },
- "peerDependencies": {
- "webpack": "^5.0.0"
- },
- "peerDependenciesMeta": {
- "@parcel/css": {
- "optional": true
- },
- "@swc/css": {
- "optional": true
- },
- "clean-css": {
- "optional": true
- },
- "csso": {
- "optional": true
- },
- "esbuild": {
- "optional": true
- },
- "lightningcss": {
- "optional": true
- }
- }
- },
- "node_modules/css-minimizer-webpack-plugin/node_modules/ajv": {
- "version": "8.12.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
- "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
- "dependencies": {
- "fast-deep-equal": "^3.1.1",
- "json-schema-traverse": "^1.0.0",
- "require-from-string": "^2.0.2",
- "uri-js": "^4.2.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/css-minimizer-webpack-plugin/node_modules/ajv-keywords": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
- "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
- "dependencies": {
- "fast-deep-equal": "^3.1.3"
- },
- "peerDependencies": {
- "ajv": "^8.8.2"
- }
- },
- "node_modules/css-minimizer-webpack-plugin/node_modules/json-schema-traverse": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
- "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
- },
- "node_modules/css-minimizer-webpack-plugin/node_modules/schema-utils": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz",
- "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==",
- "dependencies": {
- "@types/json-schema": "^7.0.9",
- "ajv": "^8.9.0",
- "ajv-formats": "^2.1.1",
- "ajv-keywords": "^5.1.0"
- },
- "engines": {
- "node": ">= 12.13.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- }
- },
- "node_modules/css-select": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
- "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
- "dependencies": {
- "boolbase": "^1.0.0",
- "css-what": "^6.1.0",
- "domhandler": "^5.0.2",
- "domutils": "^3.0.1",
- "nth-check": "^2.0.1"
- },
- "funding": {
- "url": "https://github.com/sponsors/fb55"
- }
- },
- "node_modules/css-tree": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
- "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
- "dependencies": {
- "mdn-data": "2.0.14",
- "source-map": "^0.6.1"
- },
- "engines": {
- "node": ">=8.0.0"
- }
- },
- "node_modules/css-what": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
- "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
- "engines": {
- "node": ">= 6"
- },
- "funding": {
- "url": "https://github.com/sponsors/fb55"
- }
- },
- "node_modules/cssesc": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
- "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
- "bin": {
- "cssesc": "bin/cssesc"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/cssnano": {
- "version": "5.1.15",
- "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.15.tgz",
- "integrity": "sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw==",
- "dependencies": {
- "cssnano-preset-default": "^5.2.14",
- "lilconfig": "^2.0.3",
- "yaml": "^1.10.2"
- },
- "engines": {
- "node": "^10 || ^12 || >=14.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/cssnano"
- },
- "peerDependencies": {
- "postcss": "^8.2.15"
- }
- },
- "node_modules/cssnano-preset-advanced": {
- "version": "5.3.10",
- "resolved": "https://registry.npmjs.org/cssnano-preset-advanced/-/cssnano-preset-advanced-5.3.10.tgz",
- "integrity": "sha512-fnYJyCS9jgMU+cmHO1rPSPf9axbQyD7iUhLO5Df6O4G+fKIOMps+ZbU0PdGFejFBBZ3Pftf18fn1eG7MAPUSWQ==",
- "dependencies": {
- "autoprefixer": "^10.4.12",
- "cssnano-preset-default": "^5.2.14",
- "postcss-discard-unused": "^5.1.0",
- "postcss-merge-idents": "^5.1.1",
- "postcss-reduce-idents": "^5.2.0",
- "postcss-zindex": "^5.1.0"
- },
- "engines": {
- "node": "^10 || ^12 || >=14.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.15"
- }
- },
- "node_modules/cssnano-preset-default": {
- "version": "5.2.14",
- "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.14.tgz",
- "integrity": "sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==",
- "dependencies": {
- "css-declaration-sorter": "^6.3.1",
- "cssnano-utils": "^3.1.0",
- "postcss-calc": "^8.2.3",
- "postcss-colormin": "^5.3.1",
- "postcss-convert-values": "^5.1.3",
- "postcss-discard-comments": "^5.1.2",
- "postcss-discard-duplicates": "^5.1.0",
- "postcss-discard-empty": "^5.1.1",
- "postcss-discard-overridden": "^5.1.0",
- "postcss-merge-longhand": "^5.1.7",
- "postcss-merge-rules": "^5.1.4",
- "postcss-minify-font-values": "^5.1.0",
- "postcss-minify-gradients": "^5.1.1",
- "postcss-minify-params": "^5.1.4",
- "postcss-minify-selectors": "^5.2.1",
- "postcss-normalize-charset": "^5.1.0",
- "postcss-normalize-display-values": "^5.1.0",
- "postcss-normalize-positions": "^5.1.1",
- "postcss-normalize-repeat-style": "^5.1.1",
- "postcss-normalize-string": "^5.1.0",
- "postcss-normalize-timing-functions": "^5.1.0",
- "postcss-normalize-unicode": "^5.1.1",
- "postcss-normalize-url": "^5.1.0",
- "postcss-normalize-whitespace": "^5.1.1",
- "postcss-ordered-values": "^5.1.3",
- "postcss-reduce-initial": "^5.1.2",
- "postcss-reduce-transforms": "^5.1.0",
- "postcss-svgo": "^5.1.0",
- "postcss-unique-selectors": "^5.1.1"
- },
- "engines": {
- "node": "^10 || ^12 || >=14.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.15"
- }
- },
- "node_modules/cssnano-utils": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz",
- "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==",
- "engines": {
- "node": "^10 || ^12 || >=14.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.15"
- }
- },
- "node_modules/csso": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz",
- "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==",
- "dependencies": {
- "css-tree": "^1.1.2"
- },
- "engines": {
- "node": ">=8.0.0"
- }
- },
- "node_modules/csstype": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
- "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
- },
- "node_modules/debug": {
- "version": "4.3.4",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
- "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
- "dependencies": {
- "ms": "2.1.2"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/decompress-response": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz",
- "integrity": "sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==",
- "dependencies": {
- "mimic-response": "^1.0.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/deep-extend": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
- "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
- "engines": {
- "node": ">=4.0.0"
- }
- },
- "node_modules/deepmerge": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
- "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/default-gateway": {
- "version": "6.0.3",
- "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz",
- "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==",
- "dependencies": {
- "execa": "^5.0.0"
- },
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/defer-to-connect": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz",
- "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ=="
- },
- "node_modules/define-lazy-prop": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
- "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/define-properties": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz",
- "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==",
- "dependencies": {
- "has-property-descriptors": "^1.0.0",
- "object-keys": "^1.1.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/del": {
- "version": "6.1.1",
- "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz",
- "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==",
- "dependencies": {
- "globby": "^11.0.1",
- "graceful-fs": "^4.2.4",
- "is-glob": "^4.0.1",
- "is-path-cwd": "^2.2.0",
- "is-path-inside": "^3.0.2",
- "p-map": "^4.0.0",
- "rimraf": "^3.0.2",
- "slash": "^3.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/depd": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
- "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/destroy": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
- "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
- "engines": {
- "node": ">= 0.8",
- "npm": "1.2.8000 || >= 1.4.16"
- }
- },
- "node_modules/detab": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/detab/-/detab-2.0.4.tgz",
- "integrity": "sha512-8zdsQA5bIkoRECvCrNKPla84lyoR7DSAyf7p0YgXzBO9PDJx8KntPUay7NS6yp+KdxdVtiE5SpHKtbp2ZQyA9g==",
- "dependencies": {
- "repeat-string": "^1.5.4"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/detect-node": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
- "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="
- },
- "node_modules/detect-port": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.5.1.tgz",
- "integrity": "sha512-aBzdj76lueB6uUst5iAs7+0H/oOjqI5D16XUWxlWMIMROhcM0rfsNVk93zTngq1dDNpoXRr++Sus7ETAExppAQ==",
- "dependencies": {
- "address": "^1.0.1",
- "debug": "4"
- },
- "bin": {
- "detect": "bin/detect-port.js",
- "detect-port": "bin/detect-port.js"
- }
- },
- "node_modules/detect-port-alt": {
- "version": "1.1.6",
- "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz",
- "integrity": "sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==",
- "dependencies": {
- "address": "^1.0.1",
- "debug": "^2.6.0"
- },
- "bin": {
- "detect": "bin/detect-port",
- "detect-port": "bin/detect-port"
- },
- "engines": {
- "node": ">= 4.2.1"
- }
- },
- "node_modules/detect-port-alt/node_modules/debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "dependencies": {
- "ms": "2.0.0"
- }
- },
- "node_modules/detect-port-alt/node_modules/ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
- },
- "node_modules/dir-glob": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
- "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
- "dependencies": {
- "path-type": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/dns-equal": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz",
- "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg=="
- },
- "node_modules/dns-packet": {
- "version": "5.6.0",
- "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.0.tgz",
- "integrity": "sha512-rza3UH1LwdHh9qyPXp8lkwpjSNk/AMD3dPytUoRoqnypDUhY0xvbdmVhWOfxO68frEfV9BU8V12Ez7ZsHGZpCQ==",
- "dependencies": {
- "@leichtgewicht/ip-codec": "^2.0.1"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/dom-converter": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz",
- "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==",
- "dependencies": {
- "utila": "~0.4"
- }
- },
- "node_modules/dom-serializer": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
- "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
- "dependencies": {
- "domelementtype": "^2.3.0",
- "domhandler": "^5.0.2",
- "entities": "^4.2.0"
- },
- "funding": {
- "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
- }
- },
- "node_modules/domelementtype": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
- "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/fb55"
- }
- ]
- },
- "node_modules/domhandler": {
- "version": "5.0.3",
- "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
- "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
- "dependencies": {
- "domelementtype": "^2.3.0"
- },
- "engines": {
- "node": ">= 4"
- },
- "funding": {
- "url": "https://github.com/fb55/domhandler?sponsor=1"
- }
- },
- "node_modules/domutils": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
- "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
- "dependencies": {
- "dom-serializer": "^2.0.0",
- "domelementtype": "^2.3.0",
- "domhandler": "^5.0.3"
- },
- "funding": {
- "url": "https://github.com/fb55/domutils?sponsor=1"
- }
- },
- "node_modules/dot-case": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
- "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==",
- "dependencies": {
- "no-case": "^3.0.4",
- "tslib": "^2.0.3"
- }
- },
- "node_modules/dot-prop": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz",
- "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==",
- "dependencies": {
- "is-obj": "^2.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/dot-prop/node_modules/is-obj": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
- "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/duplexer": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
- "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="
- },
- "node_modules/duplexer3": {
- "version": "0.1.5",
- "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz",
- "integrity": "sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA=="
- },
- "node_modules/eastasianwidth": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
- "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
- },
- "node_modules/ee-first": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
- "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
- },
- "node_modules/electron-to-chromium": {
- "version": "1.4.434",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.434.tgz",
- "integrity": "sha512-5Gvm09UZTQRaWrimRtWRO5rvaX6Kpk5WHAPKDa7A4Gj6NIPuJ8w8WNpnxCXdd+CJJt6RBU6tUw0KyULoW6XuHw=="
- },
- "node_modules/emoji-regex": {
- "version": "9.2.2",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
- "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
- },
- "node_modules/emojis-list": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
- "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
- "engines": {
- "node": ">= 4"
- }
- },
- "node_modules/emoticon": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/emoticon/-/emoticon-3.2.0.tgz",
- "integrity": "sha512-SNujglcLTTg+lDAcApPNgEdudaqQFiAbJCqzjNxJkvN9vAwCGi0uu8IUVvx+f16h+V44KCY6Y2yboroc9pilHg==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/encodeurl": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
- "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/end-of-stream": {
- "version": "1.4.4",
- "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
- "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
- "dependencies": {
- "once": "^1.4.0"
- }
- },
- "node_modules/enhanced-resolve": {
- "version": "5.15.0",
- "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz",
- "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==",
- "dependencies": {
- "graceful-fs": "^4.2.4",
- "tapable": "^2.2.0"
- },
- "engines": {
- "node": ">=10.13.0"
- }
- },
- "node_modules/entities": {
- "version": "4.5.0",
- "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
- "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
- "engines": {
- "node": ">=0.12"
- },
- "funding": {
- "url": "https://github.com/fb55/entities?sponsor=1"
- }
- },
- "node_modules/error-ex": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
- "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
- "dependencies": {
- "is-arrayish": "^0.2.1"
- }
- },
- "node_modules/es-module-lexer": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.0.tgz",
- "integrity": "sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA=="
- },
- "node_modules/escalade": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
- "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/escape-goat": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz",
- "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/escape-html": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
- "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
- },
- "node_modules/escape-string-regexp": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
- "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/eslint-scope": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
- "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
- "dependencies": {
- "esrecurse": "^4.3.0",
- "estraverse": "^4.1.1"
- },
- "engines": {
- "node": ">=8.0.0"
- }
- },
- "node_modules/esprima": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
- "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
- "bin": {
- "esparse": "bin/esparse.js",
- "esvalidate": "bin/esvalidate.js"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/esrecurse": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
- "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
- "dependencies": {
- "estraverse": "^5.2.0"
- },
- "engines": {
- "node": ">=4.0"
- }
- },
- "node_modules/esrecurse/node_modules/estraverse": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
- "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
- "engines": {
- "node": ">=4.0"
- }
- },
- "node_modules/estraverse": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
- "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
- "engines": {
- "node": ">=4.0"
- }
- },
- "node_modules/esutils": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
- "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/eta": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/eta/-/eta-2.2.0.tgz",
- "integrity": "sha512-UVQ72Rqjy/ZKQalzV5dCCJP80GrmPrMxh6NlNf+erV6ObL0ZFkhCstWRawS85z3smdr3d2wXPsZEY7rDPfGd2g==",
- "engines": {
- "node": ">=6.0.0"
- },
- "funding": {
- "url": "https://github.com/eta-dev/eta?sponsor=1"
- }
- },
- "node_modules/etag": {
- "version": "1.8.1",
- "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
- "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/eval": {
- "version": "0.1.8",
- "resolved": "https://registry.npmjs.org/eval/-/eval-0.1.8.tgz",
- "integrity": "sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==",
- "dependencies": {
- "@types/node": "*",
- "require-like": ">= 0.1.1"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/eventemitter3": {
- "version": "4.0.7",
- "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
- "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
- },
- "node_modules/events": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
- "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
- "engines": {
- "node": ">=0.8.x"
- }
- },
- "node_modules/execa": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
- "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
- "dependencies": {
- "cross-spawn": "^7.0.3",
- "get-stream": "^6.0.0",
- "human-signals": "^2.1.0",
- "is-stream": "^2.0.0",
- "merge-stream": "^2.0.0",
- "npm-run-path": "^4.0.1",
- "onetime": "^5.1.2",
- "signal-exit": "^3.0.3",
- "strip-final-newline": "^2.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sindresorhus/execa?sponsor=1"
- }
- },
- "node_modules/execa/node_modules/get-stream": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
- "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/express": {
- "version": "4.18.2",
- "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
- "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
- "dependencies": {
- "accepts": "~1.3.8",
- "array-flatten": "1.1.1",
- "body-parser": "1.20.1",
- "content-disposition": "0.5.4",
- "content-type": "~1.0.4",
- "cookie": "0.5.0",
- "cookie-signature": "1.0.6",
- "debug": "2.6.9",
- "depd": "2.0.0",
- "encodeurl": "~1.0.2",
- "escape-html": "~1.0.3",
- "etag": "~1.8.1",
- "finalhandler": "1.2.0",
- "fresh": "0.5.2",
- "http-errors": "2.0.0",
- "merge-descriptors": "1.0.1",
- "methods": "~1.1.2",
- "on-finished": "2.4.1",
- "parseurl": "~1.3.3",
- "path-to-regexp": "0.1.7",
- "proxy-addr": "~2.0.7",
- "qs": "6.11.0",
- "range-parser": "~1.2.1",
- "safe-buffer": "5.2.1",
- "send": "0.18.0",
- "serve-static": "1.15.0",
- "setprototypeof": "1.2.0",
- "statuses": "2.0.1",
- "type-is": "~1.6.18",
- "utils-merge": "1.0.1",
- "vary": "~1.1.2"
- },
- "engines": {
- "node": ">= 0.10.0"
- }
- },
- "node_modules/express/node_modules/array-flatten": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
- "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
- },
- "node_modules/express/node_modules/content-disposition": {
- "version": "0.5.4",
- "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
- "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
- "dependencies": {
- "safe-buffer": "5.2.1"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/express/node_modules/debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "dependencies": {
- "ms": "2.0.0"
- }
- },
- "node_modules/express/node_modules/ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
- },
- "node_modules/express/node_modules/path-to-regexp": {
- "version": "0.1.7",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
- "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
- },
- "node_modules/express/node_modules/range-parser": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
- "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/extend": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
- "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
- },
- "node_modules/extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
- "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
- "dependencies": {
- "is-extendable": "^0.1.0"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/fast-deep-equal": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
- "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
- },
- "node_modules/fast-glob": {
- "version": "3.2.12",
- "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
- "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==",
- "dependencies": {
- "@nodelib/fs.stat": "^2.0.2",
- "@nodelib/fs.walk": "^1.2.3",
- "glob-parent": "^5.1.2",
- "merge2": "^1.3.0",
- "micromatch": "^4.0.4"
- },
- "engines": {
- "node": ">=8.6.0"
- }
- },
- "node_modules/fast-json-stable-stringify": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
- "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
- },
- "node_modules/fast-url-parser": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz",
- "integrity": "sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==",
- "dependencies": {
- "punycode": "^1.3.2"
- }
- },
- "node_modules/fastq": {
- "version": "1.15.0",
- "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
- "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
- "dependencies": {
- "reusify": "^1.0.4"
- }
- },
- "node_modules/faye-websocket": {
- "version": "0.11.4",
- "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz",
- "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==",
- "dependencies": {
- "websocket-driver": ">=0.5.1"
- },
- "engines": {
- "node": ">=0.8.0"
- }
- },
- "node_modules/fbemitter": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/fbemitter/-/fbemitter-3.0.0.tgz",
- "integrity": "sha512-KWKaceCwKQU0+HPoop6gn4eOHk50bBv/VxjJtGMfwmJt3D29JpN4H4eisCtIPA+a8GVBam+ldMMpMjJUvpDyHw==",
- "dependencies": {
- "fbjs": "^3.0.0"
- }
- },
- "node_modules/fbjs": {
- "version": "3.0.5",
- "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.5.tgz",
- "integrity": "sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==",
- "dependencies": {
- "cross-fetch": "^3.1.5",
- "fbjs-css-vars": "^1.0.0",
- "loose-envify": "^1.0.0",
- "object-assign": "^4.1.0",
- "promise": "^7.1.1",
- "setimmediate": "^1.0.5",
- "ua-parser-js": "^1.0.35"
- }
- },
- "node_modules/fbjs-css-vars": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz",
- "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ=="
- },
- "node_modules/feed": {
- "version": "4.2.2",
- "resolved": "https://registry.npmjs.org/feed/-/feed-4.2.2.tgz",
- "integrity": "sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==",
- "dependencies": {
- "xml-js": "^1.6.11"
- },
- "engines": {
- "node": ">=0.4.0"
- }
- },
- "node_modules/file-loader": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz",
- "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==",
- "dependencies": {
- "loader-utils": "^2.0.0",
- "schema-utils": "^3.0.0"
- },
- "engines": {
- "node": ">= 10.13.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- },
- "peerDependencies": {
- "webpack": "^4.0.0 || ^5.0.0"
- }
- },
- "node_modules/file-loader/node_modules/schema-utils": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
- "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
- "dependencies": {
- "@types/json-schema": "^7.0.8",
- "ajv": "^6.12.5",
- "ajv-keywords": "^3.5.2"
- },
- "engines": {
- "node": ">= 10.13.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- }
- },
- "node_modules/filesize": {
- "version": "8.0.7",
- "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz",
- "integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==",
- "engines": {
- "node": ">= 0.4.0"
- }
- },
- "node_modules/fill-range": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
- "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
- "dependencies": {
- "to-regex-range": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/finalhandler": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
- "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
- "dependencies": {
- "debug": "2.6.9",
- "encodeurl": "~1.0.2",
- "escape-html": "~1.0.3",
- "on-finished": "2.4.1",
- "parseurl": "~1.3.3",
- "statuses": "2.0.1",
- "unpipe": "~1.0.0"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/finalhandler/node_modules/debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "dependencies": {
- "ms": "2.0.0"
- }
- },
- "node_modules/finalhandler/node_modules/ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
- },
- "node_modules/find-cache-dir": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz",
- "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==",
- "dependencies": {
- "commondir": "^1.0.1",
- "make-dir": "^3.0.2",
- "pkg-dir": "^4.1.0"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/avajs/find-cache-dir?sponsor=1"
- }
- },
- "node_modules/find-up": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
- "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
- "dependencies": {
- "locate-path": "^5.0.0",
- "path-exists": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/flux": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/flux/-/flux-4.0.4.tgz",
- "integrity": "sha512-NCj3XlayA2UsapRpM7va6wU1+9rE5FIL7qoMcmxWHRzbp0yujihMBm9BBHZ1MDIk5h5o2Bl6eGiCe8rYELAmYw==",
- "dependencies": {
- "fbemitter": "^3.0.0",
- "fbjs": "^3.0.1"
- },
- "peerDependencies": {
- "react": "^15.0.2 || ^16.0.0 || ^17.0.0"
- }
- },
- "node_modules/follow-redirects": {
- "version": "1.15.2",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
- "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
- "funding": [
- {
- "type": "individual",
- "url": "https://github.com/sponsors/RubenVerborgh"
- }
- ],
- "engines": {
- "node": ">=4.0"
- },
- "peerDependenciesMeta": {
- "debug": {
- "optional": true
- }
- }
- },
- "node_modules/fork-ts-checker-webpack-plugin": {
- "version": "6.5.3",
- "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz",
- "integrity": "sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==",
- "dependencies": {
- "@babel/code-frame": "^7.8.3",
- "@types/json-schema": "^7.0.5",
- "chalk": "^4.1.0",
- "chokidar": "^3.4.2",
- "cosmiconfig": "^6.0.0",
- "deepmerge": "^4.2.2",
- "fs-extra": "^9.0.0",
- "glob": "^7.1.6",
- "memfs": "^3.1.2",
- "minimatch": "^3.0.4",
- "schema-utils": "2.7.0",
- "semver": "^7.3.2",
- "tapable": "^1.0.0"
- },
- "engines": {
- "node": ">=10",
- "yarn": ">=1.0.0"
- },
- "peerDependencies": {
- "eslint": ">= 6",
- "typescript": ">= 2.7",
- "vue-template-compiler": "*",
- "webpack": ">= 4"
- },
- "peerDependenciesMeta": {
- "eslint": {
- "optional": true
- },
- "vue-template-compiler": {
- "optional": true
- }
- }
- },
- "node_modules/fork-ts-checker-webpack-plugin/node_modules/cosmiconfig": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz",
- "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==",
- "dependencies": {
- "@types/parse-json": "^4.0.0",
- "import-fresh": "^3.1.0",
- "parse-json": "^5.0.0",
- "path-type": "^4.0.0",
- "yaml": "^1.7.2"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/fork-ts-checker-webpack-plugin/node_modules/fs-extra": {
- "version": "9.1.0",
- "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
- "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
- "dependencies": {
- "at-least-node": "^1.0.0",
- "graceful-fs": "^4.2.0",
- "jsonfile": "^6.0.1",
- "universalify": "^2.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": {
- "version": "2.7.0",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz",
- "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==",
- "dependencies": {
- "@types/json-schema": "^7.0.4",
- "ajv": "^6.12.2",
- "ajv-keywords": "^3.4.1"
- },
- "engines": {
- "node": ">= 8.9.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- }
- },
- "node_modules/fork-ts-checker-webpack-plugin/node_modules/tapable": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz",
- "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/forwarded": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
- "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/fraction.js": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
- "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==",
- "engines": {
- "node": "*"
- },
- "funding": {
- "type": "patreon",
- "url": "https://www.patreon.com/infusion"
- }
- },
- "node_modules/fresh": {
- "version": "0.5.2",
- "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
- "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/fs-extra": {
- "version": "10.1.0",
- "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
- "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
- "dependencies": {
- "graceful-fs": "^4.2.0",
- "jsonfile": "^6.0.1",
- "universalify": "^2.0.0"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/fs-monkey": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.4.tgz",
- "integrity": "sha512-INM/fWAxMICjttnD0DX1rBvinKskj5G1w+oy/pnm9u/tSlnBrzFonJMcalKJ30P8RRsPzKcCG7Q8l0jx5Fh9YQ=="
- },
- "node_modules/fs.realpath": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
- "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
- },
- "node_modules/fsevents": {
- "version": "2.3.2",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
- "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
- "hasInstallScript": true,
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
- }
- },
- "node_modules/function-bind": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
- "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
- },
- "node_modules/gensync": {
- "version": "1.0.0-beta.2",
- "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
- "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/get-intrinsic": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
- "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
- "dependencies": {
- "function-bind": "^1.1.1",
- "has": "^1.0.3",
- "has-proto": "^1.0.1",
- "has-symbols": "^1.0.3"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/get-own-enumerable-property-symbols": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz",
- "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g=="
- },
- "node_modules/get-stream": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
- "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
- "dependencies": {
- "pump": "^3.0.0"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/github-slugger": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.5.0.tgz",
- "integrity": "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw=="
- },
- "node_modules/glob": {
- "version": "7.2.3",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
- "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
- "dependencies": {
- "fs.realpath": "^1.0.0",
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "^3.1.1",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
- },
- "engines": {
- "node": "*"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/glob-parent": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
- "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
- "dependencies": {
- "is-glob": "^4.0.1"
- },
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/glob-to-regexp": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
- "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="
- },
- "node_modules/global-dirs": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz",
- "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==",
- "dependencies": {
- "ini": "2.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/global-dirs/node_modules/ini": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz",
- "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==",
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/global-modules": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz",
- "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==",
- "dependencies": {
- "global-prefix": "^3.0.0"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/global-prefix": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz",
- "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==",
- "dependencies": {
- "ini": "^1.3.5",
- "kind-of": "^6.0.2",
- "which": "^1.3.1"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/global-prefix/node_modules/which": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
- "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
- "dependencies": {
- "isexe": "^2.0.0"
- },
- "bin": {
- "which": "bin/which"
- }
- },
- "node_modules/globals": {
- "version": "11.12.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
- "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/globby": {
- "version": "11.1.0",
- "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
- "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
- "dependencies": {
- "array-union": "^2.1.0",
- "dir-glob": "^3.0.1",
- "fast-glob": "^3.2.9",
- "ignore": "^5.2.0",
- "merge2": "^1.4.1",
- "slash": "^3.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/got": {
- "version": "9.6.0",
- "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz",
- "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==",
- "dependencies": {
- "@sindresorhus/is": "^0.14.0",
- "@szmarczak/http-timer": "^1.1.2",
- "cacheable-request": "^6.0.0",
- "decompress-response": "^3.3.0",
- "duplexer3": "^0.1.4",
- "get-stream": "^4.1.0",
- "lowercase-keys": "^1.0.1",
- "mimic-response": "^1.0.1",
- "p-cancelable": "^1.0.0",
- "to-readable-stream": "^1.0.0",
- "url-parse-lax": "^3.0.0"
- },
- "engines": {
- "node": ">=8.6"
- }
- },
- "node_modules/graceful-fs": {
- "version": "4.2.11",
- "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
- "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
- },
- "node_modules/gray-matter": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
- "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==",
- "dependencies": {
- "js-yaml": "^3.13.1",
- "kind-of": "^6.0.2",
- "section-matter": "^1.0.0",
- "strip-bom-string": "^1.0.0"
- },
- "engines": {
- "node": ">=6.0"
- }
- },
- "node_modules/gray-matter/node_modules/argparse": {
- "version": "1.0.10",
- "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
- "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
- "dependencies": {
- "sprintf-js": "~1.0.2"
- }
- },
- "node_modules/gray-matter/node_modules/js-yaml": {
- "version": "3.14.1",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
- "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
- "dependencies": {
- "argparse": "^1.0.7",
- "esprima": "^4.0.0"
- },
- "bin": {
- "js-yaml": "bin/js-yaml.js"
- }
- },
- "node_modules/gzip-size": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz",
- "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==",
- "dependencies": {
- "duplexer": "^0.1.2"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/handle-thing": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",
- "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg=="
- },
- "node_modules/has": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
- "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
- "dependencies": {
- "function-bind": "^1.1.1"
- },
- "engines": {
- "node": ">= 0.4.0"
- }
- },
- "node_modules/has-flag": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
- "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/has-property-descriptors": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz",
- "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==",
- "dependencies": {
- "get-intrinsic": "^1.1.1"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/has-proto": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
- "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/has-symbols": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
- "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/has-yarn": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz",
- "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/hast-to-hyperscript": {
- "version": "9.0.1",
- "resolved": "https://registry.npmjs.org/hast-to-hyperscript/-/hast-to-hyperscript-9.0.1.tgz",
- "integrity": "sha512-zQgLKqF+O2F72S1aa4y2ivxzSlko3MAvxkwG8ehGmNiqd98BIN3JM1rAJPmplEyLmGLO2QZYJtIneOSZ2YbJuA==",
- "dependencies": {
- "@types/unist": "^2.0.3",
- "comma-separated-tokens": "^1.0.0",
- "property-information": "^5.3.0",
- "space-separated-tokens": "^1.0.0",
- "style-to-object": "^0.3.0",
- "unist-util-is": "^4.0.0",
- "web-namespaces": "^1.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/hast-util-from-parse5": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-6.0.1.tgz",
- "integrity": "sha512-jeJUWiN5pSxW12Rh01smtVkZgZr33wBokLzKLwinYOUfSzm1Nl/c3GUGebDyOKjdsRgMvoVbV0VpAcpjF4NrJA==",
- "dependencies": {
- "@types/parse5": "^5.0.0",
- "hastscript": "^6.0.0",
- "property-information": "^5.0.0",
- "vfile": "^4.0.0",
- "vfile-location": "^3.2.0",
- "web-namespaces": "^1.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/hast-util-parse-selector": {
- "version": "2.2.5",
- "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz",
- "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/hast-util-raw": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-6.0.1.tgz",
- "integrity": "sha512-ZMuiYA+UF7BXBtsTBNcLBF5HzXzkyE6MLzJnL605LKE8GJylNjGc4jjxazAHUtcwT5/CEt6afRKViYB4X66dig==",
- "dependencies": {
- "@types/hast": "^2.0.0",
- "hast-util-from-parse5": "^6.0.0",
- "hast-util-to-parse5": "^6.0.0",
- "html-void-elements": "^1.0.0",
- "parse5": "^6.0.0",
- "unist-util-position": "^3.0.0",
- "vfile": "^4.0.0",
- "web-namespaces": "^1.0.0",
- "xtend": "^4.0.0",
- "zwitch": "^1.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/hast-util-raw/node_modules/parse5": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
- "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="
- },
- "node_modules/hast-util-to-parse5": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-6.0.0.tgz",
- "integrity": "sha512-Lu5m6Lgm/fWuz8eWnrKezHtVY83JeRGaNQ2kn9aJgqaxvVkFCZQBEhgodZUDUvoodgyROHDb3r5IxAEdl6suJQ==",
- "dependencies": {
- "hast-to-hyperscript": "^9.0.0",
- "property-information": "^5.0.0",
- "web-namespaces": "^1.0.0",
- "xtend": "^4.0.0",
- "zwitch": "^1.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/hastscript": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz",
- "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==",
- "dependencies": {
- "@types/hast": "^2.0.0",
- "comma-separated-tokens": "^1.0.0",
- "hast-util-parse-selector": "^2.0.0",
- "property-information": "^5.0.0",
- "space-separated-tokens": "^1.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/he": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
- "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
- "bin": {
- "he": "bin/he"
- }
- },
- "node_modules/history": {
- "version": "4.10.1",
- "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
- "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
- "dependencies": {
- "@babel/runtime": "^7.1.2",
- "loose-envify": "^1.2.0",
- "resolve-pathname": "^3.0.0",
- "tiny-invariant": "^1.0.2",
- "tiny-warning": "^1.0.0",
- "value-equal": "^1.0.1"
- }
- },
- "node_modules/hoist-non-react-statics": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
- "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
- "dependencies": {
- "react-is": "^16.7.0"
- }
- },
- "node_modules/hpack.js": {
- "version": "2.1.6",
- "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz",
- "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==",
- "dependencies": {
- "inherits": "^2.0.1",
- "obuf": "^1.0.0",
- "readable-stream": "^2.0.1",
- "wbuf": "^1.1.0"
- }
- },
- "node_modules/hpack.js/node_modules/isarray": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
- "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
- },
- "node_modules/hpack.js/node_modules/readable-stream": {
- "version": "2.3.8",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
- "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
- "dependencies": {
- "core-util-is": "~1.0.0",
- "inherits": "~2.0.3",
- "isarray": "~1.0.0",
- "process-nextick-args": "~2.0.0",
- "safe-buffer": "~5.1.1",
- "string_decoder": "~1.1.1",
- "util-deprecate": "~1.0.1"
- }
- },
- "node_modules/hpack.js/node_modules/safe-buffer": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
- "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
- },
- "node_modules/hpack.js/node_modules/string_decoder": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
- "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
- "dependencies": {
- "safe-buffer": "~5.1.0"
- }
- },
- "node_modules/html-entities": {
- "version": "2.3.6",
- "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.6.tgz",
- "integrity": "sha512-9o0+dcpIw2/HxkNuYKxSJUF/MMRZQECK4GnF+oQOmJ83yCVHTWgCH5aOXxK5bozNRmM8wtgryjHD3uloPBDEGw==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/mdevils"
- },
- {
- "type": "patreon",
- "url": "https://patreon.com/mdevils"
- }
- ]
- },
- "node_modules/html-minifier-terser": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
- "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==",
- "dependencies": {
- "camel-case": "^4.1.2",
- "clean-css": "^5.2.2",
- "commander": "^8.3.0",
- "he": "^1.2.0",
- "param-case": "^3.0.4",
- "relateurl": "^0.2.7",
- "terser": "^5.10.0"
- },
- "bin": {
- "html-minifier-terser": "cli.js"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/html-minifier-terser/node_modules/commander": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
- "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
- "engines": {
- "node": ">= 12"
- }
- },
- "node_modules/html-tags": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz",
- "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==",
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/html-void-elements": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-1.0.5.tgz",
- "integrity": "sha512-uE/TxKuyNIcx44cIWnjr/rfIATDH7ZaOMmstu0CwhFG1Dunhlp4OC6/NMbhiwoq5BpW0ubi303qnEk/PZj614w==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/html-webpack-plugin": {
- "version": "5.5.3",
- "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.3.tgz",
- "integrity": "sha512-6YrDKTuqaP/TquFH7h4srYWsZx+x6k6+FbsTm0ziCwGHDP78Unr1r9F/H4+sGmMbX08GQcJ+K64x55b+7VM/jg==",
- "dependencies": {
- "@types/html-minifier-terser": "^6.0.0",
- "html-minifier-terser": "^6.0.2",
- "lodash": "^4.17.21",
- "pretty-error": "^4.0.0",
- "tapable": "^2.0.0"
- },
- "engines": {
- "node": ">=10.13.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/html-webpack-plugin"
- },
- "peerDependencies": {
- "webpack": "^5.20.0"
- }
- },
- "node_modules/htmlparser2": {
- "version": "8.0.2",
- "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
- "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
- "funding": [
- "https://github.com/fb55/htmlparser2?sponsor=1",
- {
- "type": "github",
- "url": "https://github.com/sponsors/fb55"
- }
- ],
- "dependencies": {
- "domelementtype": "^2.3.0",
- "domhandler": "^5.0.3",
- "domutils": "^3.0.1",
- "entities": "^4.4.0"
- }
- },
- "node_modules/http-cache-semantics": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
- "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="
- },
- "node_modules/http-deceiver": {
- "version": "1.2.7",
- "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz",
- "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw=="
- },
- "node_modules/http-errors": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
- "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
- "dependencies": {
- "depd": "2.0.0",
- "inherits": "2.0.4",
- "setprototypeof": "1.2.0",
- "statuses": "2.0.1",
- "toidentifier": "1.0.1"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/http-parser-js": {
- "version": "0.5.8",
- "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz",
- "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q=="
- },
- "node_modules/http-proxy": {
- "version": "1.18.1",
- "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
- "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
- "dependencies": {
- "eventemitter3": "^4.0.0",
- "follow-redirects": "^1.0.0",
- "requires-port": "^1.0.0"
- },
- "engines": {
- "node": ">=8.0.0"
- }
- },
- "node_modules/http-proxy-middleware": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz",
- "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==",
- "dependencies": {
- "@types/http-proxy": "^1.17.8",
- "http-proxy": "^1.18.1",
- "is-glob": "^4.0.1",
- "is-plain-obj": "^3.0.0",
- "micromatch": "^4.0.2"
- },
- "engines": {
- "node": ">=12.0.0"
- },
- "peerDependencies": {
- "@types/express": "^4.17.13"
- },
- "peerDependenciesMeta": {
- "@types/express": {
- "optional": true
- }
- }
- },
- "node_modules/http-proxy-middleware/node_modules/is-plain-obj": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz",
- "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/human-signals": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
- "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
- "engines": {
- "node": ">=10.17.0"
- }
- },
- "node_modules/iconv-lite": {
- "version": "0.4.24",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
- "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
- "dependencies": {
- "safer-buffer": ">= 2.1.2 < 3"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/icss-utils": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
- "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==",
- "engines": {
- "node": "^10 || ^12 || >= 14"
- },
- "peerDependencies": {
- "postcss": "^8.1.0"
- }
- },
- "node_modules/ignore": {
- "version": "5.2.4",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
- "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
- "engines": {
- "node": ">= 4"
- }
- },
- "node_modules/image-size": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.0.2.tgz",
- "integrity": "sha512-xfOoWjceHntRb3qFCrh5ZFORYH8XCdYpASltMhZ/Q0KZiOwjdE/Yl2QCiWdwD+lygV5bMCvauzgu5PxBX/Yerg==",
- "dependencies": {
- "queue": "6.0.2"
- },
- "bin": {
- "image-size": "bin/image-size.js"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/immer": {
- "version": "9.0.21",
- "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
- "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/immer"
- }
- },
- "node_modules/import-fresh": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
- "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
- "dependencies": {
- "parent-module": "^1.0.0",
- "resolve-from": "^4.0.0"
- },
- "engines": {
- "node": ">=6"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/import-lazy": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz",
- "integrity": "sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A==",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/imurmurhash": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
- "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
- "engines": {
- "node": ">=0.8.19"
- }
- },
- "node_modules/indent-string": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
- "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/infima": {
- "version": "0.2.0-alpha.43",
- "resolved": "https://registry.npmjs.org/infima/-/infima-0.2.0-alpha.43.tgz",
- "integrity": "sha512-2uw57LvUqW0rK/SWYnd/2rRfxNA5DDNOh33jxF7fy46VWoNhGxiUQyVZHbBMjQ33mQem0cjdDVwgWVAmlRfgyQ==",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/inflight": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
- "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
- "dependencies": {
- "once": "^1.3.0",
- "wrappy": "1"
- }
- },
- "node_modules/inherits": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
- },
- "node_modules/ini": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
- "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
- },
- "node_modules/inline-style-parser": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz",
- "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q=="
- },
- "node_modules/interpret": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz",
- "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==",
- "engines": {
- "node": ">= 0.10"
- }
- },
- "node_modules/invariant": {
- "version": "2.2.4",
- "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
- "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
- "dependencies": {
- "loose-envify": "^1.0.0"
- }
- },
- "node_modules/ipaddr.js": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz",
- "integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==",
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/is-alphabetical": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz",
- "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/is-alphanumerical": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz",
- "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==",
- "dependencies": {
- "is-alphabetical": "^1.0.0",
- "is-decimal": "^1.0.0"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/is-arrayish": {
- "version": "0.2.1",
- "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
- "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="
- },
- "node_modules/is-binary-path": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
- "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
- "dependencies": {
- "binary-extensions": "^2.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/is-buffer": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
- "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/is-ci": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz",
- "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==",
- "dependencies": {
- "ci-info": "^2.0.0"
- },
- "bin": {
- "is-ci": "bin.js"
- }
- },
- "node_modules/is-ci/node_modules/ci-info": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz",
- "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="
- },
- "node_modules/is-core-module": {
- "version": "2.12.1",
- "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz",
- "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==",
- "dependencies": {
- "has": "^1.0.3"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-decimal": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz",
- "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/is-docker": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
- "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
- "bin": {
- "is-docker": "cli.js"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/is-extendable": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
- "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/is-extglob": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
- "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/is-fullwidth-code-point": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
- "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/is-glob": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
- "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
- "dependencies": {
- "is-extglob": "^2.1.1"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/is-hexadecimal": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz",
- "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/is-installed-globally": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz",
- "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==",
- "dependencies": {
- "global-dirs": "^3.0.0",
- "is-path-inside": "^3.0.2"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/is-npm": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz",
- "integrity": "sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/is-number": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
- "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
- "engines": {
- "node": ">=0.12.0"
- }
- },
- "node_modules/is-obj": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
- "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/is-path-cwd": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz",
- "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/is-path-inside": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
- "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/is-plain-obj": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
- "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/is-plain-object": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
- "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
- "dependencies": {
- "isobject": "^3.0.1"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/is-regexp": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz",
- "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/is-root": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz",
- "integrity": "sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/is-stream": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
- "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/is-typedarray": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
- "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="
- },
- "node_modules/is-whitespace-character": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz",
- "integrity": "sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/is-word-character": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/is-word-character/-/is-word-character-1.0.4.tgz",
- "integrity": "sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/is-wsl": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
- "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
- "dependencies": {
- "is-docker": "^2.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/is-yarn-global": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz",
- "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw=="
- },
- "node_modules/isarray": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
- "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="
- },
- "node_modules/isexe": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
- },
- "node_modules/isobject": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
- "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/jest-util": {
- "version": "29.5.0",
- "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.5.0.tgz",
- "integrity": "sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==",
- "dependencies": {
- "@jest/types": "^29.5.0",
- "@types/node": "*",
- "chalk": "^4.0.0",
- "ci-info": "^3.2.0",
- "graceful-fs": "^4.2.9",
- "picomatch": "^2.2.3"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/jest-worker": {
- "version": "29.5.0",
- "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.5.0.tgz",
- "integrity": "sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==",
- "dependencies": {
- "@types/node": "*",
- "jest-util": "^29.5.0",
- "merge-stream": "^2.0.0",
- "supports-color": "^8.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "node_modules/jest-worker/node_modules/supports-color": {
- "version": "8.1.1",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
- "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/supports-color?sponsor=1"
- }
- },
- "node_modules/jiti": {
- "version": "1.18.2",
- "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz",
- "integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==",
- "bin": {
- "jiti": "bin/jiti.js"
- }
- },
- "node_modules/joi": {
- "version": "17.9.2",
- "resolved": "https://registry.npmjs.org/joi/-/joi-17.9.2.tgz",
- "integrity": "sha512-Itk/r+V4Dx0V3c7RLFdRh12IOjySm2/WGPMubBT92cQvRfYZhPM2W0hZlctjj72iES8jsRCwp7S/cRmWBnJ4nw==",
- "dependencies": {
- "@hapi/hoek": "^9.0.0",
- "@hapi/topo": "^5.0.0",
- "@sideway/address": "^4.1.3",
- "@sideway/formula": "^3.0.1",
- "@sideway/pinpoint": "^2.0.0"
- }
- },
- "node_modules/js-tokens": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
- },
- "node_modules/js-yaml": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
- "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
- "dependencies": {
- "argparse": "^2.0.1"
- },
- "bin": {
- "js-yaml": "bin/js-yaml.js"
- }
- },
- "node_modules/jsesc": {
- "version": "2.5.2",
- "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
- "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
- "bin": {
- "jsesc": "bin/jsesc"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/json-buffer": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz",
- "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ=="
- },
- "node_modules/json-parse-even-better-errors": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
- "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
- },
- "node_modules/json-schema-traverse": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
- },
- "node_modules/json5": {
- "version": "2.2.3",
- "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
- "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
- "bin": {
- "json5": "lib/cli.js"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/jsonfile": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
- "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
- "dependencies": {
- "universalify": "^2.0.0"
- },
- "optionalDependencies": {
- "graceful-fs": "^4.1.6"
- }
- },
- "node_modules/keyv": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz",
- "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==",
- "dependencies": {
- "json-buffer": "3.0.0"
- }
- },
- "node_modules/kind-of": {
- "version": "6.0.3",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
- "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/kleur": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
- "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/latest-version": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz",
- "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==",
- "dependencies": {
- "package-json": "^6.3.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/launch-editor": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.0.tgz",
- "integrity": "sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ==",
- "dependencies": {
- "picocolors": "^1.0.0",
- "shell-quote": "^1.7.3"
- }
- },
- "node_modules/leven": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
- "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/lilconfig": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
- "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/lines-and-columns": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
- "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
- },
- "node_modules/loader-runner": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
- "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==",
- "engines": {
- "node": ">=6.11.5"
- }
- },
- "node_modules/loader-utils": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
- "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
- "dependencies": {
- "big.js": "^5.2.2",
- "emojis-list": "^3.0.0",
- "json5": "^2.1.2"
- },
- "engines": {
- "node": ">=8.9.0"
- }
- },
- "node_modules/locate-path": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
- "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
- "dependencies": {
- "p-locate": "^4.1.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/lodash": {
- "version": "4.17.21",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
- "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
- },
- "node_modules/lodash.curry": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/lodash.curry/-/lodash.curry-4.1.1.tgz",
- "integrity": "sha512-/u14pXGviLaweY5JI0IUzgzF2J6Ne8INyzAZjImcryjgkZ+ebruBxy2/JaOOkTqScddcYtakjhSaeemV8lR0tA=="
- },
- "node_modules/lodash.debounce": {
- "version": "4.0.8",
- "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
- "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="
- },
- "node_modules/lodash.flow": {
- "version": "3.5.0",
- "resolved": "https://registry.npmjs.org/lodash.flow/-/lodash.flow-3.5.0.tgz",
- "integrity": "sha512-ff3BX/tSioo+XojX4MOsOMhJw0nZoUEF011LX8g8d3gvjVbxd89cCio4BCXronjxcTUIJUoqKEUA+n4CqvvRPw=="
- },
- "node_modules/lodash.memoize": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
- "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag=="
- },
- "node_modules/lodash.uniq": {
- "version": "4.5.0",
- "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
- "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ=="
- },
- "node_modules/loose-envify": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
- "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
- "dependencies": {
- "js-tokens": "^3.0.0 || ^4.0.0"
- },
- "bin": {
- "loose-envify": "cli.js"
- }
- },
- "node_modules/lower-case": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
- "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==",
- "dependencies": {
- "tslib": "^2.0.3"
- }
- },
- "node_modules/lowercase-keys": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz",
- "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/lru-cache": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
- "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
- "dependencies": {
- "yallist": "^3.0.2"
- }
- },
- "node_modules/make-dir": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
- "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
- "dependencies": {
- "semver": "^6.0.0"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/make-dir/node_modules/semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
- "node_modules/markdown-escapes": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.4.tgz",
- "integrity": "sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/mdast-squeeze-paragraphs": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/mdast-squeeze-paragraphs/-/mdast-squeeze-paragraphs-4.0.0.tgz",
- "integrity": "sha512-zxdPn69hkQ1rm4J+2Cs2j6wDEv7O17TfXTJ33tl/+JPIoEmtV9t2ZzBM5LPHE8QlHsmVD8t3vPKCyY3oH+H8MQ==",
- "dependencies": {
- "unist-util-remove": "^2.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/mdast-util-definitions": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz",
- "integrity": "sha512-k8AJ6aNnUkB7IE+5azR9h81O5EQ/cTDXtWdMq9Kk5KcEW/8ritU5CeLg/9HhOC++nALHBlaogJ5jz0Ybk3kPMQ==",
- "dependencies": {
- "unist-util-visit": "^2.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/mdast-util-to-hast": {
- "version": "10.0.1",
- "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-10.0.1.tgz",
- "integrity": "sha512-BW3LM9SEMnjf4HXXVApZMt8gLQWVNXc3jryK0nJu/rOXPOnlkUjmdkDlmxMirpbU9ILncGFIwLH/ubnWBbcdgA==",
- "dependencies": {
- "@types/mdast": "^3.0.0",
- "@types/unist": "^2.0.0",
- "mdast-util-definitions": "^4.0.0",
- "mdurl": "^1.0.0",
- "unist-builder": "^2.0.0",
- "unist-util-generated": "^1.0.0",
- "unist-util-position": "^3.0.0",
- "unist-util-visit": "^2.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/mdast-util-to-string": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz",
- "integrity": "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/mdn-data": {
- "version": "2.0.14",
- "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
- "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow=="
- },
- "node_modules/mdurl": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
- "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g=="
- },
- "node_modules/media-typer": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
- "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/memfs": {
- "version": "3.5.3",
- "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz",
- "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==",
- "dependencies": {
- "fs-monkey": "^1.0.4"
- },
- "engines": {
- "node": ">= 4.0.0"
- }
- },
- "node_modules/merge-descriptors": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
- "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
- },
- "node_modules/merge-stream": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
- "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="
- },
- "node_modules/merge2": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
- "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/methods": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
- "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/micromatch": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
- "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
- "dependencies": {
- "braces": "^3.0.2",
- "picomatch": "^2.3.1"
- },
- "engines": {
- "node": ">=8.6"
- }
- },
- "node_modules/mime": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
- "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
- "bin": {
- "mime": "cli.js"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/mime-db": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
- "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/mime-types": {
- "version": "2.1.18",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz",
- "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==",
- "dependencies": {
- "mime-db": "~1.33.0"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/mimic-fn": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
- "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/mimic-response": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
- "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/mini-css-extract-plugin": {
- "version": "2.7.6",
- "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.6.tgz",
- "integrity": "sha512-Qk7HcgaPkGG6eD77mLvZS1nmxlao3j+9PkrT9Uc7HAE1id3F41+DdBRYRYkbyfNRGzm8/YWtzhw7nVPmwhqTQw==",
- "dependencies": {
- "schema-utils": "^4.0.0"
- },
- "engines": {
- "node": ">= 12.13.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- },
- "peerDependencies": {
- "webpack": "^5.0.0"
- }
- },
- "node_modules/mini-css-extract-plugin/node_modules/ajv": {
- "version": "8.12.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
- "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
- "dependencies": {
- "fast-deep-equal": "^3.1.1",
- "json-schema-traverse": "^1.0.0",
- "require-from-string": "^2.0.2",
- "uri-js": "^4.2.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/mini-css-extract-plugin/node_modules/ajv-keywords": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
- "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
- "dependencies": {
- "fast-deep-equal": "^3.1.3"
- },
- "peerDependencies": {
- "ajv": "^8.8.2"
- }
- },
- "node_modules/mini-css-extract-plugin/node_modules/json-schema-traverse": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
- "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
- },
- "node_modules/mini-css-extract-plugin/node_modules/schema-utils": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz",
- "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==",
- "dependencies": {
- "@types/json-schema": "^7.0.9",
- "ajv": "^8.9.0",
- "ajv-formats": "^2.1.1",
- "ajv-keywords": "^5.1.0"
- },
- "engines": {
- "node": ">= 12.13.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- }
- },
- "node_modules/minimalistic-assert": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
- "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
- },
- "node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "dependencies": {
- "brace-expansion": "^1.1.7"
- },
- "engines": {
- "node": "*"
- }
- },
- "node_modules/minimist": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
- "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/mrmime": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz",
- "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==",
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
- },
- "node_modules/multicast-dns": {
- "version": "7.2.5",
- "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz",
- "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==",
- "dependencies": {
- "dns-packet": "^5.2.2",
- "thunky": "^1.0.2"
- },
- "bin": {
- "multicast-dns": "cli.js"
- }
- },
- "node_modules/nanoid": {
- "version": "3.3.6",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
- "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "bin": {
- "nanoid": "bin/nanoid.cjs"
- },
- "engines": {
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
- }
- },
- "node_modules/negotiator": {
- "version": "0.6.3",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
- "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/neo-async": {
- "version": "2.6.2",
- "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
- "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
- },
- "node_modules/no-case": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
- "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==",
- "dependencies": {
- "lower-case": "^2.0.2",
- "tslib": "^2.0.3"
- }
- },
- "node_modules/node-emoji": {
- "version": "1.11.0",
- "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz",
- "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==",
- "dependencies": {
- "lodash": "^4.17.21"
- }
- },
- "node_modules/node-fetch": {
- "version": "2.6.11",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz",
- "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==",
- "dependencies": {
- "whatwg-url": "^5.0.0"
- },
- "engines": {
- "node": "4.x || >=6.0.0"
- },
- "peerDependencies": {
- "encoding": "^0.1.0"
- },
- "peerDependenciesMeta": {
- "encoding": {
- "optional": true
- }
- }
- },
- "node_modules/node-forge": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
- "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
- "engines": {
- "node": ">= 6.13.0"
- }
- },
- "node_modules/node-releases": {
- "version": "2.0.12",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.12.tgz",
- "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ=="
- },
- "node_modules/normalize-path": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
- "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/normalize-range": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
- "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/normalize-url": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
- "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/npm-run-path": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
- "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
- "dependencies": {
- "path-key": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/nprogress": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz",
- "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA=="
- },
- "node_modules/nth-check": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
- "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
- "dependencies": {
- "boolbase": "^1.0.0"
- },
- "funding": {
- "url": "https://github.com/fb55/nth-check?sponsor=1"
- }
- },
- "node_modules/object-assign": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
- "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/object-inspect": {
- "version": "1.12.3",
- "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
- "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/object-keys": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
- "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/object.assign": {
- "version": "4.1.4",
- "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz",
- "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==",
- "dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.1.4",
- "has-symbols": "^1.0.3",
- "object-keys": "^1.1.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/obuf": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
- "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="
- },
- "node_modules/on-finished": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
- "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
- "dependencies": {
- "ee-first": "1.1.1"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/on-headers": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
- "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/once": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
- "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
- "dependencies": {
- "wrappy": "1"
- }
- },
- "node_modules/onetime": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
- "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
- "dependencies": {
- "mimic-fn": "^2.1.0"
- },
- "engines": {
- "node": ">=6"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/open": {
- "version": "8.4.2",
- "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
- "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==",
- "dependencies": {
- "define-lazy-prop": "^2.0.0",
- "is-docker": "^2.1.1",
- "is-wsl": "^2.2.0"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/opener": {
- "version": "1.5.2",
- "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
- "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
- "bin": {
- "opener": "bin/opener-bin.js"
- }
- },
- "node_modules/p-cancelable": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz",
- "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/p-limit": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
- "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
- "dependencies": {
- "p-try": "^2.0.0"
- },
- "engines": {
- "node": ">=6"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/p-locate": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
- "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
- "dependencies": {
- "p-limit": "^2.2.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/p-map": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
- "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
- "dependencies": {
- "aggregate-error": "^3.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/p-retry": {
- "version": "4.6.2",
- "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
- "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==",
- "dependencies": {
- "@types/retry": "0.12.0",
- "retry": "^0.13.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/p-try": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
- "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/package-json": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz",
- "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==",
- "dependencies": {
- "got": "^9.6.0",
- "registry-auth-token": "^4.0.0",
- "registry-url": "^5.0.0",
- "semver": "^6.2.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/package-json/node_modules/semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
- "node_modules/param-case": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
- "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==",
- "dependencies": {
- "dot-case": "^3.0.4",
- "tslib": "^2.0.3"
- }
- },
- "node_modules/parent-module": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
- "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
- "dependencies": {
- "callsites": "^3.0.0"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/parse-entities": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz",
- "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==",
- "dependencies": {
- "character-entities": "^1.0.0",
- "character-entities-legacy": "^1.0.0",
- "character-reference-invalid": "^1.0.0",
- "is-alphanumerical": "^1.0.0",
- "is-decimal": "^1.0.0",
- "is-hexadecimal": "^1.0.0"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/parse-json": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
- "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
- "dependencies": {
- "@babel/code-frame": "^7.0.0",
- "error-ex": "^1.3.1",
- "json-parse-even-better-errors": "^2.3.0",
- "lines-and-columns": "^1.1.6"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/parse-numeric-range": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz",
- "integrity": "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ=="
- },
- "node_modules/parse5": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
- "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==",
- "dependencies": {
- "entities": "^4.4.0"
- },
- "funding": {
- "url": "https://github.com/inikulin/parse5?sponsor=1"
- }
- },
- "node_modules/parse5-htmlparser2-tree-adapter": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz",
- "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==",
- "dependencies": {
- "domhandler": "^5.0.2",
- "parse5": "^7.0.0"
- },
- "funding": {
- "url": "https://github.com/inikulin/parse5?sponsor=1"
- }
- },
- "node_modules/parseurl": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
- "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/pascal-case": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz",
- "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==",
- "dependencies": {
- "no-case": "^3.0.4",
- "tslib": "^2.0.3"
- }
- },
- "node_modules/path-exists": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
- "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/path-is-absolute": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
- "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/path-is-inside": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
- "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w=="
- },
- "node_modules/path-key": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
- "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/path-parse": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
- "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
- },
- "node_modules/path-to-regexp": {
- "version": "1.8.0",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
- "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
- "dependencies": {
- "isarray": "0.0.1"
- }
- },
- "node_modules/path-type": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
- "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/picocolors": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
- "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
- },
- "node_modules/picomatch": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
- "engines": {
- "node": ">=8.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/pkg-dir": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
- "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
- "dependencies": {
- "find-up": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/pkg-up": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz",
- "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==",
- "dependencies": {
- "find-up": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/pkg-up/node_modules/find-up": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
- "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
- "dependencies": {
- "locate-path": "^3.0.0"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/pkg-up/node_modules/locate-path": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
- "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
- "dependencies": {
- "p-locate": "^3.0.0",
- "path-exists": "^3.0.0"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/pkg-up/node_modules/p-locate": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
- "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
- "dependencies": {
- "p-limit": "^2.0.0"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/pkg-up/node_modules/path-exists": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
- "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/postcss": {
- "version": "8.4.24",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz",
- "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==",
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/postcss"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "dependencies": {
- "nanoid": "^3.3.6",
- "picocolors": "^1.0.0",
- "source-map-js": "^1.0.2"
- },
- "engines": {
- "node": "^10 || ^12 || >=14"
- }
- },
- "node_modules/postcss-calc": {
- "version": "8.2.4",
- "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz",
- "integrity": "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==",
- "dependencies": {
- "postcss-selector-parser": "^6.0.9",
- "postcss-value-parser": "^4.2.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.2"
- }
- },
- "node_modules/postcss-colormin": {
- "version": "5.3.1",
- "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.3.1.tgz",
- "integrity": "sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==",
- "dependencies": {
- "browserslist": "^4.21.4",
- "caniuse-api": "^3.0.0",
- "colord": "^2.9.1",
- "postcss-value-parser": "^4.2.0"
- },
- "engines": {
- "node": "^10 || ^12 || >=14.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.15"
- }
- },
- "node_modules/postcss-convert-values": {
- "version": "5.1.3",
- "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz",
- "integrity": "sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==",
- "dependencies": {
- "browserslist": "^4.21.4",
- "postcss-value-parser": "^4.2.0"
- },
- "engines": {
- "node": "^10 || ^12 || >=14.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.15"
- }
- },
- "node_modules/postcss-discard-comments": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz",
- "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==",
- "engines": {
- "node": "^10 || ^12 || >=14.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.15"
- }
- },
- "node_modules/postcss-discard-duplicates": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz",
- "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==",
- "engines": {
- "node": "^10 || ^12 || >=14.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.15"
- }
- },
- "node_modules/postcss-discard-empty": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz",
- "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==",
- "engines": {
- "node": "^10 || ^12 || >=14.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.15"
- }
- },
- "node_modules/postcss-discard-overridden": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz",
- "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==",
- "engines": {
- "node": "^10 || ^12 || >=14.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.15"
- }
- },
- "node_modules/postcss-discard-unused": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/postcss-discard-unused/-/postcss-discard-unused-5.1.0.tgz",
- "integrity": "sha512-KwLWymI9hbwXmJa0dkrzpRbSJEh0vVUd7r8t0yOGPcfKzyJJxFM8kLyC5Ev9avji6nY95pOp1W6HqIrfT+0VGw==",
- "dependencies": {
- "postcss-selector-parser": "^6.0.5"
- },
- "engines": {
- "node": "^10 || ^12 || >=14.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.15"
- }
- },
- "node_modules/postcss-loader": {
- "version": "7.3.3",
- "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.3.tgz",
- "integrity": "sha512-YgO/yhtevGO/vJePCQmTxiaEwER94LABZN0ZMT4A0vsak9TpO+RvKRs7EmJ8peIlB9xfXCsS7M8LjqncsUZ5HA==",
- "dependencies": {
- "cosmiconfig": "^8.2.0",
- "jiti": "^1.18.2",
- "semver": "^7.3.8"
- },
- "engines": {
- "node": ">= 14.15.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- },
- "peerDependencies": {
- "postcss": "^7.0.0 || ^8.0.1",
- "webpack": "^5.0.0"
- }
- },
- "node_modules/postcss-loader/node_modules/cosmiconfig": {
- "version": "8.2.0",
- "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.2.0.tgz",
- "integrity": "sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==",
- "dependencies": {
- "import-fresh": "^3.2.1",
- "js-yaml": "^4.1.0",
- "parse-json": "^5.0.0",
- "path-type": "^4.0.0"
- },
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/d-fischer"
- }
- },
- "node_modules/postcss-merge-idents": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/postcss-merge-idents/-/postcss-merge-idents-5.1.1.tgz",
- "integrity": "sha512-pCijL1TREiCoog5nQp7wUe+TUonA2tC2sQ54UGeMmryK3UFGIYKqDyjnqd6RcuI4znFn9hWSLNN8xKE/vWcUQw==",
- "dependencies": {
- "cssnano-utils": "^3.1.0",
- "postcss-value-parser": "^4.2.0"
- },
- "engines": {
- "node": "^10 || ^12 || >=14.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.15"
- }
- },
- "node_modules/postcss-merge-longhand": {
- "version": "5.1.7",
- "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz",
- "integrity": "sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==",
- "dependencies": {
- "postcss-value-parser": "^4.2.0",
- "stylehacks": "^5.1.1"
- },
- "engines": {
- "node": "^10 || ^12 || >=14.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.15"
- }
- },
- "node_modules/postcss-merge-rules": {
- "version": "5.1.4",
- "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz",
- "integrity": "sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==",
- "dependencies": {
- "browserslist": "^4.21.4",
- "caniuse-api": "^3.0.0",
- "cssnano-utils": "^3.1.0",
- "postcss-selector-parser": "^6.0.5"
- },
- "engines": {
- "node": "^10 || ^12 || >=14.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.15"
- }
- },
- "node_modules/postcss-minify-font-values": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz",
- "integrity": "sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==",
- "dependencies": {
- "postcss-value-parser": "^4.2.0"
- },
- "engines": {
- "node": "^10 || ^12 || >=14.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.15"
- }
- },
- "node_modules/postcss-minify-gradients": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz",
- "integrity": "sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==",
- "dependencies": {
- "colord": "^2.9.1",
- "cssnano-utils": "^3.1.0",
- "postcss-value-parser": "^4.2.0"
- },
- "engines": {
- "node": "^10 || ^12 || >=14.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.15"
- }
- },
- "node_modules/postcss-minify-params": {
- "version": "5.1.4",
- "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz",
- "integrity": "sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==",
- "dependencies": {
- "browserslist": "^4.21.4",
- "cssnano-utils": "^3.1.0",
- "postcss-value-parser": "^4.2.0"
- },
- "engines": {
- "node": "^10 || ^12 || >=14.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.15"
- }
- },
- "node_modules/postcss-minify-selectors": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz",
- "integrity": "sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==",
- "dependencies": {
- "postcss-selector-parser": "^6.0.5"
- },
- "engines": {
- "node": "^10 || ^12 || >=14.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.15"
- }
- },
- "node_modules/postcss-modules-extract-imports": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz",
- "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==",
- "engines": {
- "node": "^10 || ^12 || >= 14"
- },
- "peerDependencies": {
- "postcss": "^8.1.0"
- }
- },
- "node_modules/postcss-modules-local-by-default": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz",
- "integrity": "sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==",
- "dependencies": {
- "icss-utils": "^5.0.0",
- "postcss-selector-parser": "^6.0.2",
- "postcss-value-parser": "^4.1.0"
- },
- "engines": {
- "node": "^10 || ^12 || >= 14"
- },
- "peerDependencies": {
- "postcss": "^8.1.0"
- }
- },
- "node_modules/postcss-modules-scope": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz",
- "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==",
- "dependencies": {
- "postcss-selector-parser": "^6.0.4"
- },
- "engines": {
- "node": "^10 || ^12 || >= 14"
- },
- "peerDependencies": {
- "postcss": "^8.1.0"
- }
- },
- "node_modules/postcss-modules-values": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz",
- "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==",
- "dependencies": {
- "icss-utils": "^5.0.0"
- },
- "engines": {
- "node": "^10 || ^12 || >= 14"
- },
- "peerDependencies": {
- "postcss": "^8.1.0"
- }
- },
- "node_modules/postcss-normalize-charset": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz",
- "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==",
- "engines": {
- "node": "^10 || ^12 || >=14.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.15"
- }
- },
- "node_modules/postcss-normalize-display-values": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz",
- "integrity": "sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==",
- "dependencies": {
- "postcss-value-parser": "^4.2.0"
- },
- "engines": {
- "node": "^10 || ^12 || >=14.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.15"
- }
- },
- "node_modules/postcss-normalize-positions": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz",
- "integrity": "sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==",
- "dependencies": {
- "postcss-value-parser": "^4.2.0"
- },
- "engines": {
- "node": "^10 || ^12 || >=14.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.15"
- }
- },
- "node_modules/postcss-normalize-repeat-style": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz",
- "integrity": "sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==",
- "dependencies": {
- "postcss-value-parser": "^4.2.0"
- },
- "engines": {
- "node": "^10 || ^12 || >=14.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.15"
- }
- },
- "node_modules/postcss-normalize-string": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz",
- "integrity": "sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==",
- "dependencies": {
- "postcss-value-parser": "^4.2.0"
- },
- "engines": {
- "node": "^10 || ^12 || >=14.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.15"
- }
- },
- "node_modules/postcss-normalize-timing-functions": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz",
- "integrity": "sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==",
- "dependencies": {
- "postcss-value-parser": "^4.2.0"
- },
- "engines": {
- "node": "^10 || ^12 || >=14.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.15"
- }
- },
- "node_modules/postcss-normalize-unicode": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz",
- "integrity": "sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==",
- "dependencies": {
- "browserslist": "^4.21.4",
- "postcss-value-parser": "^4.2.0"
- },
- "engines": {
- "node": "^10 || ^12 || >=14.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.15"
- }
- },
- "node_modules/postcss-normalize-url": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz",
- "integrity": "sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==",
- "dependencies": {
- "normalize-url": "^6.0.1",
- "postcss-value-parser": "^4.2.0"
- },
- "engines": {
- "node": "^10 || ^12 || >=14.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.15"
- }
- },
- "node_modules/postcss-normalize-whitespace": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz",
- "integrity": "sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==",
- "dependencies": {
- "postcss-value-parser": "^4.2.0"
- },
- "engines": {
- "node": "^10 || ^12 || >=14.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.15"
- }
- },
- "node_modules/postcss-ordered-values": {
- "version": "5.1.3",
- "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz",
- "integrity": "sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==",
- "dependencies": {
- "cssnano-utils": "^3.1.0",
- "postcss-value-parser": "^4.2.0"
- },
- "engines": {
- "node": "^10 || ^12 || >=14.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.15"
- }
- },
- "node_modules/postcss-reduce-idents": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/postcss-reduce-idents/-/postcss-reduce-idents-5.2.0.tgz",
- "integrity": "sha512-BTrLjICoSB6gxbc58D5mdBK8OhXRDqud/zodYfdSi52qvDHdMwk+9kB9xsM8yJThH/sZU5A6QVSmMmaN001gIg==",
- "dependencies": {
- "postcss-value-parser": "^4.2.0"
- },
- "engines": {
- "node": "^10 || ^12 || >=14.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.15"
- }
- },
- "node_modules/postcss-reduce-initial": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz",
- "integrity": "sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==",
- "dependencies": {
- "browserslist": "^4.21.4",
- "caniuse-api": "^3.0.0"
- },
- "engines": {
- "node": "^10 || ^12 || >=14.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.15"
- }
- },
- "node_modules/postcss-reduce-transforms": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz",
- "integrity": "sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==",
- "dependencies": {
- "postcss-value-parser": "^4.2.0"
- },
- "engines": {
- "node": "^10 || ^12 || >=14.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.15"
- }
- },
- "node_modules/postcss-selector-parser": {
- "version": "6.0.13",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz",
- "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==",
- "dependencies": {
- "cssesc": "^3.0.0",
- "util-deprecate": "^1.0.2"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/postcss-sort-media-queries": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/postcss-sort-media-queries/-/postcss-sort-media-queries-4.4.1.tgz",
- "integrity": "sha512-QDESFzDDGKgpiIh4GYXsSy6sek2yAwQx1JASl5AxBtU1Lq2JfKBljIPNdil989NcSKRQX1ToiaKphImtBuhXWw==",
- "dependencies": {
- "sort-css-media-queries": "2.1.0"
- },
- "engines": {
- "node": ">=10.0.0"
- },
- "peerDependencies": {
- "postcss": "^8.4.16"
- }
- },
- "node_modules/postcss-svgo": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.1.0.tgz",
- "integrity": "sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==",
- "dependencies": {
- "postcss-value-parser": "^4.2.0",
- "svgo": "^2.7.0"
- },
- "engines": {
- "node": "^10 || ^12 || >=14.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.15"
- }
- },
- "node_modules/postcss-unique-selectors": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz",
- "integrity": "sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==",
- "dependencies": {
- "postcss-selector-parser": "^6.0.5"
- },
- "engines": {
- "node": "^10 || ^12 || >=14.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.15"
- }
- },
- "node_modules/postcss-value-parser": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
- "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
- },
- "node_modules/postcss-zindex": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/postcss-zindex/-/postcss-zindex-5.1.0.tgz",
- "integrity": "sha512-fgFMf0OtVSBR1va1JNHYgMxYk73yhn/qb4uQDq1DLGYolz8gHCyr/sesEuGUaYs58E3ZJRcpoGuPVoB7Meiq9A==",
- "engines": {
- "node": "^10 || ^12 || >=14.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.15"
- }
- },
- "node_modules/prepend-http": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
- "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/pretty-error": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz",
- "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==",
- "dependencies": {
- "lodash": "^4.17.20",
- "renderkid": "^3.0.0"
- }
- },
- "node_modules/pretty-time": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/pretty-time/-/pretty-time-1.1.0.tgz",
- "integrity": "sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/prism-react-renderer": {
- "version": "1.3.5",
- "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-1.3.5.tgz",
- "integrity": "sha512-IJ+MSwBWKG+SM3b2SUfdrhC+gu01QkV2KmRQgREThBfSQRoufqRfxfHUxpG1WcaFjP+kojcFyO9Qqtpgt3qLCg==",
- "peerDependencies": {
- "react": ">=0.14.9"
- }
- },
- "node_modules/prismjs": {
- "version": "1.29.0",
- "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
- "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/process-nextick-args": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
- "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
- },
- "node_modules/promise": {
- "version": "7.3.1",
- "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
- "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
- "dependencies": {
- "asap": "~2.0.3"
- }
- },
- "node_modules/prompts": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
- "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==",
- "dependencies": {
- "kleur": "^3.0.3",
- "sisteransi": "^1.0.5"
- },
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/prop-types": {
- "version": "15.8.1",
- "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
- "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
- "dependencies": {
- "loose-envify": "^1.4.0",
- "object-assign": "^4.1.1",
- "react-is": "^16.13.1"
- }
- },
- "node_modules/property-information": {
- "version": "5.6.0",
- "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz",
- "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==",
- "dependencies": {
- "xtend": "^4.0.0"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/proxy-addr": {
- "version": "2.0.7",
- "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
- "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
- "dependencies": {
- "forwarded": "0.2.0",
- "ipaddr.js": "1.9.1"
- },
- "engines": {
- "node": ">= 0.10"
- }
- },
- "node_modules/proxy-addr/node_modules/ipaddr.js": {
- "version": "1.9.1",
- "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
- "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
- "engines": {
- "node": ">= 0.10"
- }
- },
- "node_modules/pump": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
- "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
- "dependencies": {
- "end-of-stream": "^1.1.0",
- "once": "^1.3.1"
- }
- },
- "node_modules/punycode": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
- "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ=="
- },
- "node_modules/pupa": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz",
- "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==",
- "dependencies": {
- "escape-goat": "^2.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/pure-color": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/pure-color/-/pure-color-1.3.0.tgz",
- "integrity": "sha512-QFADYnsVoBMw1srW7OVKEYjG+MbIa49s54w1MA1EDY6r2r/sTcKKYqRX1f4GYvnXP7eN/Pe9HFcX+hwzmrXRHA=="
- },
- "node_modules/qs": {
- "version": "6.11.0",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
- "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
- "dependencies": {
- "side-channel": "^1.0.4"
- },
- "engines": {
- "node": ">=0.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/queue": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
- "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
- "dependencies": {
- "inherits": "~2.0.3"
- }
- },
- "node_modules/queue-microtask": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
- "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ]
- },
- "node_modules/randombytes": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
- "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
- "dependencies": {
- "safe-buffer": "^5.1.0"
- }
- },
- "node_modules/range-parser": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
- "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/raw-body": {
- "version": "2.5.1",
- "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
- "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
- "dependencies": {
- "bytes": "3.1.2",
- "http-errors": "2.0.0",
- "iconv-lite": "0.4.24",
- "unpipe": "1.0.0"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/raw-body/node_modules/bytes": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
- "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/rc": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
- "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
- "dependencies": {
- "deep-extend": "^0.6.0",
- "ini": "~1.3.0",
- "minimist": "^1.2.0",
- "strip-json-comments": "~2.0.1"
- },
- "bin": {
- "rc": "cli.js"
- }
- },
- "node_modules/rc/node_modules/strip-json-comments": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
- "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/react": {
- "version": "17.0.2",
- "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
- "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==",
- "dependencies": {
- "loose-envify": "^1.1.0",
- "object-assign": "^4.1.1"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/react-base16-styling": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.6.0.tgz",
- "integrity": "sha512-yvh/7CArceR/jNATXOKDlvTnPKPmGZz7zsenQ3jUwLzHkNUR0CvY3yGYJbWJ/nnxsL8Sgmt5cO3/SILVuPO6TQ==",
- "dependencies": {
- "base16": "^1.0.0",
- "lodash.curry": "^4.0.1",
- "lodash.flow": "^3.3.0",
- "pure-color": "^1.2.0"
- }
- },
- "node_modules/react-dev-utils": {
- "version": "12.0.1",
- "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
- "integrity": "sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==",
- "dependencies": {
- "@babel/code-frame": "^7.16.0",
- "address": "^1.1.2",
- "browserslist": "^4.18.1",
- "chalk": "^4.1.2",
- "cross-spawn": "^7.0.3",
- "detect-port-alt": "^1.1.6",
- "escape-string-regexp": "^4.0.0",
- "filesize": "^8.0.6",
- "find-up": "^5.0.0",
- "fork-ts-checker-webpack-plugin": "^6.5.0",
- "global-modules": "^2.0.0",
- "globby": "^11.0.4",
- "gzip-size": "^6.0.0",
- "immer": "^9.0.7",
- "is-root": "^2.1.0",
- "loader-utils": "^3.2.0",
- "open": "^8.4.0",
- "pkg-up": "^3.1.0",
- "prompts": "^2.4.2",
- "react-error-overlay": "^6.0.11",
- "recursive-readdir": "^2.2.2",
- "shell-quote": "^1.7.3",
- "strip-ansi": "^6.0.1",
- "text-table": "^0.2.0"
- },
- "engines": {
- "node": ">=14"
- }
- },
- "node_modules/react-dev-utils/node_modules/find-up": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
- "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
- "dependencies": {
- "locate-path": "^6.0.0",
- "path-exists": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/react-dev-utils/node_modules/loader-utils": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz",
- "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==",
- "engines": {
- "node": ">= 12.13.0"
- }
- },
- "node_modules/react-dev-utils/node_modules/locate-path": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
- "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
- "dependencies": {
- "p-locate": "^5.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/react-dev-utils/node_modules/p-limit": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
- "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
- "dependencies": {
- "yocto-queue": "^0.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/react-dev-utils/node_modules/p-locate": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
- "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
- "dependencies": {
- "p-limit": "^3.0.2"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/react-dom": {
- "version": "17.0.2",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
- "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==",
- "dependencies": {
- "loose-envify": "^1.1.0",
- "object-assign": "^4.1.1",
- "scheduler": "^0.20.2"
- },
- "peerDependencies": {
- "react": "17.0.2"
- }
- },
- "node_modules/react-error-overlay": {
- "version": "6.0.11",
- "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz",
- "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg=="
- },
- "node_modules/react-fast-compare": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
- "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="
- },
- "node_modules/react-helmet-async": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/react-helmet-async/-/react-helmet-async-1.3.0.tgz",
- "integrity": "sha512-9jZ57/dAn9t3q6hneQS0wukqC2ENOBgMNVEhb/ZG9ZSxUetzVIw4iAmEU38IaVg3QGYauQPhSeUTuIUtFglWpg==",
- "dependencies": {
- "@babel/runtime": "^7.12.5",
- "invariant": "^2.2.4",
- "prop-types": "^15.7.2",
- "react-fast-compare": "^3.2.0",
- "shallowequal": "^1.1.0"
- },
- "peerDependencies": {
- "react": "^16.6.0 || ^17.0.0 || ^18.0.0",
- "react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0"
- }
- },
- "node_modules/react-is": {
- "version": "16.13.1",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
- "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
- },
- "node_modules/react-json-view": {
- "version": "1.21.3",
- "resolved": "https://registry.npmjs.org/react-json-view/-/react-json-view-1.21.3.tgz",
- "integrity": "sha512-13p8IREj9/x/Ye4WI/JpjhoIwuzEgUAtgJZNBJckfzJt1qyh24BdTm6UQNGnyTq9dapQdrqvquZTo3dz1X6Cjw==",
- "dependencies": {
- "flux": "^4.0.1",
- "react-base16-styling": "^0.6.0",
- "react-lifecycles-compat": "^3.0.4",
- "react-textarea-autosize": "^8.3.2"
- },
- "peerDependencies": {
- "react": "^17.0.0 || ^16.3.0 || ^15.5.4",
- "react-dom": "^17.0.0 || ^16.3.0 || ^15.5.4"
- }
- },
- "node_modules/react-lifecycles-compat": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
- "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
- },
- "node_modules/react-loadable": {
- "name": "@docusaurus/react-loadable",
- "version": "5.5.2",
- "resolved": "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-5.5.2.tgz",
- "integrity": "sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ==",
- "dependencies": {
- "@types/react": "*",
- "prop-types": "^15.6.2"
- },
- "peerDependencies": {
- "react": "*"
- }
- },
- "node_modules/react-loadable-ssr-addon-v5-slorber": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.1.tgz",
- "integrity": "sha512-lq3Lyw1lGku8zUEJPDxsNm1AfYHBrO9Y1+olAYwpUJ2IGFBskM0DMKok97A6LWUpHm+o7IvQBOWu9MLenp9Z+A==",
- "dependencies": {
- "@babel/runtime": "^7.10.3"
- },
- "engines": {
- "node": ">=10.13.0"
- },
- "peerDependencies": {
- "react-loadable": "*",
- "webpack": ">=4.41.1 || 5.x"
- }
- },
- "node_modules/react-router": {
- "version": "5.3.4",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz",
- "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==",
- "dependencies": {
- "@babel/runtime": "^7.12.13",
- "history": "^4.9.0",
- "hoist-non-react-statics": "^3.1.0",
- "loose-envify": "^1.3.1",
- "path-to-regexp": "^1.7.0",
- "prop-types": "^15.6.2",
- "react-is": "^16.6.0",
- "tiny-invariant": "^1.0.2",
- "tiny-warning": "^1.0.0"
- },
- "peerDependencies": {
- "react": ">=15"
- }
- },
- "node_modules/react-router-config": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/react-router-config/-/react-router-config-5.1.1.tgz",
- "integrity": "sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg==",
- "dependencies": {
- "@babel/runtime": "^7.1.2"
- },
- "peerDependencies": {
- "react": ">=15",
- "react-router": ">=5"
- }
- },
- "node_modules/react-router-dom": {
- "version": "5.3.4",
- "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz",
- "integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==",
- "dependencies": {
- "@babel/runtime": "^7.12.13",
- "history": "^4.9.0",
- "loose-envify": "^1.3.1",
- "prop-types": "^15.6.2",
- "react-router": "5.3.4",
- "tiny-invariant": "^1.0.2",
- "tiny-warning": "^1.0.0"
- },
- "peerDependencies": {
- "react": ">=15"
- }
- },
- "node_modules/react-textarea-autosize": {
- "version": "8.4.1",
- "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.4.1.tgz",
- "integrity": "sha512-aD2C+qK6QypknC+lCMzteOdIjoMbNlgSFmJjCV+DrfTPwp59i/it9mMNf2HDzvRjQgKAyBDPyLJhcrzElf2U4Q==",
- "dependencies": {
- "@babel/runtime": "^7.20.13",
- "use-composed-ref": "^1.3.0",
- "use-latest": "^1.2.1"
- },
- "engines": {
- "node": ">=10"
- },
- "peerDependencies": {
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
- }
- },
- "node_modules/readable-stream": {
- "version": "3.6.2",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
- "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
- "dependencies": {
- "inherits": "^2.0.3",
- "string_decoder": "^1.1.1",
- "util-deprecate": "^1.0.1"
- },
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/readdirp": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
- "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
- "dependencies": {
- "picomatch": "^2.2.1"
- },
- "engines": {
- "node": ">=8.10.0"
- }
- },
- "node_modules/reading-time": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/reading-time/-/reading-time-1.5.0.tgz",
- "integrity": "sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg=="
- },
- "node_modules/rechoir": {
- "version": "0.6.2",
- "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
- "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==",
- "dependencies": {
- "resolve": "^1.1.6"
- },
- "engines": {
- "node": ">= 0.10"
- }
- },
- "node_modules/recursive-readdir": {
- "version": "2.2.3",
- "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz",
- "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==",
- "dependencies": {
- "minimatch": "^3.0.5"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/regenerate": {
- "version": "1.4.2",
- "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
- "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A=="
- },
- "node_modules/regenerate-unicode-properties": {
- "version": "10.1.0",
- "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz",
- "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==",
- "dependencies": {
- "regenerate": "^1.4.2"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/regenerator-runtime": {
- "version": "0.13.11",
- "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
- "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
- },
- "node_modules/regenerator-transform": {
- "version": "0.15.1",
- "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz",
- "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==",
- "dependencies": {
- "@babel/runtime": "^7.8.4"
- }
- },
- "node_modules/regexpu-core": {
- "version": "5.3.2",
- "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz",
- "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==",
- "dependencies": {
- "@babel/regjsgen": "^0.8.0",
- "regenerate": "^1.4.2",
- "regenerate-unicode-properties": "^10.1.0",
- "regjsparser": "^0.9.1",
- "unicode-match-property-ecmascript": "^2.0.0",
- "unicode-match-property-value-ecmascript": "^2.1.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/registry-auth-token": {
- "version": "4.2.2",
- "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.2.tgz",
- "integrity": "sha512-PC5ZysNb42zpFME6D/XlIgtNGdTl8bBOCw90xQLVMpzuuubJKYDWFAEuUNc+Cn8Z8724tg2SDhDRrkVEsqfDMg==",
- "dependencies": {
- "rc": "1.2.8"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/registry-url": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz",
- "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==",
- "dependencies": {
- "rc": "^1.2.8"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/regjsparser": {
- "version": "0.9.1",
- "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz",
- "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==",
- "dependencies": {
- "jsesc": "~0.5.0"
- },
- "bin": {
- "regjsparser": "bin/parser"
- }
- },
- "node_modules/regjsparser/node_modules/jsesc": {
- "version": "0.5.0",
- "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
- "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==",
- "bin": {
- "jsesc": "bin/jsesc"
- }
- },
- "node_modules/relateurl": {
- "version": "0.2.7",
- "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
- "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==",
- "engines": {
- "node": ">= 0.10"
- }
- },
- "node_modules/remark-emoji": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/remark-emoji/-/remark-emoji-2.2.0.tgz",
- "integrity": "sha512-P3cj9s5ggsUvWw5fS2uzCHJMGuXYRb0NnZqYlNecewXt8QBU9n5vW3DUUKOhepS8F9CwdMx9B8a3i7pqFWAI5w==",
- "dependencies": {
- "emoticon": "^3.2.0",
- "node-emoji": "^1.10.0",
- "unist-util-visit": "^2.0.3"
- }
- },
- "node_modules/remark-footnotes": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/remark-footnotes/-/remark-footnotes-2.0.0.tgz",
- "integrity": "sha512-3Clt8ZMH75Ayjp9q4CorNeyjwIxHFcTkaektplKGl2A1jNGEUey8cKL0ZC5vJwfcD5GFGsNLImLG/NGzWIzoMQ==",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/remark-mdx": {
- "version": "1.6.22",
- "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-1.6.22.tgz",
- "integrity": "sha512-phMHBJgeV76uyFkH4rvzCftLfKCr2RZuF+/gmVcaKrpsihyzmhXjA0BEMDaPTXG5y8qZOKPVo83NAOX01LPnOQ==",
- "dependencies": {
- "@babel/core": "7.12.9",
- "@babel/helper-plugin-utils": "7.10.4",
- "@babel/plugin-proposal-object-rest-spread": "7.12.1",
- "@babel/plugin-syntax-jsx": "7.12.1",
- "@mdx-js/util": "1.6.22",
- "is-alphabetical": "1.0.4",
- "remark-parse": "8.0.3",
- "unified": "9.2.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/remark-mdx/node_modules/@babel/core": {
- "version": "7.12.9",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.9.tgz",
- "integrity": "sha512-gTXYh3M5wb7FRXQy+FErKFAv90BnlOuNn1QkCK2lREoPAjrQCO49+HVSrFoe5uakFAF5eenS75KbO2vQiLrTMQ==",
- "dependencies": {
- "@babel/code-frame": "^7.10.4",
- "@babel/generator": "^7.12.5",
- "@babel/helper-module-transforms": "^7.12.1",
- "@babel/helpers": "^7.12.5",
- "@babel/parser": "^7.12.7",
- "@babel/template": "^7.12.7",
- "@babel/traverse": "^7.12.9",
- "@babel/types": "^7.12.7",
- "convert-source-map": "^1.7.0",
- "debug": "^4.1.0",
- "gensync": "^1.0.0-beta.1",
- "json5": "^2.1.2",
- "lodash": "^4.17.19",
- "resolve": "^1.3.2",
- "semver": "^5.4.1",
- "source-map": "^0.5.0"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/babel"
- }
- },
- "node_modules/remark-mdx/node_modules/@babel/helper-plugin-utils": {
- "version": "7.10.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz",
- "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg=="
- },
- "node_modules/remark-mdx/node_modules/@babel/plugin-syntax-jsx": {
- "version": "7.12.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.1.tgz",
- "integrity": "sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg==",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.10.4"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/remark-mdx/node_modules/semver": {
- "version": "5.7.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
- "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
- "bin": {
- "semver": "bin/semver"
- }
- },
- "node_modules/remark-mdx/node_modules/source-map": {
- "version": "0.5.7",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
- "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/remark-mdx/node_modules/unified": {
- "version": "9.2.0",
- "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.0.tgz",
- "integrity": "sha512-vx2Z0vY+a3YoTj8+pttM3tiJHCwY5UFbYdiWrwBEbHmK8pvsPj2rtAX2BFfgXen8T39CJWblWRDT4L5WGXtDdg==",
- "dependencies": {
- "bail": "^1.0.0",
- "extend": "^3.0.0",
- "is-buffer": "^2.0.0",
- "is-plain-obj": "^2.0.0",
- "trough": "^1.0.0",
- "vfile": "^4.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/remark-parse": {
- "version": "8.0.3",
- "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-8.0.3.tgz",
- "integrity": "sha512-E1K9+QLGgggHxCQtLt++uXltxEprmWzNfg+MxpfHsZlrddKzZ/hZyWHDbK3/Ap8HJQqYJRXP+jHczdL6q6i85Q==",
- "dependencies": {
- "ccount": "^1.0.0",
- "collapse-white-space": "^1.0.2",
- "is-alphabetical": "^1.0.0",
- "is-decimal": "^1.0.0",
- "is-whitespace-character": "^1.0.0",
- "is-word-character": "^1.0.0",
- "markdown-escapes": "^1.0.0",
- "parse-entities": "^2.0.0",
- "repeat-string": "^1.5.4",
- "state-toggle": "^1.0.0",
- "trim": "0.0.1",
- "trim-trailing-lines": "^1.0.0",
- "unherit": "^1.0.4",
- "unist-util-remove-position": "^2.0.0",
- "vfile-location": "^3.0.0",
- "xtend": "^4.0.1"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/remark-squeeze-paragraphs": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/remark-squeeze-paragraphs/-/remark-squeeze-paragraphs-4.0.0.tgz",
- "integrity": "sha512-8qRqmL9F4nuLPIgl92XUuxI3pFxize+F1H0e/W3llTk0UsjJaj01+RrirkMw7P21RKe4X6goQhYRSvNWX+70Rw==",
- "dependencies": {
- "mdast-squeeze-paragraphs": "^4.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/renderkid": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz",
- "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==",
- "dependencies": {
- "css-select": "^4.1.3",
- "dom-converter": "^0.2.0",
- "htmlparser2": "^6.1.0",
- "lodash": "^4.17.21",
- "strip-ansi": "^6.0.1"
- }
- },
- "node_modules/renderkid/node_modules/css-select": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz",
- "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==",
- "dependencies": {
- "boolbase": "^1.0.0",
- "css-what": "^6.0.1",
- "domhandler": "^4.3.1",
- "domutils": "^2.8.0",
- "nth-check": "^2.0.1"
- },
- "funding": {
- "url": "https://github.com/sponsors/fb55"
- }
- },
- "node_modules/renderkid/node_modules/dom-serializer": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
- "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==",
- "dependencies": {
- "domelementtype": "^2.0.1",
- "domhandler": "^4.2.0",
- "entities": "^2.0.0"
- },
- "funding": {
- "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
- }
- },
- "node_modules/renderkid/node_modules/domhandler": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz",
- "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==",
- "dependencies": {
- "domelementtype": "^2.2.0"
- },
- "engines": {
- "node": ">= 4"
- },
- "funding": {
- "url": "https://github.com/fb55/domhandler?sponsor=1"
- }
- },
- "node_modules/renderkid/node_modules/domutils": {
- "version": "2.8.0",
- "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
- "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==",
- "dependencies": {
- "dom-serializer": "^1.0.1",
- "domelementtype": "^2.2.0",
- "domhandler": "^4.2.0"
- },
- "funding": {
- "url": "https://github.com/fb55/domutils?sponsor=1"
- }
- },
- "node_modules/renderkid/node_modules/entities": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
- "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
- "funding": {
- "url": "https://github.com/fb55/entities?sponsor=1"
- }
- },
- "node_modules/renderkid/node_modules/htmlparser2": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
- "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==",
- "funding": [
- "https://github.com/fb55/htmlparser2?sponsor=1",
- {
- "type": "github",
- "url": "https://github.com/sponsors/fb55"
- }
- ],
- "dependencies": {
- "domelementtype": "^2.0.1",
- "domhandler": "^4.0.0",
- "domutils": "^2.5.2",
- "entities": "^2.0.0"
- }
- },
- "node_modules/repeat-string": {
- "version": "1.6.1",
- "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
- "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==",
- "engines": {
- "node": ">=0.10"
- }
- },
- "node_modules/require-from-string": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
- "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/require-like": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz",
- "integrity": "sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A==",
- "engines": {
- "node": "*"
- }
- },
- "node_modules/requires-port": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
- "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
- },
- "node_modules/resolve": {
- "version": "1.22.2",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
- "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==",
- "dependencies": {
- "is-core-module": "^2.11.0",
- "path-parse": "^1.0.7",
- "supports-preserve-symlinks-flag": "^1.0.0"
- },
- "bin": {
- "resolve": "bin/resolve"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/resolve-from": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
- "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/resolve-pathname": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
- "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng=="
- },
- "node_modules/responselike": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz",
- "integrity": "sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==",
- "dependencies": {
- "lowercase-keys": "^1.0.0"
- }
- },
- "node_modules/retry": {
- "version": "0.13.1",
- "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
- "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
- "engines": {
- "node": ">= 4"
- }
- },
- "node_modules/reusify": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
- "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
- "engines": {
- "iojs": ">=1.0.0",
- "node": ">=0.10.0"
- }
- },
- "node_modules/rimraf": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
- "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
- "dependencies": {
- "glob": "^7.1.3"
- },
- "bin": {
- "rimraf": "bin.js"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/rtl-detect": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/rtl-detect/-/rtl-detect-1.0.4.tgz",
- "integrity": "sha512-EBR4I2VDSSYr7PkBmFy04uhycIpDKp+21p/jARYXlCSjQksTBQcJ0HFUPOO79EPPH5JS6VAhiIQbycf0O3JAxQ=="
- },
- "node_modules/rtlcss": {
- "version": "3.5.0",
- "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-3.5.0.tgz",
- "integrity": "sha512-wzgMaMFHQTnyi9YOwsx9LjOxYXJPzS8sYnFaKm6R5ysvTkwzHiB0vxnbHwchHQT65PTdBjDG21/kQBWI7q9O7A==",
- "dependencies": {
- "find-up": "^5.0.0",
- "picocolors": "^1.0.0",
- "postcss": "^8.3.11",
- "strip-json-comments": "^3.1.1"
- },
- "bin": {
- "rtlcss": "bin/rtlcss.js"
- }
- },
- "node_modules/rtlcss/node_modules/find-up": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
- "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
- "dependencies": {
- "locate-path": "^6.0.0",
- "path-exists": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/rtlcss/node_modules/locate-path": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
- "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
- "dependencies": {
- "p-locate": "^5.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/rtlcss/node_modules/p-limit": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
- "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
- "dependencies": {
- "yocto-queue": "^0.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/rtlcss/node_modules/p-locate": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
- "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
- "dependencies": {
- "p-limit": "^3.0.2"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/run-parallel": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
- "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "dependencies": {
- "queue-microtask": "^1.2.2"
- }
- },
- "node_modules/rxjs": {
- "version": "7.8.1",
- "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
- "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
- "dependencies": {
- "tslib": "^2.1.0"
- }
- },
- "node_modules/safe-buffer": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
- "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ]
- },
- "node_modules/safer-buffer": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
- "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
- },
- "node_modules/sax": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
- "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
- },
- "node_modules/scheduler": {
- "version": "0.20.2",
- "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz",
- "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==",
- "dependencies": {
- "loose-envify": "^1.1.0",
- "object-assign": "^4.1.1"
- }
- },
- "node_modules/schema-utils": {
- "version": "2.7.1",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz",
- "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==",
- "dependencies": {
- "@types/json-schema": "^7.0.5",
- "ajv": "^6.12.4",
- "ajv-keywords": "^3.5.2"
- },
- "engines": {
- "node": ">= 8.9.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- }
- },
- "node_modules/search-insights": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.6.0.tgz",
- "integrity": "sha512-vU2/fJ+h/Mkm/DJOe+EaM5cafJv/1rRTZpGJTuFPf/Q5LjzgMDsqPdSaZsAe+GAWHHsfsu+rQSAn6c8IGtBEVw==",
- "peer": true,
- "engines": {
- "node": ">=8.16.0"
- }
- },
- "node_modules/section-matter": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
- "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
- "dependencies": {
- "extend-shallow": "^2.0.1",
- "kind-of": "^6.0.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/select-hose": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
- "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg=="
- },
- "node_modules/selfsigned": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.1.1.tgz",
- "integrity": "sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==",
- "dependencies": {
- "node-forge": "^1"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/semver": {
- "version": "7.5.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz",
- "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==",
- "dependencies": {
- "lru-cache": "^6.0.0"
- },
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/semver-diff": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz",
- "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==",
- "dependencies": {
- "semver": "^6.3.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/semver-diff/node_modules/semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
- "node_modules/semver/node_modules/lru-cache": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
- "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "dependencies": {
- "yallist": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/semver/node_modules/yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
- },
- "node_modules/send": {
- "version": "0.18.0",
- "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
- "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
- "dependencies": {
- "debug": "2.6.9",
- "depd": "2.0.0",
- "destroy": "1.2.0",
- "encodeurl": "~1.0.2",
- "escape-html": "~1.0.3",
- "etag": "~1.8.1",
- "fresh": "0.5.2",
- "http-errors": "2.0.0",
- "mime": "1.6.0",
- "ms": "2.1.3",
- "on-finished": "2.4.1",
- "range-parser": "~1.2.1",
- "statuses": "2.0.1"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/send/node_modules/debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "dependencies": {
- "ms": "2.0.0"
- }
- },
- "node_modules/send/node_modules/debug/node_modules/ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
- },
- "node_modules/send/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
- },
- "node_modules/send/node_modules/range-parser": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
- "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/serialize-javascript": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz",
- "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==",
- "dependencies": {
- "randombytes": "^2.1.0"
- }
- },
- "node_modules/serve-handler": {
- "version": "6.1.5",
- "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.5.tgz",
- "integrity": "sha512-ijPFle6Hwe8zfmBxJdE+5fta53fdIY0lHISJvuikXB3VYFafRjMRpOffSPvCYsbKyBA7pvy9oYr/BT1O3EArlg==",
- "dependencies": {
- "bytes": "3.0.0",
- "content-disposition": "0.5.2",
- "fast-url-parser": "1.1.3",
- "mime-types": "2.1.18",
- "minimatch": "3.1.2",
- "path-is-inside": "1.0.2",
- "path-to-regexp": "2.2.1",
- "range-parser": "1.2.0"
- }
- },
- "node_modules/serve-handler/node_modules/path-to-regexp": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.2.1.tgz",
- "integrity": "sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ=="
- },
- "node_modules/serve-index": {
- "version": "1.9.1",
- "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz",
- "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==",
- "dependencies": {
- "accepts": "~1.3.4",
- "batch": "0.6.1",
- "debug": "2.6.9",
- "escape-html": "~1.0.3",
- "http-errors": "~1.6.2",
- "mime-types": "~2.1.17",
- "parseurl": "~1.3.2"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/serve-index/node_modules/debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "dependencies": {
- "ms": "2.0.0"
- }
- },
- "node_modules/serve-index/node_modules/depd": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
- "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/serve-index/node_modules/http-errors": {
- "version": "1.6.3",
- "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
- "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==",
- "dependencies": {
- "depd": "~1.1.2",
- "inherits": "2.0.3",
- "setprototypeof": "1.1.0",
- "statuses": ">= 1.4.0 < 2"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/serve-index/node_modules/inherits": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
- "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw=="
- },
- "node_modules/serve-index/node_modules/ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
- },
- "node_modules/serve-index/node_modules/setprototypeof": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
- "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
- },
- "node_modules/serve-index/node_modules/statuses": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
- "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/serve-static": {
- "version": "1.15.0",
- "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
- "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
- "dependencies": {
- "encodeurl": "~1.0.2",
- "escape-html": "~1.0.3",
- "parseurl": "~1.3.3",
- "send": "0.18.0"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/setimmediate": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
- "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
- },
- "node_modules/setprototypeof": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
- "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
- },
- "node_modules/shallow-clone": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
- "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==",
- "dependencies": {
- "kind-of": "^6.0.2"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/shallowequal": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
- "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
- },
- "node_modules/shebang-command": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
- "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
- "dependencies": {
- "shebang-regex": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/shebang-regex": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
- "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/shell-quote": {
- "version": "1.8.1",
- "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz",
- "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/shelljs": {
- "version": "0.8.5",
- "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz",
- "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==",
- "dependencies": {
- "glob": "^7.0.0",
- "interpret": "^1.0.0",
- "rechoir": "^0.6.2"
- },
- "bin": {
- "shjs": "bin/shjs"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/side-channel": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
- "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
- "dependencies": {
- "call-bind": "^1.0.0",
- "get-intrinsic": "^1.0.2",
- "object-inspect": "^1.9.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/signal-exit": {
- "version": "3.0.7",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
- "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
- },
- "node_modules/sirv": {
- "version": "1.0.19",
- "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.19.tgz",
- "integrity": "sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==",
- "dependencies": {
- "@polka/url": "^1.0.0-next.20",
- "mrmime": "^1.0.0",
- "totalist": "^1.0.0"
- },
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/sisteransi": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
- "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="
- },
- "node_modules/sitemap": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-7.1.1.tgz",
- "integrity": "sha512-mK3aFtjz4VdJN0igpIJrinf3EO8U8mxOPsTBzSsy06UtjZQJ3YY3o3Xa7zSc5nMqcMrRwlChHZ18Kxg0caiPBg==",
- "dependencies": {
- "@types/node": "^17.0.5",
- "@types/sax": "^1.2.1",
- "arg": "^5.0.0",
- "sax": "^1.2.4"
- },
- "bin": {
- "sitemap": "dist/cli.js"
- },
- "engines": {
- "node": ">=12.0.0",
- "npm": ">=5.6.0"
- }
- },
- "node_modules/sitemap/node_modules/@types/node": {
- "version": "17.0.45",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz",
- "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw=="
- },
- "node_modules/slash": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
- "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/sockjs": {
- "version": "0.3.24",
- "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz",
- "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==",
- "dependencies": {
- "faye-websocket": "^0.11.3",
- "uuid": "^8.3.2",
- "websocket-driver": "^0.7.4"
- }
- },
- "node_modules/sort-css-media-queries": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/sort-css-media-queries/-/sort-css-media-queries-2.1.0.tgz",
- "integrity": "sha512-IeWvo8NkNiY2vVYdPa27MCQiR0MN0M80johAYFVxWWXQ44KU84WNxjslwBHmc/7ZL2ccwkM7/e6S5aiKZXm7jA==",
- "engines": {
- "node": ">= 6.3.0"
- }
- },
- "node_modules/source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/source-map-js": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
- "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/source-map-support": {
- "version": "0.5.21",
- "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
- "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
- "dependencies": {
- "buffer-from": "^1.0.0",
- "source-map": "^0.6.0"
- }
- },
- "node_modules/space-separated-tokens": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz",
- "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/spdy": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz",
- "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==",
- "dependencies": {
- "debug": "^4.1.0",
- "handle-thing": "^2.0.0",
- "http-deceiver": "^1.2.7",
- "select-hose": "^2.0.0",
- "spdy-transport": "^3.0.0"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/spdy-transport": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz",
- "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==",
- "dependencies": {
- "debug": "^4.1.0",
- "detect-node": "^2.0.4",
- "hpack.js": "^2.1.6",
- "obuf": "^1.1.2",
- "readable-stream": "^3.0.6",
- "wbuf": "^1.7.3"
- }
- },
- "node_modules/sprintf-js": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
- "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="
- },
- "node_modules/stable": {
- "version": "0.1.8",
- "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
- "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==",
- "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility"
- },
- "node_modules/state-toggle": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz",
- "integrity": "sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/statuses": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
- "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/std-env": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.3.3.tgz",
- "integrity": "sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg=="
- },
- "node_modules/string_decoder": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
- "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
- "dependencies": {
- "safe-buffer": "~5.2.0"
- }
- },
- "node_modules/string-width": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
- "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
- "dependencies": {
- "eastasianwidth": "^0.2.0",
- "emoji-regex": "^9.2.2",
- "strip-ansi": "^7.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/string-width/node_modules/ansi-regex": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
- "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-regex?sponsor=1"
- }
- },
- "node_modules/string-width/node_modules/strip-ansi": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
- "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
- "dependencies": {
- "ansi-regex": "^6.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/strip-ansi?sponsor=1"
- }
- },
- "node_modules/stringify-object": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz",
- "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==",
- "dependencies": {
- "get-own-enumerable-property-symbols": "^3.0.0",
- "is-obj": "^1.0.1",
- "is-regexp": "^1.0.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/strip-bom-string": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
- "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/strip-final-newline": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
- "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/strip-json-comments": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
- "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/style-to-object": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz",
- "integrity": "sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==",
- "dependencies": {
- "inline-style-parser": "0.1.1"
- }
- },
- "node_modules/stylehacks": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz",
- "integrity": "sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==",
- "dependencies": {
- "browserslist": "^4.21.4",
- "postcss-selector-parser": "^6.0.4"
- },
- "engines": {
- "node": "^10 || ^12 || >=14.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.15"
- }
- },
- "node_modules/supports-color": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
- "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/supports-preserve-symlinks-flag": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
- "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/svg-parser": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz",
- "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ=="
- },
- "node_modules/svgo": {
- "version": "2.8.0",
- "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz",
- "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==",
- "dependencies": {
- "@trysound/sax": "0.2.0",
- "commander": "^7.2.0",
- "css-select": "^4.1.3",
- "css-tree": "^1.1.3",
- "csso": "^4.2.0",
- "picocolors": "^1.0.0",
- "stable": "^0.1.8"
- },
- "bin": {
- "svgo": "bin/svgo"
- },
- "engines": {
- "node": ">=10.13.0"
- }
- },
- "node_modules/svgo/node_modules/commander": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
- "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/svgo/node_modules/css-select": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz",
- "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==",
- "dependencies": {
- "boolbase": "^1.0.0",
- "css-what": "^6.0.1",
- "domhandler": "^4.3.1",
- "domutils": "^2.8.0",
- "nth-check": "^2.0.1"
- },
- "funding": {
- "url": "https://github.com/sponsors/fb55"
- }
- },
- "node_modules/svgo/node_modules/dom-serializer": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
- "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==",
- "dependencies": {
- "domelementtype": "^2.0.1",
- "domhandler": "^4.2.0",
- "entities": "^2.0.0"
- },
- "funding": {
- "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
- }
- },
- "node_modules/svgo/node_modules/domhandler": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz",
- "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==",
- "dependencies": {
- "domelementtype": "^2.2.0"
- },
- "engines": {
- "node": ">= 4"
- },
- "funding": {
- "url": "https://github.com/fb55/domhandler?sponsor=1"
- }
- },
- "node_modules/svgo/node_modules/domutils": {
- "version": "2.8.0",
- "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
- "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==",
- "dependencies": {
- "dom-serializer": "^1.0.1",
- "domelementtype": "^2.2.0",
- "domhandler": "^4.2.0"
- },
- "funding": {
- "url": "https://github.com/fb55/domutils?sponsor=1"
- }
- },
- "node_modules/svgo/node_modules/entities": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
- "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
- "funding": {
- "url": "https://github.com/fb55/entities?sponsor=1"
- }
- },
- "node_modules/tapable": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
- "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/terser": {
- "version": "5.18.1",
- "resolved": "https://registry.npmjs.org/terser/-/terser-5.18.1.tgz",
- "integrity": "sha512-j1n0Ao919h/Ai5r43VAnfV/7azUYW43GPxK7qSATzrsERfW7+y2QW9Cp9ufnRF5CQUWbnLSo7UJokSWCqg4tsQ==",
- "dependencies": {
- "@jridgewell/source-map": "^0.3.3",
- "acorn": "^8.8.2",
- "commander": "^2.20.0",
- "source-map-support": "~0.5.20"
- },
- "bin": {
- "terser": "bin/terser"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/terser-webpack-plugin": {
- "version": "5.3.9",
- "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz",
- "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==",
- "dependencies": {
- "@jridgewell/trace-mapping": "^0.3.17",
- "jest-worker": "^27.4.5",
- "schema-utils": "^3.1.1",
- "serialize-javascript": "^6.0.1",
- "terser": "^5.16.8"
- },
- "engines": {
- "node": ">= 10.13.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- },
- "peerDependencies": {
- "webpack": "^5.1.0"
- },
- "peerDependenciesMeta": {
- "@swc/core": {
- "optional": true
- },
- "esbuild": {
- "optional": true
- },
- "uglify-js": {
- "optional": true
- }
- }
- },
- "node_modules/terser-webpack-plugin/node_modules/jest-worker": {
- "version": "27.5.1",
- "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
- "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
- "dependencies": {
- "@types/node": "*",
- "merge-stream": "^2.0.0",
- "supports-color": "^8.0.0"
- },
- "engines": {
- "node": ">= 10.13.0"
- }
- },
- "node_modules/terser-webpack-plugin/node_modules/schema-utils": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
- "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
- "dependencies": {
- "@types/json-schema": "^7.0.8",
- "ajv": "^6.12.5",
- "ajv-keywords": "^3.5.2"
- },
- "engines": {
- "node": ">= 10.13.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- }
- },
- "node_modules/terser-webpack-plugin/node_modules/supports-color": {
- "version": "8.1.1",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
- "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/supports-color?sponsor=1"
- }
- },
- "node_modules/terser/node_modules/commander": {
- "version": "2.20.3",
- "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
- "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
- },
- "node_modules/text-table": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
- "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="
- },
- "node_modules/thunky": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
- "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="
- },
- "node_modules/tiny-invariant": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz",
- "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw=="
- },
- "node_modules/tiny-warning": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
- "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
- },
- "node_modules/to-fast-properties": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
- "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/to-readable-stream": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz",
- "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/to-regex-range": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
- "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
- "dependencies": {
- "is-number": "^7.0.0"
- },
- "engines": {
- "node": ">=8.0"
- }
- },
- "node_modules/toidentifier": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
- "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
- "engines": {
- "node": ">=0.6"
- }
- },
- "node_modules/totalist": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz",
- "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/tr46": {
- "version": "0.0.3",
- "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
- "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
- },
- "node_modules/trim": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz",
- "integrity": "sha512-YzQV+TZg4AxpKxaTHK3c3D+kRDCGVEE7LemdlQZoQXn0iennk10RsIoY6ikzAqJTc9Xjl9C1/waHom/J86ziAQ==",
- "deprecated": "Use String.prototype.trim() instead"
- },
- "node_modules/trim-trailing-lines": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.4.tgz",
- "integrity": "sha512-rjUWSqnfTNrjbB9NQWfPMH/xRK1deHeGsHoVfpxJ++XeYXE0d6B1En37AHfw3jtfTU7dzMzZL2jjpe8Qb5gLIQ==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/trough": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz",
- "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/tslib": {
- "version": "2.5.3",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz",
- "integrity": "sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w=="
- },
- "node_modules/type-fest": {
- "version": "2.19.0",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
- "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
- "engines": {
- "node": ">=12.20"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/type-is": {
- "version": "1.6.18",
- "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
- "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
- "dependencies": {
- "media-typer": "0.3.0",
- "mime-types": "~2.1.24"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/type-is/node_modules/mime-db": {
- "version": "1.52.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
- "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/type-is/node_modules/mime-types": {
- "version": "2.1.35",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
- "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
- "dependencies": {
- "mime-db": "1.52.0"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/typedarray-to-buffer": {
- "version": "3.1.5",
- "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
- "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
- "dependencies": {
- "is-typedarray": "^1.0.0"
- }
- },
- "node_modules/typescript": {
- "version": "4.9.5",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
- "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
- "bin": {
- "tsc": "bin/tsc",
- "tsserver": "bin/tsserver"
- },
- "engines": {
- "node": ">=4.2.0"
- }
- },
- "node_modules/ua-parser-js": {
- "version": "1.0.35",
- "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.35.tgz",
- "integrity": "sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA==",
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/ua-parser-js"
- },
- {
- "type": "paypal",
- "url": "https://paypal.me/faisalman"
- }
- ],
- "engines": {
- "node": "*"
- }
- },
- "node_modules/unherit": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz",
- "integrity": "sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ==",
- "dependencies": {
- "inherits": "^2.0.0",
- "xtend": "^4.0.0"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/unicode-canonical-property-names-ecmascript": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz",
- "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/unicode-match-property-ecmascript": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz",
- "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==",
- "dependencies": {
- "unicode-canonical-property-names-ecmascript": "^2.0.0",
- "unicode-property-aliases-ecmascript": "^2.0.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/unicode-match-property-value-ecmascript": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz",
- "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/unicode-property-aliases-ecmascript": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz",
- "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/unified": {
- "version": "9.2.2",
- "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz",
- "integrity": "sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==",
- "dependencies": {
- "bail": "^1.0.0",
- "extend": "^3.0.0",
- "is-buffer": "^2.0.0",
- "is-plain-obj": "^2.0.0",
- "trough": "^1.0.0",
- "vfile": "^4.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/unique-string": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz",
- "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==",
- "dependencies": {
- "crypto-random-string": "^2.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/unist-builder": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-2.0.3.tgz",
- "integrity": "sha512-f98yt5pnlMWlzP539tPc4grGMsFaQQlP/vM396b00jngsiINumNmsY8rkXjfoi1c6QaM8nQ3vaGDuoKWbe/1Uw==",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/unist-util-generated": {
- "version": "1.1.6",
- "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-1.1.6.tgz",
- "integrity": "sha512-cln2Mm1/CZzN5ttGK7vkoGw+RZ8VcUH6BtGbq98DDtRGquAAOXig1mrBQYelOwMXYS8rK+vZDyyojSjp7JX+Lg==",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/unist-util-is": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz",
- "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/unist-util-position": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-3.1.0.tgz",
- "integrity": "sha512-w+PkwCbYSFw8vpgWD0v7zRCl1FpY3fjDSQ3/N/wNd9Ffa4gPi8+4keqt99N3XW6F99t/mUzp2xAhNmfKWp95QA==",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/unist-util-remove": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/unist-util-remove/-/unist-util-remove-2.1.0.tgz",
- "integrity": "sha512-J8NYPyBm4baYLdCbjmf1bhPu45Cr1MWTm77qd9istEkzWpnN6O9tMsEbB2JhNnBCqGENRqEWomQ+He6au0B27Q==",
- "dependencies": {
- "unist-util-is": "^4.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/unist-util-remove-position": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-2.0.1.tgz",
- "integrity": "sha512-fDZsLYIe2uT+oGFnuZmy73K6ZxOPG/Qcm+w7jbEjaFcJgbQ6cqjs/eSPzXhsmGpAsWPkqZM9pYjww5QTn3LHMA==",
- "dependencies": {
- "unist-util-visit": "^2.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/unist-util-stringify-position": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz",
- "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==",
- "dependencies": {
- "@types/unist": "^2.0.2"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/unist-util-visit": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz",
- "integrity": "sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==",
- "dependencies": {
- "@types/unist": "^2.0.0",
- "unist-util-is": "^4.0.0",
- "unist-util-visit-parents": "^3.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/unist-util-visit-parents": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz",
- "integrity": "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==",
- "dependencies": {
- "@types/unist": "^2.0.0",
- "unist-util-is": "^4.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/universalify": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
- "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
- "engines": {
- "node": ">= 10.0.0"
- }
- },
- "node_modules/unpipe": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
- "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/update-browserslist-db": {
- "version": "1.0.11",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz",
- "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==",
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/browserslist"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "dependencies": {
- "escalade": "^3.1.1",
- "picocolors": "^1.0.0"
- },
- "bin": {
- "update-browserslist-db": "cli.js"
- },
- "peerDependencies": {
- "browserslist": ">= 4.21.0"
- }
- },
- "node_modules/update-notifier": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-5.1.0.tgz",
- "integrity": "sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw==",
- "dependencies": {
- "boxen": "^5.0.0",
- "chalk": "^4.1.0",
- "configstore": "^5.0.1",
- "has-yarn": "^2.1.0",
- "import-lazy": "^2.1.0",
- "is-ci": "^2.0.0",
- "is-installed-globally": "^0.4.0",
- "is-npm": "^5.0.0",
- "is-yarn-global": "^0.3.0",
- "latest-version": "^5.1.0",
- "pupa": "^2.1.1",
- "semver": "^7.3.4",
- "semver-diff": "^3.1.1",
- "xdg-basedir": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/yeoman/update-notifier?sponsor=1"
- }
- },
- "node_modules/update-notifier/node_modules/boxen": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz",
- "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==",
- "dependencies": {
- "ansi-align": "^3.0.0",
- "camelcase": "^6.2.0",
- "chalk": "^4.1.0",
- "cli-boxes": "^2.2.1",
- "string-width": "^4.2.2",
- "type-fest": "^0.20.2",
- "widest-line": "^3.1.0",
- "wrap-ansi": "^7.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/update-notifier/node_modules/cli-boxes": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz",
- "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==",
- "engines": {
- "node": ">=6"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/update-notifier/node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
- },
- "node_modules/update-notifier/node_modules/string-width": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/update-notifier/node_modules/type-fest": {
- "version": "0.20.2",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
- "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/update-notifier/node_modules/widest-line": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz",
- "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==",
- "dependencies": {
- "string-width": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/update-notifier/node_modules/wrap-ansi": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
- "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
- "dependencies": {
- "ansi-styles": "^4.0.0",
- "string-width": "^4.1.0",
- "strip-ansi": "^6.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
- "node_modules/uri-js": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
- "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
- "dependencies": {
- "punycode": "^2.1.0"
- }
- },
- "node_modules/uri-js/node_modules/punycode": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
- "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/url-loader": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz",
- "integrity": "sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==",
- "dependencies": {
- "loader-utils": "^2.0.0",
- "mime-types": "^2.1.27",
- "schema-utils": "^3.0.0"
- },
- "engines": {
- "node": ">= 10.13.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- },
- "peerDependencies": {
- "file-loader": "*",
- "webpack": "^4.0.0 || ^5.0.0"
- },
- "peerDependenciesMeta": {
- "file-loader": {
- "optional": true
- }
- }
- },
- "node_modules/url-loader/node_modules/mime-db": {
- "version": "1.52.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
- "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/url-loader/node_modules/mime-types": {
- "version": "2.1.35",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
- "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
- "dependencies": {
- "mime-db": "1.52.0"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/url-loader/node_modules/schema-utils": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
- "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
- "dependencies": {
- "@types/json-schema": "^7.0.8",
- "ajv": "^6.12.5",
- "ajv-keywords": "^3.5.2"
- },
- "engines": {
- "node": ">= 10.13.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- }
- },
- "node_modules/url-parse-lax": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
- "integrity": "sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==",
- "dependencies": {
- "prepend-http": "^2.0.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/use-composed-ref": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.3.0.tgz",
- "integrity": "sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==",
- "peerDependencies": {
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
- }
- },
- "node_modules/use-isomorphic-layout-effect": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz",
- "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==",
- "peerDependencies": {
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/use-latest": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.2.1.tgz",
- "integrity": "sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw==",
- "dependencies": {
- "use-isomorphic-layout-effect": "^1.1.1"
- },
- "peerDependencies": {
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/use-sync-external-store": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
- "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
- "peerDependencies": {
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
- }
- },
- "node_modules/util-deprecate": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
- "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
- },
- "node_modules/utila": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz",
- "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA=="
- },
- "node_modules/utility-types": {
- "version": "3.10.0",
- "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz",
- "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==",
- "engines": {
- "node": ">= 4"
- }
- },
- "node_modules/utils-merge": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
- "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
- "engines": {
- "node": ">= 0.4.0"
- }
- },
- "node_modules/uuid": {
- "version": "8.3.2",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
- "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
- "bin": {
- "uuid": "dist/bin/uuid"
- }
- },
- "node_modules/value-equal": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
- "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="
- },
- "node_modules/vary": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
- "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/vfile": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz",
- "integrity": "sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==",
- "dependencies": {
- "@types/unist": "^2.0.0",
- "is-buffer": "^2.0.0",
- "unist-util-stringify-position": "^2.0.0",
- "vfile-message": "^2.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/vfile-location": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-3.2.0.tgz",
- "integrity": "sha512-aLEIZKv/oxuCDZ8lkJGhuhztf/BW4M+iHdCwglA/eWc+vtuRFJj8EtgceYFX4LRjOhCAAiNHsKGssC6onJ+jbA==",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/vfile-message": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz",
- "integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==",
- "dependencies": {
- "@types/unist": "^2.0.0",
- "unist-util-stringify-position": "^2.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/wait-on": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-6.0.1.tgz",
- "integrity": "sha512-zht+KASY3usTY5u2LgaNqn/Cd8MukxLGjdcZxT2ns5QzDmTFc4XoWBgC+C/na+sMRZTuVygQoMYwdcVjHnYIVw==",
- "dependencies": {
- "axios": "^0.25.0",
- "joi": "^17.6.0",
- "lodash": "^4.17.21",
- "minimist": "^1.2.5",
- "rxjs": "^7.5.4"
- },
- "bin": {
- "wait-on": "bin/wait-on"
- },
- "engines": {
- "node": ">=10.0.0"
- }
- },
- "node_modules/watchpack": {
- "version": "2.4.0",
- "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
- "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==",
- "dependencies": {
- "glob-to-regexp": "^0.4.1",
- "graceful-fs": "^4.1.2"
- },
- "engines": {
- "node": ">=10.13.0"
- }
- },
- "node_modules/wbuf": {
- "version": "1.7.3",
- "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz",
- "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==",
- "dependencies": {
- "minimalistic-assert": "^1.0.0"
- }
- },
- "node_modules/web-namespaces": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-1.1.4.tgz",
- "integrity": "sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/webidl-conversions": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
- "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
- },
- "node_modules/webpack": {
- "version": "5.87.0",
- "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.87.0.tgz",
- "integrity": "sha512-GOu1tNbQ7p1bDEoFRs2YPcfyGs8xq52yyPBZ3m2VGnXGtV9MxjrkABHm4V9Ia280OefsSLzvbVoXcfLxjKY/Iw==",
- "dependencies": {
- "@types/eslint-scope": "^3.7.3",
- "@types/estree": "^1.0.0",
- "@webassemblyjs/ast": "^1.11.5",
- "@webassemblyjs/wasm-edit": "^1.11.5",
- "@webassemblyjs/wasm-parser": "^1.11.5",
- "acorn": "^8.7.1",
- "acorn-import-assertions": "^1.9.0",
- "browserslist": "^4.14.5",
- "chrome-trace-event": "^1.0.2",
- "enhanced-resolve": "^5.15.0",
- "es-module-lexer": "^1.2.1",
- "eslint-scope": "5.1.1",
- "events": "^3.2.0",
- "glob-to-regexp": "^0.4.1",
- "graceful-fs": "^4.2.9",
- "json-parse-even-better-errors": "^2.3.1",
- "loader-runner": "^4.2.0",
- "mime-types": "^2.1.27",
- "neo-async": "^2.6.2",
- "schema-utils": "^3.2.0",
- "tapable": "^2.1.1",
- "terser-webpack-plugin": "^5.3.7",
- "watchpack": "^2.4.0",
- "webpack-sources": "^3.2.3"
- },
- "bin": {
- "webpack": "bin/webpack.js"
- },
- "engines": {
- "node": ">=10.13.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- },
- "peerDependenciesMeta": {
- "webpack-cli": {
- "optional": true
- }
- }
- },
- "node_modules/webpack-bundle-analyzer": {
- "version": "4.9.0",
- "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.9.0.tgz",
- "integrity": "sha512-+bXGmO1LyiNx0i9enBu3H8mv42sj/BJWhZNFwjz92tVnBa9J3JMGo2an2IXlEleoDOPn/Hofl5hr/xCpObUDtw==",
- "dependencies": {
- "@discoveryjs/json-ext": "0.5.7",
- "acorn": "^8.0.4",
- "acorn-walk": "^8.0.0",
- "chalk": "^4.1.0",
- "commander": "^7.2.0",
- "gzip-size": "^6.0.0",
- "lodash": "^4.17.20",
- "opener": "^1.5.2",
- "sirv": "^1.0.7",
- "ws": "^7.3.1"
- },
- "bin": {
- "webpack-bundle-analyzer": "lib/bin/analyzer.js"
- },
- "engines": {
- "node": ">= 10.13.0"
- }
- },
- "node_modules/webpack-bundle-analyzer/node_modules/commander": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
- "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/webpack-dev-middleware": {
- "version": "5.3.3",
- "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz",
- "integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==",
- "dependencies": {
- "colorette": "^2.0.10",
- "memfs": "^3.4.3",
- "mime-types": "^2.1.31",
- "range-parser": "^1.2.1",
- "schema-utils": "^4.0.0"
- },
- "engines": {
- "node": ">= 12.13.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- },
- "peerDependencies": {
- "webpack": "^4.0.0 || ^5.0.0"
- }
- },
- "node_modules/webpack-dev-middleware/node_modules/ajv": {
- "version": "8.12.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
- "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
- "dependencies": {
- "fast-deep-equal": "^3.1.1",
- "json-schema-traverse": "^1.0.0",
- "require-from-string": "^2.0.2",
- "uri-js": "^4.2.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/webpack-dev-middleware/node_modules/ajv-keywords": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
- "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
- "dependencies": {
- "fast-deep-equal": "^3.1.3"
- },
- "peerDependencies": {
- "ajv": "^8.8.2"
- }
- },
- "node_modules/webpack-dev-middleware/node_modules/json-schema-traverse": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
- "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
- },
- "node_modules/webpack-dev-middleware/node_modules/mime-db": {
- "version": "1.52.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
- "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/webpack-dev-middleware/node_modules/mime-types": {
- "version": "2.1.35",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
- "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
- "dependencies": {
- "mime-db": "1.52.0"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/webpack-dev-middleware/node_modules/range-parser": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
- "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/webpack-dev-middleware/node_modules/schema-utils": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz",
- "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==",
- "dependencies": {
- "@types/json-schema": "^7.0.9",
- "ajv": "^8.9.0",
- "ajv-formats": "^2.1.1",
- "ajv-keywords": "^5.1.0"
- },
- "engines": {
- "node": ">= 12.13.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- }
- },
- "node_modules/webpack-dev-server": {
- "version": "4.15.1",
- "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz",
- "integrity": "sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==",
- "dependencies": {
- "@types/bonjour": "^3.5.9",
- "@types/connect-history-api-fallback": "^1.3.5",
- "@types/express": "^4.17.13",
- "@types/serve-index": "^1.9.1",
- "@types/serve-static": "^1.13.10",
- "@types/sockjs": "^0.3.33",
- "@types/ws": "^8.5.5",
- "ansi-html-community": "^0.0.8",
- "bonjour-service": "^1.0.11",
- "chokidar": "^3.5.3",
- "colorette": "^2.0.10",
- "compression": "^1.7.4",
- "connect-history-api-fallback": "^2.0.0",
- "default-gateway": "^6.0.3",
- "express": "^4.17.3",
- "graceful-fs": "^4.2.6",
- "html-entities": "^2.3.2",
- "http-proxy-middleware": "^2.0.3",
- "ipaddr.js": "^2.0.1",
- "launch-editor": "^2.6.0",
- "open": "^8.0.9",
- "p-retry": "^4.5.0",
- "rimraf": "^3.0.2",
- "schema-utils": "^4.0.0",
- "selfsigned": "^2.1.1",
- "serve-index": "^1.9.1",
- "sockjs": "^0.3.24",
- "spdy": "^4.0.2",
- "webpack-dev-middleware": "^5.3.1",
- "ws": "^8.13.0"
- },
- "bin": {
- "webpack-dev-server": "bin/webpack-dev-server.js"
- },
- "engines": {
- "node": ">= 12.13.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- },
- "peerDependencies": {
- "webpack": "^4.37.0 || ^5.0.0"
- },
- "peerDependenciesMeta": {
- "webpack": {
- "optional": true
- },
- "webpack-cli": {
- "optional": true
- }
- }
- },
- "node_modules/webpack-dev-server/node_modules/ajv": {
- "version": "8.12.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
- "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
- "dependencies": {
- "fast-deep-equal": "^3.1.1",
- "json-schema-traverse": "^1.0.0",
- "require-from-string": "^2.0.2",
- "uri-js": "^4.2.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/webpack-dev-server/node_modules/ajv-keywords": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
- "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
- "dependencies": {
- "fast-deep-equal": "^3.1.3"
- },
- "peerDependencies": {
- "ajv": "^8.8.2"
- }
- },
- "node_modules/webpack-dev-server/node_modules/json-schema-traverse": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
- "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
- },
- "node_modules/webpack-dev-server/node_modules/schema-utils": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz",
- "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==",
- "dependencies": {
- "@types/json-schema": "^7.0.9",
- "ajv": "^8.9.0",
- "ajv-formats": "^2.1.1",
- "ajv-keywords": "^5.1.0"
- },
- "engines": {
- "node": ">= 12.13.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- }
- },
- "node_modules/webpack-dev-server/node_modules/ws": {
- "version": "8.13.0",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
- "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
- "engines": {
- "node": ">=10.0.0"
- },
- "peerDependencies": {
- "bufferutil": "^4.0.1",
- "utf-8-validate": ">=5.0.2"
- },
- "peerDependenciesMeta": {
- "bufferutil": {
- "optional": true
- },
- "utf-8-validate": {
- "optional": true
- }
- }
- },
- "node_modules/webpack-merge": {
- "version": "5.9.0",
- "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.9.0.tgz",
- "integrity": "sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg==",
- "dependencies": {
- "clone-deep": "^4.0.1",
- "wildcard": "^2.0.0"
- },
- "engines": {
- "node": ">=10.0.0"
- }
- },
- "node_modules/webpack-sources": {
- "version": "3.2.3",
- "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
- "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
- "engines": {
- "node": ">=10.13.0"
- }
- },
- "node_modules/webpack/node_modules/mime-db": {
- "version": "1.52.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
- "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/webpack/node_modules/mime-types": {
- "version": "2.1.35",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
- "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
- "dependencies": {
- "mime-db": "1.52.0"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/webpack/node_modules/schema-utils": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
- "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
- "dependencies": {
- "@types/json-schema": "^7.0.8",
- "ajv": "^6.12.5",
- "ajv-keywords": "^3.5.2"
- },
- "engines": {
- "node": ">= 10.13.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- }
- },
- "node_modules/webpackbar": {
- "version": "5.0.2",
- "resolved": "https://registry.npmjs.org/webpackbar/-/webpackbar-5.0.2.tgz",
- "integrity": "sha512-BmFJo7veBDgQzfWXl/wwYXr/VFus0614qZ8i9znqcl9fnEdiVkdbi0TedLQ6xAK92HZHDJ0QmyQ0fmuZPAgCYQ==",
- "dependencies": {
- "chalk": "^4.1.0",
- "consola": "^2.15.3",
- "pretty-time": "^1.1.0",
- "std-env": "^3.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "peerDependencies": {
- "webpack": "3 || 4 || 5"
- }
- },
- "node_modules/websocket-driver": {
- "version": "0.7.4",
- "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",
- "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==",
- "dependencies": {
- "http-parser-js": ">=0.5.1",
- "safe-buffer": ">=5.1.0",
- "websocket-extensions": ">=0.1.1"
- },
- "engines": {
- "node": ">=0.8.0"
- }
- },
- "node_modules/websocket-extensions": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz",
- "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==",
- "engines": {
- "node": ">=0.8.0"
- }
- },
- "node_modules/whatwg-url": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
- "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
- "dependencies": {
- "tr46": "~0.0.3",
- "webidl-conversions": "^3.0.0"
- }
- },
- "node_modules/which": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
- "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
- "dependencies": {
- "isexe": "^2.0.0"
- },
- "bin": {
- "node-which": "bin/node-which"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/widest-line": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz",
- "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==",
- "dependencies": {
- "string-width": "^5.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/wildcard": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz",
- "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ=="
- },
- "node_modules/wrap-ansi": {
- "version": "8.1.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
- "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
- "dependencies": {
- "ansi-styles": "^6.1.0",
- "string-width": "^5.0.1",
- "strip-ansi": "^7.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
- "node_modules/wrap-ansi/node_modules/ansi-regex": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
- "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-regex?sponsor=1"
- }
- },
- "node_modules/wrap-ansi/node_modules/ansi-styles": {
- "version": "6.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
- "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/wrap-ansi/node_modules/strip-ansi": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
- "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
- "dependencies": {
- "ansi-regex": "^6.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/strip-ansi?sponsor=1"
- }
- },
- "node_modules/wrappy": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
- },
- "node_modules/write-file-atomic": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz",
- "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==",
- "dependencies": {
- "imurmurhash": "^0.1.4",
- "is-typedarray": "^1.0.0",
- "signal-exit": "^3.0.2",
- "typedarray-to-buffer": "^3.1.5"
- }
- },
- "node_modules/ws": {
- "version": "7.5.9",
- "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
- "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
- "engines": {
- "node": ">=8.3.0"
- },
- "peerDependencies": {
- "bufferutil": "^4.0.1",
- "utf-8-validate": "^5.0.2"
- },
- "peerDependenciesMeta": {
- "bufferutil": {
- "optional": true
- },
- "utf-8-validate": {
- "optional": true
- }
- }
- },
- "node_modules/xdg-basedir": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz",
- "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/xml-js": {
- "version": "1.6.11",
- "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
- "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
- "dependencies": {
- "sax": "^1.2.4"
- },
- "bin": {
- "xml-js": "bin/cli.js"
- }
- },
- "node_modules/xtend": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
- "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
- "engines": {
- "node": ">=0.4"
- }
- },
- "node_modules/yallist": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
- "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
- },
- "node_modules/yaml": {
- "version": "1.10.2",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
- "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/yocto-queue": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
- "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/zwitch": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz",
- "integrity": "sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- }
- }
-}
diff --git a/docs/package.json b/docs/package.json
deleted file mode 100644
index f4a88bda..00000000
--- a/docs/package.json
+++ /dev/null
@@ -1,46 +0,0 @@
-{
- "name": "docs",
- "version": "0.0.0",
- "private": true,
- "scripts": {
- "docusaurus": "docusaurus",
- "start": "docusaurus start",
- "build": "docusaurus build",
- "swizzle": "docusaurus swizzle",
- "deploy": "docusaurus deploy",
- "clear": "docusaurus clear",
- "serve": "docusaurus serve",
- "write-translations": "docusaurus write-translations",
- "write-heading-ids": "docusaurus write-heading-ids",
- "typecheck": "tsc"
- },
- "dependencies": {
- "@docusaurus/core": "2.4.1",
- "@docusaurus/preset-classic": "2.4.1",
- "@mdx-js/react": "^1.6.22",
- "clsx": "^1.2.1",
- "prism-react-renderer": "^1.3.5",
- "react": "^17.0.2",
- "react-dom": "^17.0.2"
- },
- "devDependencies": {
- "@docusaurus/module-type-aliases": "2.4.1",
- "@tsconfig/docusaurus": "^1.0.5",
- "typescript": "^4.7.4"
- },
- "browserslist": {
- "production": [
- ">0.5%",
- "not dead",
- "not op_mini all"
- ],
- "development": [
- "last 1 chrome version",
- "last 1 firefox version",
- "last 1 safari version"
- ]
- },
- "engines": {
- "node": ">=16.14"
- }
-}
diff --git a/docs/sidebars.js b/docs/sidebars.js
deleted file mode 100644
index 9ab54c24..00000000
--- a/docs/sidebars.js
+++ /dev/null
@@ -1,33 +0,0 @@
-/**
- * Creating a sidebar enables you to:
- - create an ordered group of docs
- - render a sidebar for each doc of that group
- - provide next/previous navigation
-
- The sidebars can be generated from the filesystem, or explicitly defined here.
-
- Create as many sidebars as you want.
- */
-
-// @ts-check
-
-/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
-const sidebars = {
- // By default, Docusaurus generates a sidebar from the docs folder structure
- tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],
-
- // But you can create a sidebar manually
- /*
- tutorialSidebar: [
- 'intro',
- 'hello',
- {
- type: 'category',
- label: 'Tutorial',
- items: ['tutorial-basics/create-a-document'],
- },
- ],
- */
-};
-
-module.exports = sidebars;
diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css
deleted file mode 100644
index 0bf05042..00000000
--- a/docs/src/css/custom.css
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * Any CSS included here will be global. The classic template
- * bundles Infima by default. Infima is a CSS framework designed to
- * work well for content-centric websites.
- */
-
-/* You can override the default Infima variables here. */
-:root {
- --ifm-color-primary: #c13a3a;
- --ifm-color-primary-dark: #29784c;
- --ifm-color-primary-darker: #277148;
- --ifm-color-primary-darkest: #205d3b;
- --ifm-color-primary-light: #33925d;
- --ifm-color-primary-lighter: #359962;
- --ifm-color-primary-lightest: #3cad6e;
- --ifm-code-font-size: 95%;
- --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
-}
-
-/* For readability concerns, you should choose a lighter palette in dark mode. */
-[data-theme='dark'] {
- --ifm-color-primary: #ffa9a9;
- --ifm-color-primary-dark: #21af90;
- --ifm-color-primary-darker: #1fa588;
- --ifm-color-primary-darkest: #1a8870;
- --ifm-color-primary-light: #29d5b0;
- --ifm-color-primary-lighter: #32d8b4;
- --ifm-color-primary-lightest: #4fddbf;
- --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
-}
diff --git a/docs/src/pages/index.module.css b/docs/src/pages/index.module.css
deleted file mode 100644
index e69de29b..00000000
diff --git a/docs/src/pages/index.tsx b/docs/src/pages/index.tsx
deleted file mode 100644
index 55dd6021..00000000
--- a/docs/src/pages/index.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import React from 'react';
-import clsx from 'clsx';
-import Link from '@docusaurus/Link';
-import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
-import Layout from '@theme/Layout';
-import {Redirect} from '@docusaurus/router';
-
-export default function Home(): JSX.Element {
- return (
-
- );
-}
diff --git a/docs/static/.nojekyll b/docs/static/.nojekyll
deleted file mode 100644
index e69de29b..00000000
diff --git a/docs/static/img/banner.jpg b/docs/static/img/banner.jpg
deleted file mode 100644
index 0d6a7d15..00000000
Binary files a/docs/static/img/banner.jpg and /dev/null differ
diff --git a/docs/static/img/favicon.ico b/docs/static/img/favicon.ico
deleted file mode 100644
index 7e919964..00000000
Binary files a/docs/static/img/favicon.ico and /dev/null differ
diff --git a/docs/static/img/latest-development-build.jpeg b/docs/static/img/latest-development-build.jpeg
deleted file mode 100644
index 4ba2551c..00000000
Binary files a/docs/static/img/latest-development-build.jpeg and /dev/null differ
diff --git a/docs/tsconfig.json b/docs/tsconfig.json
deleted file mode 100644
index 6f475698..00000000
--- a/docs/tsconfig.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- // This file is not used in compilation. It is here just for a nice editor experience.
- "extends": "@tsconfig/docusaurus/tsconfig.json",
- "compilerOptions": {
- "baseUrl": "."
- }
-}
diff --git a/entitlements.plist b/entitlements.plist
deleted file mode 100644
index 279586c5..00000000
--- a/entitlements.plist
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
- com.apple.security.cs.allow-jit
-
- com.apple.security.cs.allow-unsigned-executable-memory
-
- com.apple.security.cs.disable-library-validation
-
- com.apple.security.device.audio-input
-
-
-
diff --git a/flatpak/run-buzz.sh b/flatpak/run-buzz.sh
deleted file mode 100644
index f32217ec..00000000
--- a/flatpak/run-buzz.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/bin/sh
-echo "Running buzz..."
-echo "Note: ffmpeg errors are safe to ignore"
-
-python -m buzz
\ No newline at end of file
diff --git a/gui.py b/gui.py
new file mode 100644
index 00000000..e6562fde
--- /dev/null
+++ b/gui.py
@@ -0,0 +1,323 @@
+import enum
+import platform
+from typing import Dict, List, Optional, Tuple
+
+import sounddevice
+import whisper
+from PyQt5.QtCore import *
+from PyQt5.QtGui import *
+from PyQt5.QtWidgets import *
+from whisper import tokenizer
+
+from transcriber import Transcriber
+
+
+def get_platform_styles(all_platform_styles: Dict[str, str]):
+ return all_platform_styles.get(platform.system(), '')
+
+
+class Label(QLabel):
+ os_styles = {
+ 'Darwin': 'QLabel { color: #ddd; }'
+ }
+
+ def __init__(self, name: str, *args) -> None:
+ super().__init__(name, *args)
+ self.setStyleSheet('QLabel { text-align: right; } %s' %
+ get_platform_styles(self.os_styles))
+ self.setAlignment(Qt.AlignmentFlag(
+ Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignRight))
+
+
+class AudioDevicesComboBox(QComboBox):
+ """AudioDevicesComboBox displays a list of available audio input devices"""
+ deviceChanged = pyqtSignal(int)
+ audio_devices: List[Tuple[int, str]]
+
+ def __init__(self, *args) -> None:
+ super().__init__(*args)
+ self.audio_devices = self.get_audio_devices()
+ self.addItems(map(lambda device: device[1], self.audio_devices))
+ self.currentIndexChanged.connect(self.on_index_changed)
+
+ def get_audio_devices(self) -> List[Tuple[int, str]]:
+ devices: sounddevice.DeviceList = sounddevice.query_devices()
+ input_devices = filter(
+ lambda device: device.get('max_input_channels') > 0, devices)
+ return list(map(lambda device: (device.get('index'), device.get('name')), input_devices))
+
+ def on_index_changed(self, index: int):
+ self.deviceChanged.emit(self.audio_devices[index][0])
+
+ def get_default_device_id(self) -> Optional[int]:
+ return self.audio_devices[0][0] if len(self.audio_devices) > 0 else None
+
+
+class LanguagesComboBox(QComboBox):
+ """LanguagesComboBox displays a list of languages available to use with Whisper"""
+ languageChanged = pyqtSignal(str)
+
+ def __init__(self, default_language: str, *args) -> None:
+ super().__init__(*args)
+
+ whisper_languages = sorted(
+ [(lang, tokenizer.LANGUAGES[lang].title())
+ for lang in tokenizer.LANGUAGES.keys()],
+ key=lambda lang: lang[1])
+ self.languages = [('', 'Detect Language')] + whisper_languages
+
+ self.addItems([lang[1] for lang in self.languages])
+ self.currentIndexChanged.connect(self.on_index_changed)
+ default_language_index = next((i for i, lang in enumerate(self.languages)
+ if lang[0] == default_language), 0)
+ self.setCurrentIndex(default_language_index)
+
+ def on_index_changed(self, index: int):
+ self.languageChanged.emit(self.languages[index][0])
+
+
+class TasksComboBox(QComboBox):
+ """TasksComboBox displays a list of tasks available to use with Whisper"""
+ taskChanged = pyqtSignal(Transcriber.Task)
+
+ def __init__(self, default_task: Transcriber.Task, *args) -> None:
+ super().__init__(*args)
+ self.tasks = [i for i in Transcriber.Task]
+ self.addItems(map(lambda task: task.value.title(), self.tasks))
+ self.currentIndexChanged.connect(self.on_index_changed)
+ self.setCurrentText(default_task.value.title())
+
+ def on_index_changed(self, index: int):
+ self.taskChanged.emit(self.tasks[index])
+
+
+class ModelsComboBox(QComboBox):
+ """ModelsComboBox displays the list of available Whisper models for selection"""
+ modelNameChanged = pyqtSignal(str)
+
+ def __init__(self, default_model_name: str, *args) -> None:
+ super().__init__(*args)
+ self.models = whisper.available_models()
+ self.addItems(map(self.label, self.models))
+ self.currentIndexChanged.connect(self.on_index_changed)
+ self.setCurrentText(self.label(default_model_name))
+
+ def on_index_changed(self, index: int):
+ self.modelNameChanged.emit(self.models[index])
+
+ def label(self, model_name: str):
+ name, lang = (model_name.split('.') + [None])[:2]
+ if lang:
+ return "%s (%s)" % (name.title(), lang.upper())
+ return name.title()
+
+
+class DelaysComboBox(QComboBox):
+ """DelaysComboBox displays the list of available delays"""
+ delay_changed = pyqtSignal(int)
+
+ def __init__(self, default_delay: int, *args) -> None:
+ super().__init__(*args)
+ self.delays = [5, 10, 20, 30]
+ self.addItems(map(self.label, self.delays))
+ self.currentIndexChanged.connect(self.on_index_changed)
+ self.setCurrentText(self.label(default_delay))
+
+ def on_index_changed(self, index: int):
+ self.delay_changed.emit(self.delays[index])
+
+ def label(self, delay: int):
+ return "%ds" % delay
+
+
+class TextDisplayBox(QTextEdit):
+ """TextDisplayBox is a read-only textbox"""
+
+ os_styles = {
+ 'Darwin': '''QTextEdit {
+ border-radius: 6;
+ background-color: #252525;
+ color: #dfdfdf;
+ }''',
+ 'Windows': 'QTextEdit { background-color: #ffffff; }'
+ }
+
+ def __init__(self, *args) -> None:
+ super().__init__(*args)
+ self.setReadOnly(True)
+ self.setPlaceholderText('Click Record to begin...')
+ self.setStyleSheet(
+ '''QTextEdit {
+ padding-left: 5;
+ padding-top: 5;
+ padding-bottom: 5;
+ padding-right: 5;
+ } %s''' % get_platform_styles(self.os_styles))
+
+
+class RecordButton(QPushButton):
+ class Status(enum.Enum):
+ RECORDING = enum.auto()
+ STOPPED = enum.auto()
+
+ current_status = Status.STOPPED
+ statusChanged = pyqtSignal(Status)
+
+ def __init__(self, *args) -> None:
+ super().__init__("Record", *args)
+ self.clicked.connect(self.on_click_record)
+ self.statusChanged.connect(self.on_status_changed)
+ self.setDefault(True)
+
+ def on_click_record(self):
+ current_status: RecordButton.Status
+ if self.current_status == self.Status.RECORDING:
+ current_status = self.Status.STOPPED
+ else:
+ current_status = self.Status.RECORDING
+
+ self.statusChanged.emit(current_status)
+
+ def on_status_changed(self, status: Status):
+ self.current_status = status
+ if status == self.Status.RECORDING:
+ self.setText('Stop')
+ self.setDefault(False)
+ else:
+ self.setText('Record')
+ self.setDefault(True)
+
+
+class TranscriberWithSignal(QObject):
+ """
+ TranscriberWithSignal exports the text callback from a Transcriber
+ as a QtSignal to allow updating the UI from a secondary thread.
+ """
+
+ text_changed = pyqtSignal(str)
+
+ def __init__(self, model_name: str, language: Optional[str], task: Transcriber.Task, *args) -> None:
+ super().__init__(*args)
+ self.transcriber = Transcriber(
+ model_name=model_name, language=language,
+ text_callback=self.on_next_text, task=task)
+
+ def start_recording(self, input_device_index: Optional[int], block_duration: int):
+ self.transcriber.start_recording(
+ input_device_index=input_device_index,
+ block_duration=block_duration,
+ )
+
+ def on_next_text(self, text: str):
+ self.text_changed.emit(text.strip())
+
+ def stop_recording(self):
+ self.transcriber.stop_recording()
+
+
+class Application(QApplication):
+ current_status = RecordButton.Status.STOPPED
+ selected_model_name = 'tiny'
+ selected_language = 'en'
+ selected_device_id: Optional[int]
+ selected_delay = 10
+ selected_task = Transcriber.Task.TRANSCRIBE
+
+ def __init__(self) -> None:
+ super().__init__([])
+
+ self.window = QWidget()
+ self.window.setFixedSize(400, 500)
+ self.window.setWindowTitle('Buzz')
+
+ layout = QGridLayout()
+ self.window.setLayout(layout)
+
+ self.models_combo_box = ModelsComboBox(
+ default_model_name=self.selected_model_name)
+ self.models_combo_box.modelNameChanged.connect(self.on_model_changed)
+
+ self.languages_combo_box = LanguagesComboBox(
+ default_language=self.selected_language)
+ self.languages_combo_box.languageChanged.connect(
+ self.on_language_changed)
+
+ self.audio_devices_combo_box = AudioDevicesComboBox()
+ self.audio_devices_combo_box.deviceChanged.connect(
+ self.on_device_changed)
+ self.selected_device_id = self.audio_devices_combo_box.get_default_device_id()
+
+ self.tasks_combo_box = TasksComboBox(
+ default_task=Transcriber.Task.TRANSCRIBE)
+ self.tasks_combo_box.taskChanged.connect(self.on_task_changed)
+
+ delays_combo_box = DelaysComboBox(default_delay=self.selected_delay)
+ delays_combo_box.delay_changed.connect(self.on_delay_changed)
+
+ record_button = RecordButton()
+ record_button.statusChanged.connect(self.on_status_changed)
+
+ self.text_box = TextDisplayBox()
+
+ grid = (
+ ((0, 5, Label('Model:')), (5, 7, self.models_combo_box)),
+ ((0, 5, Label('Language:')), (5, 7, self.languages_combo_box)),
+ ((0, 5, Label('Task:')), (5, 7, self.tasks_combo_box)),
+ ((0, 5, Label('Microphone:')), (5, 7, self.audio_devices_combo_box)),
+ ((0, 5, Label('Delay:')), (5, 7, delays_combo_box)),
+ ((9, 3, record_button),),
+ ((0, 12, self.text_box),),
+ )
+
+ for (row_index, row) in enumerate(grid):
+ for (_, cell) in enumerate(row):
+ (col_offset, col_width, widget) = cell
+ layout.addWidget(widget, row_index, col_offset, 1, col_width)
+
+ self.window.show()
+
+ # TODO: might be great to send when the text has been updated rather than appending
+ def on_text_changed(self, text: str):
+ if len(text) > 0:
+ self.text_box.moveCursor(QTextCursor.MoveOperation.End)
+ self.text_box.insertPlainText(text + '\n\n')
+ self.text_box.moveCursor(QTextCursor.MoveOperation.End)
+
+ def on_device_changed(self, device_id: int):
+ self.selected_device_id = device_id
+
+ def on_status_changed(self, status: RecordButton.Status):
+ if status == RecordButton.Status.RECORDING:
+ self.start_recording()
+ else:
+ self.stop_recording()
+
+ def on_model_changed(self, model_name: str):
+ self.selected_model_name = model_name
+
+ def on_language_changed(self, language: str):
+ self.selected_language = language
+
+ def on_task_changed(self, task: Transcriber.Task):
+ self.selected_task = task
+
+ def on_delay_changed(self, delay: int):
+ self.selected_delay = delay
+
+ def start_recording(self):
+ # Clear text box placeholder because the first chunk takes a while to process
+ self.text_box.setPlaceholderText('')
+
+ self.transcriber = TranscriberWithSignal(
+ model_name=self.selected_model_name,
+ language=self.selected_language if self.selected_language != '' else None,
+ task=self.selected_task,
+ )
+ self.transcriber.text_changed.connect(self.on_text_changed)
+ self.transcriber.start_recording(
+ input_device_index=self.selected_device_id,
+ block_duration=self.selected_delay,
+ )
+
+ def stop_recording(self):
+ self.transcriber.stop_recording()
diff --git a/gui_test.py b/gui_test.py
new file mode 100644
index 00000000..84a2de17
--- /dev/null
+++ b/gui_test.py
@@ -0,0 +1,23 @@
+from gui import Application, LanguagesComboBox
+
+
+class TestApplication:
+ app = Application()
+
+ def test_should_show_window_title(self):
+ assert self.app.window.windowTitle() == 'Buzz'
+
+
+class TestLanguagesComboBox:
+ languagesComboxBox = LanguagesComboBox('en')
+
+ def test_should_show_sorted_whisper_languages(self):
+ assert self.languagesComboxBox.itemText(0) == 'Detect Language'
+ assert self.languagesComboxBox.itemText(10) == 'Belarusian'
+ assert self.languagesComboxBox.itemText(20) == 'Dutch'
+ assert self.languagesComboxBox.itemText(30) == 'Gujarati'
+ assert self.languagesComboxBox.itemText(40) == 'Japanese'
+ assert self.languagesComboxBox.itemText(50) == 'Lithuanian'
+
+ def test_should_select_default_language(self):
+ assert self.languagesComboxBox.currentText() == 'English'
diff --git a/hatch_build.py b/hatch_build.py
deleted file mode 100644
index 0aeeab4c..00000000
--- a/hatch_build.py
+++ /dev/null
@@ -1,227 +0,0 @@
-"""Custom build hook for hatchling to build whisper.cpp binaries."""
-import glob
-import subprocess
-import sys
-from pathlib import Path
-
-from hatchling.builders.hooks.plugin.interface import BuildHookInterface
-
-
-class CustomBuildHook(BuildHookInterface):
- """Build hook to compile whisper.cpp before building the package."""
-
- def initialize(self, version, build_data):
- """Run make buzz/whisper_cpp before building."""
- print("Running 'make buzz/whisper_cpp' to build whisper.cpp binaries...")
-
- # Mark wheel as platform-specific since we're including compiled binaries
- # But set tag to py3-none since binaries are standalone (no Python C extensions)
- if version == "standard": # Only for wheel builds
- import platform
-
- build_data["pure_python"] = False
-
- # Determine the platform tag based on current OS and architecture
- system = platform.system().lower()
- machine = platform.machine().lower()
-
- if system == "linux":
- if machine in ("x86_64", "amd64"):
- tag = "py3-none-manylinux_2_34_x86_64"
- else:
- raise ValueError(f"Unsupported Linux architecture: {machine}. Only x86_64 is supported.")
- elif system == "darwin":
- if machine in ("x86_64", "amd64"):
- tag = "py3-none-macosx_10_9_x86_64"
- elif machine in ("arm64", "aarch64"):
- tag = "py3-none-macosx_11_0_arm64"
- else:
- raise ValueError(f"Unsupported macOS architecture: {machine}")
- elif system == "windows":
- if machine in ("x86_64", "amd64"):
- tag = "py3-none-win_amd64"
- else:
- raise ValueError(f"Unsupported Windows architecture: {machine}. Only x86_64 is supported.")
- else:
- raise ValueError(f"Unsupported operating system: {system}")
-
- if tag:
- build_data["tag"] = tag
- print(f"Building wheel with tag: {tag}")
-
- # Get the project root directory
- project_root = Path(self.root)
-
- try:
- # Run the make command
- result = subprocess.run(
- ["make", "buzz/whisper_cpp"],
- cwd=project_root,
- check=True,
- capture_output=True,
- text=True
- )
- print(result.stdout)
- if result.stderr:
- print(result.stderr, file=sys.stderr)
- print("Successfully built whisper.cpp binaries")
-
- # Run the make command for translation files
- result = subprocess.run(
- ["make", "translation_mo"],
- cwd=project_root,
- check=True,
- capture_output=True,
- text=True
- )
- print(result.stdout)
- if result.stderr:
- print(result.stderr, file=sys.stderr)
- print("Successfully compiled translation files")
-
- # Build ctc_forced_aligner C++ extension in-place
- print("Building ctc_forced_aligner C++ extension...")
- ctc_aligner_dir = project_root / "ctc_forced_aligner"
-
- # Apply local patches before building.
- # Uses --check first to avoid touching the working tree unnecessarily,
- # which is safer in a detached-HEAD submodule.
- patches_dir = project_root / "patches"
- for patch_file in sorted(patches_dir.glob("ctc_forced_aligner_*.patch")):
- # Dry-run forward: succeeds only if patch is NOT yet applied.
- check_forward = subprocess.run(
- ["git", "apply", "--check", "--ignore-whitespace", str(patch_file)],
- cwd=ctc_aligner_dir,
- capture_output=True,
- text=True,
- )
- if check_forward.returncode == 0:
- # Patch can be applied — do it for real.
- subprocess.run(
- ["git", "apply", "--ignore-whitespace", str(patch_file)],
- cwd=ctc_aligner_dir,
- check=True,
- capture_output=True,
- text=True,
- )
- print(f"Applied patch: {patch_file.name}")
- else:
- # Dry-run failed — either already applied or genuinely broken.
- check_reverse = subprocess.run(
- ["git", "apply", "--check", "--reverse", "--ignore-whitespace", str(patch_file)],
- cwd=ctc_aligner_dir,
- capture_output=True,
- text=True,
- )
- if check_reverse.returncode == 0:
- print(f"Patch already applied (skipping): {patch_file.name}")
- else:
- print(f"WARNING: could not apply patch {patch_file.name}: {check_forward.stderr}", file=sys.stderr)
-
- result = subprocess.run(
- [sys.executable, "setup.py", "build_ext", "--inplace"],
- cwd=ctc_aligner_dir,
- check=True,
- capture_output=True,
- text=True
- )
- print(result.stdout)
- if result.stderr:
- print(result.stderr, file=sys.stderr)
- print("Successfully built ctc_forced_aligner C++ extension")
-
- # Force include all files in buzz/whisper_cpp directory
- whisper_cpp_dir = project_root / "buzz" / "whisper_cpp"
- if whisper_cpp_dir.exists():
- # Get all files in the whisper_cpp directory
- whisper_files = glob.glob(str(whisper_cpp_dir / "**" / "*"), recursive=True)
-
- # Filter only files (not directories)
- whisper_files = [f for f in whisper_files if Path(f).is_file()]
-
- # Add them to force_include
- if 'force_include' not in build_data:
- build_data['force_include'] = {}
-
- for file_path in whisper_files:
- # Convert to relative path from project root
- rel_path = Path(file_path).relative_to(project_root)
- build_data['force_include'][str(rel_path)] = str(rel_path)
-
- print(f"Force including {len(whisper_files)} files from buzz/whisper_cpp/")
- else:
- print(f"Warning: {whisper_cpp_dir} does not exist after build", file=sys.stderr)
-
- # Force include demucs package at top level (demucs_repo/demucs -> demucs/)
- demucs_pkg_dir = project_root / "demucs_repo" / "demucs"
- if demucs_pkg_dir.exists():
- # Get all files in the demucs package directory
- demucs_files = glob.glob(str(demucs_pkg_dir / "**" / "*"), recursive=True)
-
- # Filter only files (not directories)
- demucs_files = [f for f in demucs_files if Path(f).is_file()]
-
- # Add them to force_include, mapping to top-level demucs/
- if 'force_include' not in build_data:
- build_data['force_include'] = {}
-
- for file_path in demucs_files:
- # Convert to relative path from demucs package dir
- rel_from_pkg = Path(file_path).relative_to(demucs_pkg_dir)
- # Target path is demucs/
- target_path = Path("demucs") / rel_from_pkg
- build_data['force_include'][str(file_path)] = str(target_path)
-
- print(f"Force including {len(demucs_files)} files from demucs_repo/demucs/ -> demucs/")
- else:
- print(f"Warning: {demucs_pkg_dir} does not exist", file=sys.stderr)
-
- # Force include all .mo files from buzz/locale directory
- locale_dir = project_root / "buzz" / "locale"
- if locale_dir.exists():
- # Get all .mo files in the locale directory
- locale_files = glob.glob(str(locale_dir / "**" / "*.mo"), recursive=True)
-
- # Add them to force_include
- if 'force_include' not in build_data:
- build_data['force_include'] = {}
-
- for file_path in locale_files:
- # Convert to relative path from project root
- rel_path = Path(file_path).relative_to(project_root)
- build_data['force_include'][str(rel_path)] = str(rel_path)
-
- print(f"Force including {len(locale_files)} .mo files from buzz/locale/")
- else:
- print(f"Warning: {locale_dir} does not exist", file=sys.stderr)
-
- # Force include compiled extensions from ctc_forced_aligner
- ctc_aligner_pkg = project_root / "ctc_forced_aligner" / "ctc_forced_aligner"
- if ctc_aligner_pkg.exists():
- # Get all compiled extension files (.so, .pyd, .dll)
- extension_patterns = ["*.so", "*.pyd", "*.dll"]
- extension_files = []
- for pattern in extension_patterns:
- extension_files.extend(glob.glob(str(ctc_aligner_pkg / pattern)))
-
- # Add them to force_include
- if 'force_include' not in build_data:
- build_data['force_include'] = {}
-
- for file_path in extension_files:
- # Convert to relative path from project root
- rel_path = Path(file_path).relative_to(project_root)
- build_data['force_include'][str(rel_path)] = str(rel_path)
-
- print(f"Force including {len(extension_files)} compiled extension(s) from ctc_forced_aligner/")
- else:
- print(f"Warning: {ctc_aligner_pkg} does not exist", file=sys.stderr)
-
- except subprocess.CalledProcessError as e:
- print(f"Error building whisper.cpp: {e}", file=sys.stderr)
- print(f"stdout: {e.stdout}", file=sys.stderr)
- print(f"stderr: {e.stderr}", file=sys.stderr)
- sys.exit(1)
- except FileNotFoundError:
- print("Error: 'make' command not found. Please ensure make is installed.", file=sys.stderr)
- sys.exit(1)
diff --git a/installer.iss b/installer.iss
deleted file mode 100644
index 85b690d0..00000000
--- a/installer.iss
+++ /dev/null
@@ -1,88 +0,0 @@
-; Script generated by the Inno Setup Script Wizard.
-; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
-
-#define AppName "Buzz"
-#define AppExeName "Buzz.exe"
-#define AppIconPath "assets\buzz.ico"
-#define AppSourcePath "dist\Buzz\*"
-#define OutputDir "dist"
-#define AppRegKey "Software\Buzz"
-
-#define VersionFile FileRead(FileOpen("buzz\__version__.py"))
-#define AppVersion Copy(VersionFile, Pos('VERSION = "', VersionFile) + 11, 5)
-
-[Setup]
-; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
-; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
-AppId={{574290A2-EF7C-4845-85F3-BFF2F011A580}
-AppName={#AppName}
-AppVersion={#AppVersion}
-DefaultDirName={autopf}\{#AppName}
-DisableProgramGroupPage=yes
-; Uncomment the following line to run in non administrative install mode (install for current user only.)
-;PrivilegesRequired=lowest
-PrivilegesRequiredOverridesAllowed=dialog
-OutputDir={#OutputDir}
-OutputBaseFilename={#AppName}-{#AppVersion}-windows
-SetupIconFile={#AppIconPath}
-DiskSpanning=yes
-Compression=lzma
-SolidCompression=yes
-WizardStyle=modern
-
-[Languages]
-Name: "english"; MessagesFile: "compiler:Default.isl"
-
-[Tasks]
-Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
-
-[Files]
-Source: {#AppSourcePath}; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
-; NOTE: Don't use "Flags: ignoreversion" on any shared system files
-
-[Icons]
-Name: "{autoprograms}\{#AppName}"; Filename: "{app}\{#AppExeName}"
-Name: "{autodesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Tasks: desktopicon
-
-[Run]
-Filename: "{app}\{#AppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(AppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
-
-[Registry]
-Root: HKCU; Subkey: "{#AppRegKey}"
-
-[Code]
-procedure DeleteFileOrFolder(FilePath: string);
-begin
- if FileExists(FilePath) then
- begin
- DeleteFile(FilePath);
- end
- else if DirExists(FilePath) then
- begin
- DelTree(FilePath, True, True, True);
- end;
-end;
-
-procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
-begin
- if CurUninstallStep = usPostUninstall then
- begin
- if RegKeyExists(HKEY_CURRENT_USER, '{#AppRegKey}') then
- if MsgBox('Do you want to delete Buzz settings and saved files?', mbConfirmation, MB_YESNO) = IDYES
- then
- begin
- RegDeleteKeyIncludingSubkeys(HKEY_CURRENT_USER, '{#AppRegKey}');
- // Remove model and cache directories
- DeleteFileOrFolder(ExpandConstant('{localappdata}\Buzz'));
- end;
- end;
-end;
-
-procedure CurStepChanged(CurStep: TSetupStep);
-begin
- if CurStep = ssInstall then
- begin
- DeleteFileOrFolder(ExpandConstant('{app}\Buzz.exe'));
- DeleteFileOrFolder(ExpandConstant('{app}\_internal'));
- end;
-end;
\ No newline at end of file
diff --git a/main.py b/main.py
index 36656cc6..a25fd8fe 100644
--- a/main.py
+++ b/main.py
@@ -1,4 +1,10 @@
-import buzz.buzz
+import logging
-if __name__ == "__main__":
- buzz.buzz.main()
+from gui import Application
+
+logging.basicConfig(
+ level=logging.DEBUG,
+ format="[%(asctime)s] %(module)s.%(funcName)s:%(lineno)d %(levelname)s -> %(message)s")
+
+app = Application()
+app.exec()
diff --git a/msgfmt.py b/msgfmt.py
deleted file mode 100755
index 3dd21316..00000000
--- a/msgfmt.py
+++ /dev/null
@@ -1,67 +0,0 @@
-#!/usr/bin/env python
-"""
-Generate binary message catalog from textual translation description.
-
-This program converts a textual Uniforum-style message catalog (.po file) into
-a binary GNU catalog (.mo file). This is essentially the same function as the
-GNU msgfmt program, however, it is a simpler implementation.
-
-Usage: msgfmt.py [OPTIONS] filename.po
-
-Options:
- -o file
- --output-file=file
- Specify the output file to write to. If omitted, output will go to a
- file named filename.mo (based off the input file name).
-
- -h
- --help
- Print this message and exit.
-"""
-import getopt
-import sys
-import polib
-
-
-def usage(ecode, msg=""):
- """
- Print usage and msg and exit with given code.
- """
- print(__doc__, file=sys.stderr)
- if msg:
- print(msg, file=sys.stderr)
- sys.exit(ecode)
-
-
-def make(filename, outfile):
- po = polib.pofile(filename)
- po.save_as_mofile(outfile)
-
-
-def main():
- try:
- opts, args = getopt.getopt(
- sys.argv[1:], "ho:", ["help", "output-file="]
- )
- except getopt.error as msg:
- usage(1, msg)
-
- outfile = None
- # parse options
- for opt, arg in opts:
- if opt in ("-h", "--help"):
- usage(0)
- elif opt in ("-o", "--output-file"):
- outfile = arg
- # do it
- if not args:
- print("No input file given", file=sys.stderr)
- print("Try `msgfmt --help` for more information.", file=sys.stderr)
- return
-
- for filename in args:
- make(filename, outfile)
-
-
-if __name__ == "__main__":
- main()
diff --git a/patches/ctc_forced_aligner_windows_mutex.patch b/patches/ctc_forced_aligner_windows_mutex.patch
deleted file mode 100644
index 2940c9ab..00000000
--- a/patches/ctc_forced_aligner_windows_mutex.patch
+++ /dev/null
@@ -1,16 +0,0 @@
-diff --git a/setup.py b/setup.py
-index de84a25..386f662 100644
---- a/setup.py
-+++ b/setup.py
-@@ -6,7 +6,10 @@ ext_modules = [
- Pybind11Extension(
- "ctc_forced_aligner.ctc_forced_aligner",
- ["ctc_forced_aligner/forced_align_impl.cpp"],
-- extra_compile_args=["/O2"] if sys.platform == "win32" else ["-O3"],
-+ # /D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR prevents MSVC runtime mutex
-+ # static-initializer crash on newer GitHub Actions Windows runners.
-+ # See: https://github.com/actions/runner-images/issues/10004
-+ extra_compile_args=["/O2", "/D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR"] if sys.platform == "win32" else ["-O3"],
- )
- ]
-
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 00000000..efc7b410
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,1130 @@
+[[package]]
+name = "altgraph"
+version = "0.17.3"
+description = "Python graph (network) package"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "attrs"
+version = "22.1.0"
+description = "Classes Without Boilerplate"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[package.extras]
+dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"]
+docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"]
+tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"]
+tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]
+
+[[package]]
+name = "autopep8"
+version = "1.7.0"
+description = "A tool that automatically formats Python code to conform to the PEP 8 style guide"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+pycodestyle = ">=2.9.1"
+toml = "*"
+
+[[package]]
+name = "certifi"
+version = "2022.9.24"
+description = "Python package for providing Mozilla's CA Bundle."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "cffi"
+version = "1.15.1"
+description = "Foreign Function Interface for Python calling C code."
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+pycparser = "*"
+
+[[package]]
+name = "charset-normalizer"
+version = "2.1.1"
+description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
+category = "main"
+optional = false
+python-versions = ">=3.6.0"
+
+[package.extras]
+unicode_backport = ["unicodedata2"]
+
+[[package]]
+name = "colorama"
+version = "0.4.5"
+description = "Cross-platform colored terminal text."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[[package]]
+name = "coverage"
+version = "6.5.0"
+description = "Code coverage measurement for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
+
+[package.extras]
+toml = ["tomli"]
+
+[[package]]
+name = "ffmpeg"
+version = "1.4"
+description = "ffmpeg python package url [https://github.com/jiashaokun/ffmpeg]"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "ffmpeg-python"
+version = "0.2.0"
+description = "Python bindings for FFmpeg - with complex filtering support"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+future = "*"
+
+[package.extras]
+dev = ["Sphinx (==2.1.0)", "future (==0.17.1)", "numpy (==1.16.4)", "pytest (==4.6.1)", "pytest-mock (==1.10.4)", "tox (==3.12.1)"]
+
+[[package]]
+name = "filelock"
+version = "3.8.0"
+description = "A platform independent file lock."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+docs = ["furo (>=2022.6.21)", "sphinx (>=5.1.1)", "sphinx-autodoc-typehints (>=1.19.1)"]
+testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pytest-cov (>=3)", "pytest-timeout (>=2.1)"]
+
+[[package]]
+name = "future"
+version = "0.18.2"
+description = "Clean single-source support for Python 3 and 2"
+category = "main"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "huggingface-hub"
+version = "0.9.1"
+description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub"
+category = "main"
+optional = false
+python-versions = ">=3.7.0"
+
+[package.dependencies]
+filelock = "*"
+packaging = ">=20.9"
+pyyaml = ">=5.1"
+requests = "*"
+tqdm = "*"
+typing-extensions = ">=3.7.4.3"
+
+[package.extras]
+all = ["black (==22.3)", "datasets", "flake8 (>=3.8.3)", "flake8-bugbear", "isort (>=5.5.4)", "pytest", "pytest-cov", "soundfile"]
+dev = ["black (==22.3)", "datasets", "flake8 (>=3.8.3)", "flake8-bugbear", "isort (>=5.5.4)", "pytest", "pytest-cov", "soundfile"]
+fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"]
+quality = ["black (==22.3)", "flake8 (>=3.8.3)", "flake8-bugbear", "isort (>=5.5.4)"]
+tensorflow = ["graphviz", "pydot", "tensorflow"]
+testing = ["datasets", "pytest", "pytest-cov", "soundfile"]
+torch = ["torch"]
+
+[[package]]
+name = "idna"
+version = "3.4"
+description = "Internationalized Domain Names in Applications (IDNA)"
+category = "main"
+optional = false
+python-versions = ">=3.5"
+
+[[package]]
+name = "iniconfig"
+version = "1.1.1"
+description = "iniconfig: brain-dead simple config-ini parsing"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "macholib"
+version = "1.16.2"
+description = "Mach-O header analysis and editing"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+altgraph = ">=0.17"
+
+[[package]]
+name = "more-itertools"
+version = "8.14.0"
+description = "More routines for operating on iterables, beyond itertools"
+category = "main"
+optional = false
+python-versions = ">=3.5"
+
+[[package]]
+name = "numpy"
+version = "1.23.3"
+description = "NumPy is the fundamental package for array computing with Python."
+category = "main"
+optional = false
+python-versions = ">=3.8"
+
+[[package]]
+name = "packaging"
+version = "21.3"
+description = "Core utilities for Python packages"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
+
+[[package]]
+name = "pefile"
+version = "2022.5.30"
+description = "Python PE parsing module"
+category = "dev"
+optional = false
+python-versions = ">=3.6.0"
+
+[package.dependencies]
+future = "*"
+
+[[package]]
+name = "pluggy"
+version = "1.0.0"
+description = "plugin and hook calling mechanisms for python"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+testing = ["pytest", "pytest-benchmark"]
+
+[[package]]
+name = "py"
+version = "1.11.0"
+description = "library with cross-python path, ini-parsing, io, code, log facilities"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[[package]]
+name = "pycodestyle"
+version = "2.9.1"
+description = "Python style guide checker"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "pycparser"
+version = "2.21"
+description = "C parser in Python"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "pyinstaller"
+version = "5.4.1"
+description = "PyInstaller bundles a Python application and all its dependencies into a single package."
+category = "dev"
+optional = false
+python-versions = "<3.11,>=3.7"
+
+[package.dependencies]
+altgraph = "*"
+macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""}
+pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""}
+pyinstaller-hooks-contrib = ">=2021.4"
+pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""}
+setuptools = "*"
+
+[package.extras]
+encryption = ["tinyaes (>=1.0.0)"]
+hook_testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"]
+
+[[package]]
+name = "pyinstaller-hooks-contrib"
+version = "2022.10"
+description = "Community maintained hooks for PyInstaller"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "pyparsing"
+version = "3.0.9"
+description = "pyparsing module - Classes and methods to define and execute parsing grammars"
+category = "main"
+optional = false
+python-versions = ">=3.6.8"
+
+[package.extras]
+diagrams = ["jinja2", "railroad-diagrams"]
+
+[[package]]
+name = "PyQt5"
+version = "5.15.7"
+description = "Python bindings for the Qt cross platform application toolkit"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+PyQt5-Qt5 = ">=5.15.0"
+PyQt5-sip = ">=12.11,<13"
+
+[[package]]
+name = "PyQt5-Qt5"
+version = "5.15.2"
+description = "The subset of a Qt installation needed by PyQt5."
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "PyQt5-sip"
+version = "12.11.0"
+description = "The sip module support for PyQt5"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "pytest"
+version = "7.1.3"
+description = "pytest: simple powerful testing with Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+attrs = ">=19.2.0"
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+iniconfig = "*"
+packaging = "*"
+pluggy = ">=0.12,<2.0"
+py = ">=1.8.2"
+tomli = ">=1.0.0"
+
+[package.extras]
+testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
+
+[[package]]
+name = "pytest-cov"
+version = "4.0.0"
+description = "Pytest plugin for measuring coverage."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+coverage = {version = ">=5.2.1", extras = ["toml"]}
+pytest = ">=4.6"
+
+[package.extras]
+testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
+
+[[package]]
+name = "pywin32-ctypes"
+version = "0.2.0"
+description = ""
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "PyYAML"
+version = "6.0"
+description = "YAML parser and emitter for Python"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "regex"
+version = "2022.9.13"
+description = "Alternative regular expression module, to replace re."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "requests"
+version = "2.28.1"
+description = "Python HTTP for Humans."
+category = "main"
+optional = false
+python-versions = ">=3.7, <4"
+
+[package.dependencies]
+certifi = ">=2017.4.17"
+charset-normalizer = ">=2,<3"
+idna = ">=2.5,<4"
+urllib3 = ">=1.21.1,<1.27"
+
+[package.extras]
+socks = ["PySocks (>=1.5.6,!=1.5.7)"]
+use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"]
+
+[[package]]
+name = "setuptools"
+version = "65.4.0"
+description = "Easily download, build, install, upgrade, and uninstall Python packages"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
+testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
+testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
+
+[[package]]
+name = "six"
+version = "1.16.0"
+description = "Python 2 and 3 compatibility utilities"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "sounddevice"
+version = "0.4.5"
+description = "Play and Record Sound with Python"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+CFFI = ">=1.0"
+
+[package.extras]
+numpy = ["NumPy"]
+
+[[package]]
+name = "tokenizers"
+version = "0.12.1"
+description = "Fast and Customizable Tokenizers"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.extras]
+docs = ["setuptools-rust", "sphinx", "sphinx-rtd-theme"]
+testing = ["datasets", "numpy", "pytest", "requests"]
+
+[[package]]
+name = "toml"
+version = "0.10.2"
+description = "Python Library for Tom's Obvious, Minimal Language"
+category = "dev"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "tomli"
+version = "2.0.1"
+description = "A lil' TOML parser"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "torch"
+version = "1.12.1"
+description = "Tensors and Dynamic neural networks in Python with strong GPU acceleration"
+category = "main"
+optional = false
+python-versions = ">=3.7.0"
+
+[package.dependencies]
+typing-extensions = "*"
+
+[[package]]
+name = "tqdm"
+version = "4.64.1"
+description = "Fast, Extensible Progress Meter"
+category = "main"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
+[package.extras]
+dev = ["py-make (>=0.1.0)", "twine", "wheel"]
+notebook = ["ipywidgets (>=6)"]
+slack = ["slack-sdk"]
+telegram = ["requests"]
+
+[[package]]
+name = "transformers"
+version = "4.22.1"
+description = "State-of-the-art Machine Learning for JAX, PyTorch and TensorFlow"
+category = "main"
+optional = false
+python-versions = ">=3.7.0"
+
+[package.dependencies]
+filelock = "*"
+huggingface-hub = ">=0.9.0,<1.0"
+numpy = ">=1.17"
+packaging = ">=20.0"
+pyyaml = ">=5.1"
+regex = "!=2019.12.17"
+requests = "*"
+tokenizers = ">=0.11.1,<0.11.3 || >0.11.3,<0.13"
+tqdm = ">=4.27"
+
+[package.extras]
+accelerate = ["accelerate (>=0.10.0)"]
+all = ["Pillow", "accelerate (>=0.10.0)", "codecarbon (==1.2.0)", "flax (>=0.4.1)", "jax (>=0.2.8,!=0.3.2,<=0.3.6)", "jaxlib (>=0.1.65,<=0.3.6)", "librosa", "onnxconverter-common", "optax (>=0.0.8)", "optuna", "phonemizer", "protobuf (<=3.20.1)", "pyctcdecode (>=0.3.0)", "ray[tune]", "sentencepiece (>=0.1.91,!=0.1.92)", "sigopt", "tensorflow (>=2.3)", "tensorflow-text", "tf2onnx", "timm", "tokenizers (>=0.11.1,!=0.11.3,<0.13)", "torch (>=1.0)", "torchaudio"]
+audio = ["librosa", "phonemizer", "pyctcdecode (>=0.3.0)"]
+codecarbon = ["codecarbon (==1.2.0)"]
+deepspeed = ["accelerate (>=0.10.0)", "deepspeed (>=0.6.5)"]
+deepspeed-testing = ["GitPython (<3.1.19)", "accelerate (>=0.10.0)", "black (==22.3)", "cookiecutter (==1.7.3)", "datasets", "deepspeed (>=0.6.5)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "hf-doc-builder (>=0.3.0)", "nltk", "optuna", "parameterized", "protobuf (<=3.20.1)", "psutil", "pytest", "pytest-timeout", "pytest-xdist", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "timeout-decorator"]
+dev = ["GitPython (<3.1.19)", "Pillow", "accelerate (>=0.10.0)", "black (==22.3)", "codecarbon (==1.2.0)", "cookiecutter (==1.7.3)", "datasets", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "flake8 (>=3.8.3)", "flax (>=0.4.1)", "fugashi (>=1.0)", "hf-doc-builder", "hf-doc-builder (>=0.3.0)", "ipadic (>=1.0.0,<2.0)", "isort (>=5.5.4)", "jax (>=0.2.8,!=0.3.2,<=0.3.6)", "jaxlib (>=0.1.65,<=0.3.6)", "librosa", "nltk", "onnxconverter-common", "optax (>=0.0.8)", "optuna", "parameterized", "phonemizer", "protobuf (<=3.20.1)", "psutil", "pyctcdecode (>=0.3.0)", "pytest", "pytest-timeout", "pytest-xdist", "ray[tune]", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "scikit-learn", "sentencepiece (>=0.1.91,!=0.1.92)", "sigopt", "tensorflow (>=2.3)", "tensorflow-text", "tf2onnx", "timeout-decorator", "timm", "tokenizers (>=0.11.1,!=0.11.3,<0.13)", "torch (>=1.0)", "torchaudio", "unidic (>=1.0.2)", "unidic-lite (>=1.0.7)"]
+dev-tensorflow = ["GitPython (<3.1.19)", "Pillow", "black (==22.3)", "cookiecutter (==1.7.3)", "datasets", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "flake8 (>=3.8.3)", "hf-doc-builder", "hf-doc-builder (>=0.3.0)", "isort (>=5.5.4)", "librosa", "nltk", "onnxconverter-common", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)", "parameterized", "phonemizer", "protobuf (<=3.20.1)", "psutil", "pyctcdecode (>=0.3.0)", "pytest", "pytest-timeout", "pytest-xdist", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "scikit-learn", "sentencepiece (>=0.1.91,!=0.1.92)", "tensorflow (>=2.3)", "tensorflow-text", "tf2onnx", "timeout-decorator", "tokenizers (>=0.11.1,!=0.11.3,<0.13)"]
+dev-torch = ["GitPython (<3.1.19)", "Pillow", "black (==22.3)", "codecarbon (==1.2.0)", "cookiecutter (==1.7.3)", "datasets", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "flake8 (>=3.8.3)", "fugashi (>=1.0)", "hf-doc-builder", "hf-doc-builder (>=0.3.0)", "ipadic (>=1.0.0,<2.0)", "isort (>=5.5.4)", "librosa", "nltk", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)", "optuna", "parameterized", "phonemizer", "protobuf (<=3.20.1)", "psutil", "pyctcdecode (>=0.3.0)", "pytest", "pytest-timeout", "pytest-xdist", "ray[tune]", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "scikit-learn", "sentencepiece (>=0.1.91,!=0.1.92)", "sigopt", "timeout-decorator", "timm", "tokenizers (>=0.11.1,!=0.11.3,<0.13)", "torch (>=1.0)", "torchaudio", "unidic (>=1.0.2)", "unidic-lite (>=1.0.7)"]
+docs = ["Pillow", "accelerate (>=0.10.0)", "codecarbon (==1.2.0)", "flax (>=0.4.1)", "hf-doc-builder", "jax (>=0.2.8,!=0.3.2,<=0.3.6)", "jaxlib (>=0.1.65,<=0.3.6)", "librosa", "onnxconverter-common", "optax (>=0.0.8)", "optuna", "phonemizer", "protobuf (<=3.20.1)", "pyctcdecode (>=0.3.0)", "ray[tune]", "sentencepiece (>=0.1.91,!=0.1.92)", "sigopt", "tensorflow (>=2.3)", "tensorflow-text", "tf2onnx", "timm", "tokenizers (>=0.11.1,!=0.11.3,<0.13)", "torch (>=1.0)", "torchaudio"]
+docs_specific = ["hf-doc-builder"]
+fairscale = ["fairscale (>0.3)"]
+flax = ["flax (>=0.4.1)", "jax (>=0.2.8,!=0.3.2,<=0.3.6)", "jaxlib (>=0.1.65,<=0.3.6)", "optax (>=0.0.8)"]
+flax-speech = ["librosa", "phonemizer", "pyctcdecode (>=0.3.0)"]
+ftfy = ["ftfy"]
+integrations = ["optuna", "ray[tune]", "sigopt"]
+ja = ["fugashi (>=1.0)", "ipadic (>=1.0.0,<2.0)", "unidic (>=1.0.2)", "unidic-lite (>=1.0.7)"]
+modelcreation = ["cookiecutter (==1.7.3)"]
+onnx = ["onnxconverter-common", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)", "tf2onnx"]
+onnxruntime = ["onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)"]
+optuna = ["optuna"]
+quality = ["GitPython (<3.1.19)", "black (==22.3)", "flake8 (>=3.8.3)", "hf-doc-builder (>=0.3.0)", "isort (>=5.5.4)"]
+ray = ["ray[tune]"]
+retrieval = ["datasets", "faiss-cpu"]
+sagemaker = ["sagemaker (>=2.31.0)"]
+sentencepiece = ["protobuf (<=3.20.1)", "sentencepiece (>=0.1.91,!=0.1.92)"]
+serving = ["fastapi", "pydantic", "starlette", "uvicorn"]
+sigopt = ["sigopt"]
+sklearn = ["scikit-learn"]
+speech = ["librosa", "phonemizer", "pyctcdecode (>=0.3.0)", "torchaudio"]
+testing = ["GitPython (<3.1.19)", "black (==22.3)", "cookiecutter (==1.7.3)", "datasets", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "hf-doc-builder (>=0.3.0)", "nltk", "parameterized", "protobuf (<=3.20.1)", "psutil", "pytest", "pytest-timeout", "pytest-xdist", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "timeout-decorator"]
+tf = ["onnxconverter-common", "tensorflow (>=2.3)", "tensorflow-text", "tf2onnx"]
+tf-cpu = ["onnxconverter-common", "tensorflow-cpu (>=2.3)", "tensorflow-text", "tf2onnx"]
+tf-speech = ["librosa", "phonemizer", "pyctcdecode (>=0.3.0)"]
+timm = ["timm"]
+tokenizers = ["tokenizers (>=0.11.1,!=0.11.3,<0.13)"]
+torch = ["torch (>=1.0)"]
+torch-speech = ["librosa", "phonemizer", "pyctcdecode (>=0.3.0)", "torchaudio"]
+torchhub = ["filelock", "huggingface-hub (>=0.9.0,<1.0)", "importlib-metadata", "numpy (>=1.17)", "packaging (>=20.0)", "protobuf (<=3.20.1)", "regex (!=2019.12.17)", "requests", "sentencepiece (>=0.1.91,!=0.1.92)", "tokenizers (>=0.11.1,!=0.11.3,<0.13)", "torch (>=1.0)", "tqdm (>=4.27)"]
+vision = ["Pillow"]
+
+[[package]]
+name = "typing-extensions"
+version = "4.3.0"
+description = "Backported and Experimental Type Hints for Python 3.7+"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "urllib3"
+version = "1.26.12"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4"
+
+[package.extras]
+brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"]
+secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"]
+socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
+
+[[package]]
+name = "whisper"
+version = "1.0"
+description = ""
+category = "main"
+optional = false
+python-versions = "*"
+develop = false
+
+[package.dependencies]
+ffmpeg-python = "0.2.0"
+more-itertools = "*"
+numpy = "*"
+torch = "*"
+tqdm = "*"
+transformers = ">=4.19.0"
+
+[package.extras]
+dev = ["pytest"]
+
+[package.source]
+type = "git"
+url = "https://github.com/openai/whisper.git"
+reference = "HEAD"
+resolved_reference = "0b1ba3d46ebf7fe6f953acfd8cad62a4f851b49f"
+
+[metadata]
+lock-version = "1.1"
+python-versions = "~3.10.1"
+content-hash = "d62dae30187aa00d143e75390b3760a34e6be7cdb4060dabc7896e5edf414d66"
+
+[metadata.files]
+altgraph = [
+ {file = "altgraph-0.17.3-py2.py3-none-any.whl", hash = "sha256:c8ac1ca6772207179ed8003ce7687757c04b0b71536f81e2ac5755c6226458fe"},
+ {file = "altgraph-0.17.3.tar.gz", hash = "sha256:ad33358114df7c9416cdb8fa1eaa5852166c505118717021c6a8c7c7abbd03dd"},
+]
+attrs = [
+ {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
+ {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"},
+]
+autopep8 = [
+ {file = "autopep8-1.7.0-py2.py3-none-any.whl", hash = "sha256:6f09e90a2be784317e84dc1add17ebfc7abe3924239957a37e5040e27d812087"},
+ {file = "autopep8-1.7.0.tar.gz", hash = "sha256:ca9b1a83e53a7fad65d731dc7a2a2d50aa48f43850407c59f6a1a306c4201142"},
+]
+certifi = [
+ {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"},
+ {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"},
+]
+cffi = [
+ {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"},
+ {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"},
+ {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"},
+ {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"},
+ {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"},
+ {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"},
+ {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"},
+ {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"},
+ {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"},
+ {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"},
+ {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"},
+ {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"},
+ {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"},
+ {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"},
+ {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"},
+ {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"},
+ {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"},
+ {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"},
+ {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"},
+ {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"},
+ {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"},
+ {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"},
+ {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"},
+ {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"},
+ {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"},
+ {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"},
+ {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"},
+ {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"},
+ {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"},
+ {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"},
+ {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"},
+ {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"},
+ {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"},
+ {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"},
+ {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"},
+ {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"},
+ {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"},
+ {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"},
+ {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"},
+ {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"},
+ {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"},
+ {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"},
+ {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"},
+ {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"},
+ {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"},
+ {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"},
+ {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"},
+ {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"},
+ {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"},
+ {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"},
+ {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"},
+ {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"},
+ {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"},
+ {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"},
+ {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"},
+ {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"},
+ {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"},
+ {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"},
+ {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"},
+ {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"},
+ {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"},
+ {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"},
+ {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"},
+ {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"},
+]
+charset-normalizer = [
+ {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"},
+ {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"},
+]
+colorama = [
+ {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
+ {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
+]
+coverage = [
+ {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"},
+ {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"},
+ {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"},
+ {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"},
+ {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"},
+ {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"},
+ {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"},
+ {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"},
+ {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"},
+ {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"},
+ {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"},
+ {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"},
+ {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"},
+ {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"},
+ {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"},
+ {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"},
+ {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"},
+ {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"},
+ {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"},
+ {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"},
+ {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"},
+ {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"},
+ {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"},
+ {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"},
+ {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"},
+ {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"},
+ {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"},
+ {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"},
+ {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"},
+ {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"},
+ {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"},
+ {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"},
+ {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"},
+ {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"},
+ {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"},
+ {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"},
+ {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"},
+ {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"},
+ {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"},
+ {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"},
+ {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"},
+ {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"},
+ {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"},
+ {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"},
+ {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"},
+ {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"},
+ {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"},
+ {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"},
+ {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"},
+ {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"},
+]
+ffmpeg = [
+ {file = "ffmpeg-1.4.tar.gz", hash = "sha256:6931692c890ff21d39938433c2189747815dca0c60ddc7f9bb97f199dba0b5b9"},
+]
+ffmpeg-python = [
+ {file = "ffmpeg-python-0.2.0.tar.gz", hash = "sha256:65225db34627c578ef0e11c8b1eb528bb35e024752f6f10b78c011f6f64c4127"},
+ {file = "ffmpeg_python-0.2.0-py3-none-any.whl", hash = "sha256:ac441a0404e053f8b6a1113a77c0f452f1cfc62f6344a769475ffdc0f56c23c5"},
+]
+filelock = [
+ {file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"},
+ {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"},
+]
+future = [
+ {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"},
+]
+huggingface-hub = [
+ {file = "huggingface_hub-0.9.1-py3-none-any.whl", hash = "sha256:7a588046bdeb84e7bc99b3da58bbb4312a56d94ba51ebc60dfe610c18b3d0b9f"},
+ {file = "huggingface_hub-0.9.1.tar.gz", hash = "sha256:6395f26aaf44bbb4a73d3e14aca228fa39534696f651c6c82a6347f8c9f5950b"},
+]
+idna = [
+ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
+ {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
+]
+iniconfig = [
+ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
+ {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
+]
+macholib = [
+ {file = "macholib-1.16.2-py2.py3-none-any.whl", hash = "sha256:44c40f2cd7d6726af8fa6fe22549178d3a4dfecc35a9cd15ea916d9c83a688e0"},
+ {file = "macholib-1.16.2.tar.gz", hash = "sha256:557bbfa1bb255c20e9abafe7ed6cd8046b48d9525db2f9b77d3122a63a2a8bf8"},
+]
+more-itertools = [
+ {file = "more-itertools-8.14.0.tar.gz", hash = "sha256:c09443cd3d5438b8dafccd867a6bc1cb0894389e90cb53d227456b0b0bccb750"},
+ {file = "more_itertools-8.14.0-py3-none-any.whl", hash = "sha256:1bc4f91ee5b1b31ac7ceacc17c09befe6a40a503907baf9c839c229b5095cfd2"},
+]
+numpy = [
+ {file = "numpy-1.23.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c9f707b5bb73bf277d812ded9896f9512a43edff72712f31667d0a8c2f8e71ee"},
+ {file = "numpy-1.23.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ffcf105ecdd9396e05a8e58e81faaaf34d3f9875f137c7372450baa5d77c9a54"},
+ {file = "numpy-1.23.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ea3f98a0ffce3f8f57675eb9119f3f4edb81888b6874bc1953f91e0b1d4f440"},
+ {file = "numpy-1.23.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:004f0efcb2fe1c0bd6ae1fcfc69cc8b6bf2407e0f18be308612007a0762b4089"},
+ {file = "numpy-1.23.3-cp310-cp310-win32.whl", hash = "sha256:98dcbc02e39b1658dc4b4508442a560fe3ca5ca0d989f0df062534e5ca3a5c1a"},
+ {file = "numpy-1.23.3-cp310-cp310-win_amd64.whl", hash = "sha256:39a664e3d26ea854211867d20ebcc8023257c1800ae89773cbba9f9e97bae036"},
+ {file = "numpy-1.23.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1f27b5322ac4067e67c8f9378b41c746d8feac8bdd0e0ffede5324667b8a075c"},
+ {file = "numpy-1.23.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ad3ec9a748a8943e6eb4358201f7e1c12ede35f510b1a2221b70af4bb64295c"},
+ {file = "numpy-1.23.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdc9febce3e68b697d931941b263c59e0c74e8f18861f4064c1f712562903411"},
+ {file = "numpy-1.23.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:301c00cf5e60e08e04d842fc47df641d4a181e651c7135c50dc2762ffe293dbd"},
+ {file = "numpy-1.23.3-cp311-cp311-win32.whl", hash = "sha256:7cd1328e5bdf0dee621912f5833648e2daca72e3839ec1d6695e91089625f0b4"},
+ {file = "numpy-1.23.3-cp311-cp311-win_amd64.whl", hash = "sha256:8355fc10fd33a5a70981a5b8a0de51d10af3688d7a9e4a34fcc8fa0d7467bb7f"},
+ {file = "numpy-1.23.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bc6e8da415f359b578b00bcfb1d08411c96e9a97f9e6c7adada554a0812a6cc6"},
+ {file = "numpy-1.23.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:22d43376ee0acd547f3149b9ec12eec2f0ca4a6ab2f61753c5b29bb3e795ac4d"},
+ {file = "numpy-1.23.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a64403f634e5ffdcd85e0b12c08f04b3080d3e840aef118721021f9b48fc1460"},
+ {file = "numpy-1.23.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efd9d3abe5774404becdb0748178b48a218f1d8c44e0375475732211ea47c67e"},
+ {file = "numpy-1.23.3-cp38-cp38-win32.whl", hash = "sha256:f8c02ec3c4c4fcb718fdf89a6c6f709b14949408e8cf2a2be5bfa9c49548fd85"},
+ {file = "numpy-1.23.3-cp38-cp38-win_amd64.whl", hash = "sha256:e868b0389c5ccfc092031a861d4e158ea164d8b7fdbb10e3b5689b4fc6498df6"},
+ {file = "numpy-1.23.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:09f6b7bdffe57fc61d869a22f506049825d707b288039d30f26a0d0d8ea05164"},
+ {file = "numpy-1.23.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8c79d7cf86d049d0c5089231a5bcd31edb03555bd93d81a16870aa98c6cfb79d"},
+ {file = "numpy-1.23.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5d5420053bbb3dd64c30e58f9363d7a9c27444c3648e61460c1237f9ec3fa14"},
+ {file = "numpy-1.23.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5422d6a1ea9b15577a9432e26608c73a78faf0b9039437b075cf322c92e98e7"},
+ {file = "numpy-1.23.3-cp39-cp39-win32.whl", hash = "sha256:c1ba66c48b19cc9c2975c0d354f24058888cdc674bebadceb3cdc9ec403fb5d1"},
+ {file = "numpy-1.23.3-cp39-cp39-win_amd64.whl", hash = "sha256:78a63d2df1d947bd9d1b11d35564c2f9e4b57898aae4626638056ec1a231c40c"},
+ {file = "numpy-1.23.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:17c0e467ade9bda685d5ac7f5fa729d8d3e76b23195471adae2d6a6941bd2c18"},
+ {file = "numpy-1.23.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91b8d6768a75247026e951dce3b2aac79dc7e78622fc148329135ba189813584"},
+ {file = "numpy-1.23.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:94c15ca4e52671a59219146ff584488907b1f9b3fc232622b47e2cf832e94fb8"},
+ {file = "numpy-1.23.3.tar.gz", hash = "sha256:51bf49c0cd1d52be0a240aa66f3458afc4b95d8993d2d04f0d91fa60c10af6cd"},
+]
+packaging = [
+ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
+ {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
+]
+pefile = [
+ {file = "pefile-2022.5.30.tar.gz", hash = "sha256:a5488a3dd1fd021ce33f969780b88fe0f7eebb76eb20996d7318f307612a045b"},
+]
+pluggy = [
+ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
+ {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
+]
+py = [
+ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
+ {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
+]
+pycodestyle = [
+ {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"},
+ {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"},
+]
+pycparser = [
+ {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
+ {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
+]
+pyinstaller = [
+ {file = "pyinstaller-5.4.1-py3-none-macosx_10_13_universal2.whl", hash = "sha256:0ac78f1145be34adda8afb5fe4c8d659172140092c055994dab57ee2190bec71"},
+ {file = "pyinstaller-5.4.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:806df33e2b93a69f45e4c8be72d3b51449e9e0f557a3e89e0b06f5cb9a111022"},
+ {file = "pyinstaller-5.4.1-py3-none-manylinux2014_i686.whl", hash = "sha256:9a2b99f191e28dec6cb8eb5544fa436f2109073e634f8e602225ada0239aed46"},
+ {file = "pyinstaller-5.4.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:4b4d867fbc6c35bb30b09f681d469f824c69fe32f2358b8af75247bdefdf694a"},
+ {file = "pyinstaller-5.4.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:994b76d10892f1d3f9a4241109a3f7b1e9b74dea3198bf3f502d2f4ae744fa6e"},
+ {file = "pyinstaller-5.4.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:b57e8f9ec23a48b740c5dbb19cd5499150e35a2997dfd0b8f96e2deced512ecc"},
+ {file = "pyinstaller-5.4.1-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:bec1d94cba89f64a61606a1eaf33202f3a52ff2ac65e25555053fd5fe55b760d"},
+ {file = "pyinstaller-5.4.1-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:f7f0ad1bdea828c163e2032c7ce941b2d0b68a83b3b572e6ce50c77ca7987574"},
+ {file = "pyinstaller-5.4.1-py3-none-win32.whl", hash = "sha256:7da11fdee216ae0b887f9d1fe0eca7bce2b8f5d039eeac2561282feff5970d24"},
+ {file = "pyinstaller-5.4.1-py3-none-win_amd64.whl", hash = "sha256:0c8626c2db6e2d426be3e44ae08039d638481de7b580769668dd777116d911b9"},
+ {file = "pyinstaller-5.4.1.tar.gz", hash = "sha256:2a09e6bd6e121eb1a71fadb223797dc502e4fd4168931c31a5f87faa10eb5b4c"},
+]
+pyinstaller-hooks-contrib = [
+ {file = "pyinstaller-hooks-contrib-2022.10.tar.gz", hash = "sha256:e5edd4094175e78c178ef987b61be19efff6caa23d266ade456fc753e847f62e"},
+ {file = "pyinstaller_hooks_contrib-2022.10-py2.py3-none-any.whl", hash = "sha256:d1dd6ea059dc30e77813cc12a5efa8b1d228e7da8f5b884fe11775f946db1784"},
+]
+pyparsing = [
+ {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
+ {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
+]
+PyQt5 = [
+ {file = "PyQt5-5.15.7-cp37-abi3-macosx_10_13_x86_64.whl", hash = "sha256:1a793748c60d5aff3850b7abf84d47c1d41edb11231b7d7c16bef602c36be643"},
+ {file = "PyQt5-5.15.7-cp37-abi3-manylinux1_x86_64.whl", hash = "sha256:e319c9d8639e0729235c1b09c99afdadad96fa3dbd8392ab561b5ab5946ee6ef"},
+ {file = "PyQt5-5.15.7-cp37-abi3-win32.whl", hash = "sha256:08694f0a4c7d4f3d36b2311b1920e6283240ad3b7c09b515e08262e195dcdf37"},
+ {file = "PyQt5-5.15.7-cp37-abi3-win_amd64.whl", hash = "sha256:232fe5b135a095cbd024cf341d928fc672c963f88e6a52b0c605be8177c2fdb5"},
+ {file = "PyQt5-5.15.7.tar.gz", hash = "sha256:755121a52b3a08cb07275c10ebb96576d36e320e572591db16cfdbc558101594"},
+]
+PyQt5-Qt5 = [
+ {file = "PyQt5_Qt5-5.15.2-py3-none-macosx_10_13_intel.whl", hash = "sha256:76980cd3d7ae87e3c7a33bfebfaee84448fd650bad6840471d6cae199b56e154"},
+ {file = "PyQt5_Qt5-5.15.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:1988f364ec8caf87a6ee5d5a3a5210d57539988bf8e84714c7d60972692e2f4a"},
+ {file = "PyQt5_Qt5-5.15.2-py3-none-win32.whl", hash = "sha256:9cc7a768b1921f4b982ebc00a318ccb38578e44e45316c7a4a850e953e1dd327"},
+ {file = "PyQt5_Qt5-5.15.2-py3-none-win_amd64.whl", hash = "sha256:750b78e4dba6bdf1607febedc08738e318ea09e9b10aea9ff0d73073f11f6962"},
+]
+PyQt5-sip = [
+ {file = "PyQt5_sip-12.11.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f1f9e312ff8284d6dfebc5366f6f7d103f84eec23a4da0be0482403933e68660"},
+ {file = "PyQt5_sip-12.11.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:4031547dfb679be309094bfa79254f5badc5ddbe66b9ad38e319d84a7d612443"},
+ {file = "PyQt5_sip-12.11.0-cp310-cp310-win32.whl", hash = "sha256:ad21ca0ee8cae2a41b61fc04949dccfab6fe008749627d94e8c7078cb7a73af1"},
+ {file = "PyQt5_sip-12.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:3126c84568ab341c12e46ded2230f62a9a78752a70fdab13713f89a71cd44f73"},
+ {file = "PyQt5_sip-12.11.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0f77655c62ec91d47c2c99143f248624d44dd2d8a12d016e7c020508ad418aca"},
+ {file = "PyQt5_sip-12.11.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ec5e9ef78852e1f96f86d7e15c9215878422b83dde36d44f1539a3062942f19c"},
+ {file = "PyQt5_sip-12.11.0-cp37-cp37m-win32.whl", hash = "sha256:d12b81c3a08abf7657a2ebc7d3649852a1f327eb2146ebadf45930486d32e920"},
+ {file = "PyQt5_sip-12.11.0-cp37-cp37m-win_amd64.whl", hash = "sha256:b69a1911f768b489846335e31e49eb34795c6b5a038ca24d894d751e3b0b44da"},
+ {file = "PyQt5_sip-12.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:51e377789d59196213eddf458e6927f33ba9d217b614d17d20df16c9a8b2c41c"},
+ {file = "PyQt5_sip-12.11.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:4e5c1559311515291ea0ab0635529f14536954e3b973a7c7890ab7e4de1c2c23"},
+ {file = "PyQt5_sip-12.11.0-cp38-cp38-win32.whl", hash = "sha256:9bca450c5306890cb002fe36bbca18f979dd9e5b810b766dce8e3ce5e66ba795"},
+ {file = "PyQt5_sip-12.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:f6b72035da4e8fecbb0bc4a972e30a5674a9ad5608dbddaa517e983782dbf3bf"},
+ {file = "PyQt5_sip-12.11.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9356260d4feb60dbac0ab66f8a791a0d2cda1bf98c9dec8e575904a045fbf7c5"},
+ {file = "PyQt5_sip-12.11.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:205f3e1b3eea3597d8e878936c1a06e04bd23a59e8b179ee806465d72eea3071"},
+ {file = "PyQt5_sip-12.11.0-cp39-cp39-win32.whl", hash = "sha256:686071be054e5be6ca5aaaef7960931d4ba917277e839e2e978c7cbe3f43bb6e"},
+ {file = "PyQt5_sip-12.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:42320e7a94b1085ed85d49794ed4ccfe86f1cae80b44a894db908a8aba2bc60e"},
+ {file = "PyQt5_sip-12.11.0.tar.gz", hash = "sha256:b4710fd85b57edef716cc55fae45bfd5bfac6fc7ba91036f1dcc3f331ca0eb39"},
+]
+pytest = [
+ {file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"},
+ {file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"},
+]
+pytest-cov = [
+ {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"},
+ {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"},
+]
+pywin32-ctypes = [
+ {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"},
+ {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"},
+]
+PyYAML = [
+ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
+ {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
+ {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"},
+ {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"},
+ {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"},
+ {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
+ {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"},
+ {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"},
+ {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"},
+ {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"},
+ {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"},
+ {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"},
+ {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"},
+ {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"},
+ {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"},
+ {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"},
+ {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
+ {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"},
+ {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"},
+ {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"},
+ {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"},
+ {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"},
+ {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"},
+ {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"},
+ {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"},
+ {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"},
+ {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"},
+ {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"},
+ {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"},
+ {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"},
+ {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"},
+ {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"},
+ {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"},
+ {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"},
+ {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"},
+ {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"},
+ {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"},
+ {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"},
+ {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
+ {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
+]
+regex = [
+ {file = "regex-2022.9.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0394265391a86e2bbaa7606e59ac71bd9f1edf8665a59e42771a9c9adbf6fd4f"},
+ {file = "regex-2022.9.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86df2049b18745f3cd4b0f4c4ef672bfac4b80ca488e6ecfd2bbfe68d2423a2c"},
+ {file = "regex-2022.9.13-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce331b076b2b013e7d7f07157f957974ef0b0881a808e8a4a4b3b5105aee5d04"},
+ {file = "regex-2022.9.13-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:360ffbc9357794ae41336b681dff1c0463193199dfb91fcad3ec385ea4972f46"},
+ {file = "regex-2022.9.13-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18e503b1e515a10282b3f14f1b3d856194ecece4250e850fad230842ed31227f"},
+ {file = "regex-2022.9.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6e167d1ccd41d27b7b6655bb7a2dcb1b1eb1e0d2d662043470bd3b4315d8b2b"},
+ {file = "regex-2022.9.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4146cb7ae6029fc83b5c905ec6d806b7e5568dc14297c423e66b86294bad6c39"},
+ {file = "regex-2022.9.13-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a1aec4ae549fd7b3f52ceaf67e133010e2fba1538bf4d5fc5cd162a5e058d5df"},
+ {file = "regex-2022.9.13-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cab548d6d972e1de584161487b2ac1aa82edd8430d1bde69587ba61698ad1cfb"},
+ {file = "regex-2022.9.13-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3d64e1a7e6d98a4cdc8b29cb8d8ed38f73f49e55fbaa737bdb5933db99b9de22"},
+ {file = "regex-2022.9.13-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:67a4c625361db04ae40ef7c49d3cbe2c1f5ff10b5a4491327ab20f19f2fb5d40"},
+ {file = "regex-2022.9.13-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:5d0dd8b06896423211ce18fba0c75dacc49182a1d6514c004b535be7163dca0f"},
+ {file = "regex-2022.9.13-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4318f69b79f9f7d84a7420e97d4bfe872dc767c72f891d4fea5fa721c74685f7"},
+ {file = "regex-2022.9.13-cp310-cp310-win32.whl", hash = "sha256:26df88c9636a0c3f3bd9189dd435850a0c49d0b7d6e932500db3f99a6dd604d1"},
+ {file = "regex-2022.9.13-cp310-cp310-win_amd64.whl", hash = "sha256:6fe1dd1021e0f8f3f454ce2811f1b0b148f2d25bb38c712fec00316551e93650"},
+ {file = "regex-2022.9.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:83cc32a1a2fa5bac00f4abc0e6ce142e3c05d3a6d57e23bd0f187c59b4e1e43b"},
+ {file = "regex-2022.9.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2effeaf50a6838f3dd4d3c5d265f06eabc748f476e8441892645ae3a697e273"},
+ {file = "regex-2022.9.13-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59a786a55d00439d8fae4caaf71581f2aaef7297d04ee60345c3594efef5648a"},
+ {file = "regex-2022.9.13-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b701dbc124558fd2b1b08005eeca6c9160e209108fbcbd00091fcfac641ac7"},
+ {file = "regex-2022.9.13-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dab81cc4d58026861445230cfba27f9825e9223557926e7ec22156a1a140d55c"},
+ {file = "regex-2022.9.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b0c5cc3d1744a67c3b433dce91e5ef7c527d612354c1f1e8576d9e86bc5c5e2"},
+ {file = "regex-2022.9.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:518272f25da93e02af4f1e94985f5042cec21557ef3591027d0716f2adda5d0a"},
+ {file = "regex-2022.9.13-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8418ee2cb857b83881b8f981e4c636bc50a0587b12d98cb9b947408a3c484fe7"},
+ {file = "regex-2022.9.13-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cfa4c956ff0a977c4823cb3b930b0a4e82543b060733628fec7ab3eb9b1abe37"},
+ {file = "regex-2022.9.13-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:a1c4d17879dd4c4432c08a1ca1ab379f12ab54af569e945b6fc1c4cf6a74ca45"},
+ {file = "regex-2022.9.13-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:77c2879d3ba51e5ca6c2b47f2dcf3d04a976a623a8fc8236010a16c9e0b0a3c7"},
+ {file = "regex-2022.9.13-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2885ec6eea629c648ecc9bde0837ec6b92208b7f36381689937fe5d64a517e8"},
+ {file = "regex-2022.9.13-cp311-cp311-win32.whl", hash = "sha256:2dda4b096a6f630d6531728a45bd12c67ec3badf44342046dc77d4897277d4f2"},
+ {file = "regex-2022.9.13-cp311-cp311-win_amd64.whl", hash = "sha256:592b9e2e1862168e71d9e612bfdc22c451261967dbd46681f14e76dfba7105fd"},
+ {file = "regex-2022.9.13-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:df8fe00b60e4717662c7f80c810ba66dcc77309183c76b7754c0dff6f1d42054"},
+ {file = "regex-2022.9.13-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:995e70bb8c91d1b99ed2aaf8ec44863e06ad1dfbb45d7df95f76ef583ec323a9"},
+ {file = "regex-2022.9.13-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad75173349ad79f9d21e0d0896b27dcb37bfd233b09047bc0b4d226699cf5c87"},
+ {file = "regex-2022.9.13-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7681c49da1a2d4b905b4f53d86c9ba4506e79fba50c4a664d9516056e0f7dfcc"},
+ {file = "regex-2022.9.13-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bc8edc5f8ef0ebb46f3fa0d02bd825bbe9cc63d59e428ffb6981ff9672f6de1"},
+ {file = "regex-2022.9.13-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bee775ff05c9d519195bd9e8aaaccfe3971db60f89f89751ee0f234e8aeac5"},
+ {file = "regex-2022.9.13-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1a901ce5cd42658ab8f8eade51b71a6d26ad4b68c7cfc86b87efc577dfa95602"},
+ {file = "regex-2022.9.13-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:14a7ab070fa3aec288076eed6ed828587b805ef83d37c9bfccc1a4a7cfbd8111"},
+ {file = "regex-2022.9.13-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:d23ac6b4bf9e32fcde5fcdb2e1fd5e7370d6693fcac51ee1d340f0e886f50d1f"},
+ {file = "regex-2022.9.13-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:4cdbfa6d2befeaee0c899f19222e9b20fc5abbafe5e9c43a46ef819aeb7b75e5"},
+ {file = "regex-2022.9.13-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ab07934725e6f25c6f87465976cc69aef1141e86987af49d8c839c3ffd367c72"},
+ {file = "regex-2022.9.13-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d2a1371dc73e921f3c2e087c05359050f3525a9a34b476ebc8130e71bec55e97"},
+ {file = "regex-2022.9.13-cp36-cp36m-win32.whl", hash = "sha256:fcbd1edff1473d90dc5cf4b52d355cf1f47b74eb7c85ba6e45f45d0116b8edbd"},
+ {file = "regex-2022.9.13-cp36-cp36m-win_amd64.whl", hash = "sha256:fe428822b7a8c486bcd90b334e9ab541ce6cc0d6106993d59f201853e5e14121"},
+ {file = "regex-2022.9.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d7430f041755801b712ec804aaf3b094b9b5facbaa93a6339812a8e00d7bd53a"},
+ {file = "regex-2022.9.13-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:079c182f99c89524069b9cd96f5410d6af437e9dca576a7d59599a574972707e"},
+ {file = "regex-2022.9.13-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59bac44b5a07b08a261537f652c26993af9b1bbe2a29624473968dd42fc29d56"},
+ {file = "regex-2022.9.13-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a59d0377e58d96a6f11636e97992f5b51b7e1e89eb66332d1c01b35adbabfe8a"},
+ {file = "regex-2022.9.13-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9d68eb704b24bc4d441b24e4a12653acd07d2c39940548761e0985a08bc1fff"},
+ {file = "regex-2022.9.13-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0385d66e73cdd4462f3cc42c76a6576ddcc12472c30e02a2ae82061bff132c32"},
+ {file = "regex-2022.9.13-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:db45016364eec9ddbb5af93c8740c5c92eb7f5fc8848d1ae04205a40a1a2efc6"},
+ {file = "regex-2022.9.13-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:03ff695518482b946a6d3d4ce9cbbd99a21320e20d94913080aa3841f880abcd"},
+ {file = "regex-2022.9.13-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6b32b45433df1fad7fed738fe15200b6516da888e0bd1fdd6aa5e50cc16b76bc"},
+ {file = "regex-2022.9.13-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:003a2e1449d425afc817b5f0b3d4c4aa9072dd5f3dfbf6c7631b8dc7b13233de"},
+ {file = "regex-2022.9.13-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:a9eb9558e1d0f78e07082d8a70d5c4d631c8dd75575fae92105df9e19c736730"},
+ {file = "regex-2022.9.13-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:f6e0321921d2fdc082ef90c1fd0870f129c2e691bfdc4937dcb5cd308aba95c4"},
+ {file = "regex-2022.9.13-cp37-cp37m-win32.whl", hash = "sha256:3f3b4594d564ed0b2f54463a9f328cf6a5b2a32610a90cdff778d6e3e561d08b"},
+ {file = "regex-2022.9.13-cp37-cp37m-win_amd64.whl", hash = "sha256:8aba0d01e3dfd335f2cb107079b07fdddb4cd7fb2d8c8a1986f9cb8ce9246c24"},
+ {file = "regex-2022.9.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:944567bb08f52268d8600ee5bdf1798b2b62ea002cc692a39cec113244cbdd0d"},
+ {file = "regex-2022.9.13-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0b664a4d33ffc6be10996606dfc25fd3248c24cc589c0b139feb4c158053565e"},
+ {file = "regex-2022.9.13-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f06cc1190f3db3192ab8949e28f2c627e1809487e2cfc435b6524c1ce6a2f391"},
+ {file = "regex-2022.9.13-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c57d50d4d5eb0c862569ca3c840eba2a73412f31d9ecc46ef0d6b2e621a592b"},
+ {file = "regex-2022.9.13-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19a4da6f513045f5ba00e491215bd00122e5bd131847586522463e5a6b2bd65f"},
+ {file = "regex-2022.9.13-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a926339356fe29595f8e37af71db37cd87ff764e15da8ad5129bbaff35bcc5a6"},
+ {file = "regex-2022.9.13-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:091efcfdd4178a7e19a23776dc2b1fafb4f57f4d94daf340f98335817056f874"},
+ {file = "regex-2022.9.13-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:880dbeb6bdde7d926b4d8e41410b16ffcd4cb3b4c6d926280fea46e2615c7a01"},
+ {file = "regex-2022.9.13-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:73b985c9fc09a7896846e26d7b6f4d1fd5a20437055f4ef985d44729f9f928d0"},
+ {file = "regex-2022.9.13-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c0b7cb9598795b01f9a3dd3f770ab540889259def28a3bf9b2fa24d52edecba3"},
+ {file = "regex-2022.9.13-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:37e5a26e76c46f54b3baf56a6fdd56df9db89758694516413757b7d127d4c57b"},
+ {file = "regex-2022.9.13-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:99945ddb4f379bb9831c05e9f80f02f079ba361a0fb1fba1fc3b267639b6bb2e"},
+ {file = "regex-2022.9.13-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dcbcc9e72a791f622a32d17ff5011326a18996647509cac0609a7fc43adc229"},
+ {file = "regex-2022.9.13-cp38-cp38-win32.whl", hash = "sha256:d3102ab9bf16bf541ca228012d45d88d2a567c9682a805ae2c145a79d3141fdd"},
+ {file = "regex-2022.9.13-cp38-cp38-win_amd64.whl", hash = "sha256:14216ea15efc13f28d0ef1c463d86d93ca7158a79cd4aec0f9273f6d4c6bb047"},
+ {file = "regex-2022.9.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9a165a05979e212b2c2d56a9f40b69c811c98a788964e669eb322de0a3e420b4"},
+ {file = "regex-2022.9.13-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:14c71437ffb89479c89cc7022a5ea2075a842b728f37205e47c824cc17b30a42"},
+ {file = "regex-2022.9.13-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee7045623a5ace70f3765e452528b4c1f2ce669ed31959c63f54de64fe2f6ff7"},
+ {file = "regex-2022.9.13-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e521d9db006c5e4a0f8acfef738399f72b704913d4e083516774eb51645ad7c"},
+ {file = "regex-2022.9.13-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b86548b8234b2be3985dbc0b385e35f5038f0f3e6251464b827b83ebf4ed90e5"},
+ {file = "regex-2022.9.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2b39ee3b280e15824298b97cec3f7cbbe6539d8282cc8a6047a455b9a72c598"},
+ {file = "regex-2022.9.13-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6e6e61e9a38b6cc60ca3e19caabc90261f070f23352e66307b3d21a24a34aaf"},
+ {file = "regex-2022.9.13-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d837ccf3bd2474feabee96cd71144e991472e400ed26582edc8ca88ce259899c"},
+ {file = "regex-2022.9.13-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6adfe300848d61a470ec7547adc97b0ccf86de86a99e6830f1d8c8d19ecaf6b3"},
+ {file = "regex-2022.9.13-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d5b003d248e6f292475cd24b04e5f72c48412231961a675edcb653c70730e79e"},
+ {file = "regex-2022.9.13-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:d5edd3eb877c9fc2e385173d4a4e1d792bf692d79e25c1ca391802d36ecfaa01"},
+ {file = "regex-2022.9.13-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:50e764ffbd08b06aa8c4e86b8b568b6722c75d301b33b259099f237c46b2134e"},
+ {file = "regex-2022.9.13-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6d43bd402b27e0e7eae85c612725ba1ce7798f20f6fab4e8bc3de4f263294f03"},
+ {file = "regex-2022.9.13-cp39-cp39-win32.whl", hash = "sha256:7fcf7f94ccad19186820ac67e2ec7e09e0ac2dac39689f11cf71eac580503296"},
+ {file = "regex-2022.9.13-cp39-cp39-win_amd64.whl", hash = "sha256:322bd5572bed36a5b39952d88e072738926759422498a96df138d93384934ff8"},
+ {file = "regex-2022.9.13.tar.gz", hash = "sha256:f07373b6e56a6f3a0df3d75b651a278ca7bd357a796078a26a958ea1ce0588fd"},
+]
+requests = [
+ {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"},
+ {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"},
+]
+setuptools = [
+ {file = "setuptools-65.4.0-py3-none-any.whl", hash = "sha256:c2d2709550f15aab6c9110196ea312f468f41cd546bceb24127a1be6fdcaeeb1"},
+ {file = "setuptools-65.4.0.tar.gz", hash = "sha256:a8f6e213b4b0661f590ccf40de95d28a177cd747d098624ad3f69c40287297e9"},
+]
+six = [
+ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
+ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
+]
+sounddevice = [
+ {file = "sounddevice-0.4.5-py3-none-any.whl", hash = "sha256:5cea4afd9412e731f50ae09a54d68b10628a604cfd56b42a976c54d424c6c39d"},
+ {file = "sounddevice-0.4.5-py3-none-macosx_10_6_x86_64.macosx_10_6_universal2.whl", hash = "sha256:0875173595a8bd5a66b5a03a3d958e7b89c3b956b8befbe4491a24a3ce7784c0"},
+ {file = "sounddevice-0.4.5-py3-none-win32.whl", hash = "sha256:442adf53850916374a58f902200aaf9412227378181264e60c966da64be47d41"},
+ {file = "sounddevice-0.4.5-py3-none-win_amd64.whl", hash = "sha256:d3216c5d3d678c3301058e9aac7000879e255140c524c9ef98730091b67ea676"},
+ {file = "sounddevice-0.4.5.tar.gz", hash = "sha256:2fe0d41299e4f3037dad2acede4eff0666b34a1fa3da5335e47120373964bef5"},
+]
+tokenizers = [
+ {file = "tokenizers-0.12.1-cp310-cp310-macosx_10_11_x86_64.whl", hash = "sha256:d737df0f8f26e093a82bfb106b6cfb510a0e9302d35834568e5b20b73ddc5a9c"},
+ {file = "tokenizers-0.12.1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f1271224acafb27639c432e1ce4e7d38eab40305ba1c546e871d5c8a32f4f195"},
+ {file = "tokenizers-0.12.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdeba37c2fb44e1aec8a72af4cb369655b59ba313181b1b4b8183f08e759c49c"},
+ {file = "tokenizers-0.12.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53b5f4012ce3ffddd5b00827441b80dc7a0f6b41f4fc5248ae6d36e7d3920c6d"},
+ {file = "tokenizers-0.12.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5188e13fc09edfe05712ca3ae5a44e7f2b0137927b1ca210d0fad90d3e58315a"},
+ {file = "tokenizers-0.12.1-cp310-cp310-win32.whl", hash = "sha256:eff5ff411f18a201eec137b7b32fcb55e0c48b372d370bd24f965f5bad471fa4"},
+ {file = "tokenizers-0.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:bdbca79726fe883c696088ea163715b2f902aec638a8e24bcf9790ff8fa45019"},
+ {file = "tokenizers-0.12.1-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:28825dade9e52ad464164020758f9d49eb7251c32b6ae146601c506a23c67c0e"},
+ {file = "tokenizers-0.12.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91906d725cb84d8ee71ce05fbb155d39d494849622b4f9349e5176a8eb01c49b"},
+ {file = "tokenizers-0.12.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:230f51a0a82ca7b90077eaca2415f12ff9bd144607888b9c50c2ee543452322e"},
+ {file = "tokenizers-0.12.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d4339c376b695de2ad8ccaebffa75e4dc1d7857be1103d80e7925b34af8cf78"},
+ {file = "tokenizers-0.12.1-cp37-cp37m-macosx_10_11_x86_64.whl", hash = "sha256:27d93b712aa2d4346aa506ecd4ec9e94edeebeaf2d484357b482cdeffc02b5f5"},
+ {file = "tokenizers-0.12.1-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7f4cb68dc538b52240d1986d2034eb0a6373be2ab5f0787d1be3ad1444ce71b7"},
+ {file = "tokenizers-0.12.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae6c04b629ac2cd2f695739988cb70b9bd8d5e7f849f5b14c4510e942bee5770"},
+ {file = "tokenizers-0.12.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6a38b2019d4807d42afeff603a119094ee00f63bea2921136524c8814e9003f8"},
+ {file = "tokenizers-0.12.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fde8dccb9033fa344ffce3ee1837939a50e7a210a768f1cf2059beeafa755481"},
+ {file = "tokenizers-0.12.1-cp37-cp37m-win32.whl", hash = "sha256:38625595b2fd37bfcce64ff9bfb6868c07e9a7b7f205c909d94a615ce9472287"},
+ {file = "tokenizers-0.12.1-cp37-cp37m-win_amd64.whl", hash = "sha256:01abe6fbfe55e4131ca0c4c3d1a9d7ef5df424a8d536e998d2a4fc0bc57935f4"},
+ {file = "tokenizers-0.12.1-cp38-cp38-macosx_10_11_x86_64.whl", hash = "sha256:7c5c54080a7d5c89c990e0d478e0882dbac88926d43323a3aa236492a3c9455f"},
+ {file = "tokenizers-0.12.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:419d113e3bcc4fe20a313afc47af81e62906306b08fe1601e1443d747d46af1f"},
+ {file = "tokenizers-0.12.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9779944559cb7ace6a8516e402895f239b0d9d3c833c67dbaec496310e7e206"},
+ {file = "tokenizers-0.12.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7d43de14b4469b57490dbaf136a31c266cb676fa22320f01f230af9219ae9034"},
+ {file = "tokenizers-0.12.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:258873634406bd1d438c799993a5e44bbc0132ff055985c03c4fe30f702e9a33"},
+ {file = "tokenizers-0.12.1-cp38-cp38-win32.whl", hash = "sha256:3f2647cc256d6a53d18b9dcd71d377828e9f8991fbcbd6fcd8ca2ceb174552b0"},
+ {file = "tokenizers-0.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:62a723bd4b18bc55121f5c34cd8efd6c651f2d3b81f81dd50e5351fb65b8a617"},
+ {file = "tokenizers-0.12.1-cp39-cp39-macosx_10_11_x86_64.whl", hash = "sha256:411ebc89228f30218ffa9d9c49d414864b0df5026a47c24820431821c4360460"},
+ {file = "tokenizers-0.12.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:619728df2551bdfe6f96ff177f9ded958e7ed9e2af94c8d5ac2834d1eb06d112"},
+ {file = "tokenizers-0.12.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8cea98f3f9577d1541b7bb0f7a3308a911751067e1d83e01485c9d3411bbf087"},
+ {file = "tokenizers-0.12.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664f36f0a0d409c24f2201d495161fec4d8bc93e091fbb78814eb426f29905a3"},
+ {file = "tokenizers-0.12.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0bf2380ad59c50222959a9b6f231339200a826fc5cb2be09ff96d8a59f65fc5e"},
+ {file = "tokenizers-0.12.1-cp39-cp39-win32.whl", hash = "sha256:6a7a106d04154c2159db6cd7d042af2e2e0e53aee432f872fe6c8be45100436a"},
+ {file = "tokenizers-0.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:2158baf80cbc09259bfd6e0e0fc4597b611e7a72ad5443dad63918a90f1dd304"},
+ {file = "tokenizers-0.12.1.tar.gz", hash = "sha256:070746f86efa6c873db341e55cf17bb5e7bdd5450330ca8eca542f5c3dab2c66"},
+]
+toml = [
+ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
+ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
+]
+tomli = [
+ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
+ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
+]
+torch = [
+ {file = "torch-1.12.1-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:9c038662db894a23e49e385df13d47b2a777ffd56d9bcd5b832593fab0a7e286"},
+ {file = "torch-1.12.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:4e1b9c14cf13fd2ab8d769529050629a0e68a6fc5cb8e84b4a3cc1dd8c4fe541"},
+ {file = "torch-1.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:e9c8f4a311ac29fc7e8e955cfb7733deb5dbe1bdaabf5d4af2765695824b7e0d"},
+ {file = "torch-1.12.1-cp310-none-macosx_10_9_x86_64.whl", hash = "sha256:976c3f997cea38ee91a0dd3c3a42322785414748d1761ef926b789dfa97c6134"},
+ {file = "torch-1.12.1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:68104e4715a55c4bb29a85c6a8d57d820e0757da363be1ba680fa8cc5be17b52"},
+ {file = "torch-1.12.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:743784ccea0dc8f2a3fe6a536bec8c4763bd82c1352f314937cb4008d4805de1"},
+ {file = "torch-1.12.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b5dbcca369800ce99ba7ae6dee3466607a66958afca3b740690d88168752abcf"},
+ {file = "torch-1.12.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f3b52a634e62821e747e872084ab32fbcb01b7fa7dbb7471b6218279f02a178a"},
+ {file = "torch-1.12.1-cp37-none-macosx_10_9_x86_64.whl", hash = "sha256:8a34a2fbbaa07c921e1b203f59d3d6e00ed379f2b384445773bd14e328a5b6c8"},
+ {file = "torch-1.12.1-cp37-none-macosx_11_0_arm64.whl", hash = "sha256:42f639501928caabb9d1d55ddd17f07cd694de146686c24489ab8c615c2871f2"},
+ {file = "torch-1.12.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:0b44601ec56f7dd44ad8afc00846051162ef9c26a8579dda0a02194327f2d55e"},
+ {file = "torch-1.12.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:cd26d8c5640c3a28c526d41ccdca14cf1cbca0d0f2e14e8263a7ac17194ab1d2"},
+ {file = "torch-1.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:42e115dab26f60c29e298559dbec88444175528b729ae994ec4c65d56fe267dd"},
+ {file = "torch-1.12.1-cp38-none-macosx_10_9_x86_64.whl", hash = "sha256:a8320ba9ad87e80ca5a6a016e46ada4d1ba0c54626e135d99b2129a4541c509d"},
+ {file = "torch-1.12.1-cp38-none-macosx_11_0_arm64.whl", hash = "sha256:03e31c37711db2cd201e02de5826de875529e45a55631d317aadce2f1ed45aa8"},
+ {file = "torch-1.12.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:9b356aea223772cd754edb4d9ecf2a025909b8615a7668ac7d5130f86e7ec421"},
+ {file = "torch-1.12.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:6cf6f54b43c0c30335428195589bd00e764a6d27f3b9ba637aaa8c11aaf93073"},
+ {file = "torch-1.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:f00c721f489089dc6364a01fd84906348fe02243d0af737f944fddb36003400d"},
+ {file = "torch-1.12.1-cp39-none-macosx_10_9_x86_64.whl", hash = "sha256:bfec2843daa654f04fda23ba823af03e7b6f7650a873cdb726752d0e3718dada"},
+ {file = "torch-1.12.1-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:69fe2cae7c39ccadd65a123793d30e0db881f1c1927945519c5c17323131437e"},
+]
+tqdm = [
+ {file = "tqdm-4.64.1-py2.py3-none-any.whl", hash = "sha256:6fee160d6ffcd1b1c68c65f14c829c22832bc401726335ce92c52d395944a6a1"},
+ {file = "tqdm-4.64.1.tar.gz", hash = "sha256:5f4f682a004951c1b450bc753c710e9280c5746ce6ffedee253ddbcbf54cf1e4"},
+]
+transformers = [
+ {file = "transformers-4.22.1-py3-none-any.whl", hash = "sha256:8f06d0dbdb95717fc4a48c61d876f25a35ca67e95851581c681fe4e6d1cf9f94"},
+ {file = "transformers-4.22.1.tar.gz", hash = "sha256:b9834aa01979778c16130ad260e41d314f4c3a65930bdc40b1232833c988280b"},
+]
+typing-extensions = [
+ {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"},
+ {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"},
+]
+urllib3 = [
+ {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"},
+ {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"},
+]
+whisper = []
diff --git a/pyproject.toml b/pyproject.toml
index 789d4818..89356e7b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,198 +1,29 @@
-[project]
-name = "buzz-captions"
-# Change also in Makefile and buzz/__version__.py
-version = "1.4.4"
+[tool.poetry]
+name = "buzz"
+version = "0.2.1"
description = ""
-authors = [{ name = "Chidi Williams", email = "williamschidi1@gmail.com" }]
-requires-python = ">=3.12,<3.13"
+authors = ["Chidi Williams "]
+license = "MIT"
readme = "README.md"
-# License format change to remove warning in PyPI will cause snap not to build
-license = { text = "MIT" }
-dependencies = [
- "sounddevice>=0.5.3,<0.6",
- "humanize>=4.4.0,<5",
- "PyQt6==6.9.1",
- "PyQt6-Qt6==6.9.1",
- "PyQt6-sip==13.10.2",
- "openai>=1.14.2,<2",
- "keyring>=25.0.0,<26",
- "platformdirs>=4.2.1,<5",
- "dataclasses-json>=0.6.4,<0.7",
- "numpy>=1.21.2,<2",
- "requests>=2.31.0,<3",
- "yt-dlp>=2026.2.21",
- "stable-ts>=2.19.1,<3",
- "faster-whisper>=1.2.1,<2",
- "openai-whisper==20250625",
- "transformers>=4.53,<5",
- "accelerate>=1.12.0,<2",
- "peft>=0.14.0,<1",
- # Overriden in uv.tool section below to ensure CUDA 12.9 compatibility
- # Skip on Intel Macs (x86_64), use 0.49.0 on ARM Macs, 0.45.0+ elsewhere
- "bitsandbytes>=0.45.0; sys_platform != 'darwin' or platform_machine != 'x86_64'",
- "polib>=1.2.0,<2",
- "srt-equalizer>=0.1.10,<0.2",
- # For Intel macOS (x86_64) - use older versions that support Intel
- "torch==2.2.2; sys_platform == 'darwin' and platform_machine == 'x86_64'",
- "torchaudio==2.2.2; sys_platform == 'darwin' and platform_machine == 'x86_64'",
- "ctranslate2==4.3.1; sys_platform == 'darwin' and platform_machine == 'x86_64'",
- # For ARM macOS (arm64) - use latest CPU-only versions from PyPI
- "torch==2.8.0; sys_platform == 'darwin' and platform_machine == 'arm64'",
- "torchaudio==2.8.0; sys_platform == 'darwin' and platform_machine == 'arm64'",
- "ctranslate2>=4.6.2,<5; sys_platform == 'darwin' and platform_machine == 'arm64'",
- # For Linux/Windows - use CUDA versions from pytorch index
- "torch==2.8.0; sys_platform != 'darwin'",
- "torchaudio==2.8.0; sys_platform != 'darwin'",
- "ctranslate2>=4.6.2,<5; sys_platform != 'darwin'",
- # faster whisper need cudnn 9
- "nvidia-cudnn-cu12>=9,<10; sys_platform != 'darwin'",
- # CUDA runtime libraries are provided by torch dependencies, no need to specify explicitly
- "darkdetect>=0.8.0,<0.9",
- "dora-search>=0.1.12,<0.2",
- "diffq>=0.2.4,<0.3",
- "einops>=0.8.1,<0.9",
- "flake8>=7.1.2,<8",
- "hydra-colorlog>=1.2.0,<2",
- "hydra-core>=1.3.2,<2",
- "julius>=0.2.7,<0.3",
- "lameenc>=1.8.1,<2",
- "museval>=0.4.1,<0.5",
- "mypy>=1.15.0,<2",
- "openunmix>=1.3.0,<2",
- "pyyaml>=6.0.2,<7",
- "submitit>=1.5.2,<2",
- "tqdm>=4.67.1,<5",
- "treetable>=0.2.5,<0.3",
- "soundfile>=0.13.1,<0.14",
- "urllib3>=2.6.0,<3",
- "posthog>=3.23.0,<4",
- # This version works, newer have issues on Windows
- "onnxruntime==1.18.1",
- "onnx>=1.20.0", # Required for nemo-toolkit, ensures ml-dtypes is installed
- "vulkan>=1.3.275.1,<2",
- "hf-xet>=1.1.5,<2",
- "hatchling>=1.28.0",
- "cmake>=4.2.0,<5",
- # 2.5.3 is last versions with cuda 12
- "nemo-toolkit[asr]==2.5.3; sys_platform != 'darwin' or platform_machine != 'x86_64'",
- "nltk>=3.9.2",
- "uroman>=1.3.1.1",
- "lhotse==1.32.1",
- "coverage==7.12.0",
- # demucs is bundled directly in the wheel from demucs_repo/, not installed as a dependency
- "certifi==2025.11.12",
- "torchcodec>=0.9.0; sys_platform != 'darwin' or platform_machine != 'x86_64'",
- "torch>=2.2.2",
- "torchaudio>=2.2.2",
- "datasets>=4.4.1",
-]
-repository = "https://github.com/chidiwilliams/buzz"
-documentation = "https://chidiwilliams.github.io/buzz/docs"
-[project.scripts]
-buzz = "buzz.buzz:main"
+[tool.poetry.dependencies]
+python = "~3.10.1"
+sounddevice = "^0.4.5"
+whisper = {git = "https://github.com/openai/whisper.git"}
+PyQt5 = "^5.15.7"
+torch = "^1.12.1"
+tqdm = "^4.64.1"
+ffmpeg = "^1.4"
+numpy = "^1.23.3"
+transformers = "^4.22.1"
-[dependency-groups]
-dev = [
- "autopep8>=2.3.2,<3",
- "pyinstaller>=6.12.0,<7",
- "pyinstaller-hooks-contrib~=2025.1",
- "six>=1.16.0,<2",
- "pytest>=7.1.3,<8",
- "pytest-cov>=4.0.0,<5",
- "pytest-qt>=4.1.0,<5",
- "pytest-xvfb>=2.0.0,<3",
- "pytest-mock>=3.12.0,<4",
- "pytest-timeout>=2.4.0,<3",
- "pylint>=2.15.5,<3",
- "pre-commit>=2.20.0,<3",
- "pytest-benchmark>=4.0.0,<5",
- "ruff>=0.1.3,<0.2",
-]
-build = [
- "cmake>=4.2.0,<5",
- "polib>=1.2.0,<2",
-]
-
-[tool.uv]
-index-strategy = "unsafe-best-match"
-default-groups = [
- "dev",
- "build",
-]
-
-# Should be removed after nemo-toolkit update to 2.6.0
-# Forcing a CUDA 12.9 compatable bitsandbytes version
-# ARM Macs use 0.49.0, others use 0.47.0 (Intel Macs skip entirely via marker)
-override-dependencies = [
- "bitsandbytes==0.49.0; sys_platform == 'darwin' and platform_machine == 'arm64'",
- "bitsandbytes==0.47.0; sys_platform != 'darwin'",
-]
-
-[tool.uv.sources]
-torch = [
- { index = "PyPI", marker = "sys_platform == 'darwin'" },
- { index = "pytorch-cu129", marker = "sys_platform != 'darwin'" },
-]
-torchaudio = [
- { index = "PyPI", marker = "sys_platform == 'darwin'" },
- { index = "pytorch-cu129", marker = "sys_platform != 'darwin'" },
-]
-
-[[tool.uv.index]]
-name = "nvidia"
-url = "https://pypi.ngc.nvidia.com/"
-
-[[tool.uv.index]]
-name = "pytorch-cu129"
-url = "https://download.pytorch.org/whl/cu129"
-
-[[tool.uv.index]]
-name = "PyPI"
-url = "https://pypi.org/simple/"
-default = true
-
-[tool.hatch.metadata]
-allow-direct-references = true
-
-[tool.hatch.build.targets.sdist]
-include = [
- "buzz",
- "buzz/whisper_cpp/*",
- "buzz/locale/*/LC_MESSAGES/buzz.mo",
- "demucs_repo",
- "whisper_diarization",
- "deepmultilingualpunctuation",
- "ctc_forced_aligner",
-]
-
-[tool.hatch.build.targets.wheel]
-include = [
- "buzz",
- "buzz/whisper_cpp/*",
- "buzz/locale/*/LC_MESSAGES/buzz.mo",
- "whisper_diarization",
- "deepmultilingualpunctuation",
- "ctc_forced_aligner",
-]
-# Map demucs_repo/demucs to top-level demucs/ so 'import demucs' works
-sources = {"demucs_repo/demucs" = "demucs"}
-
-[tool.hatch.build.hooks.custom]
+[tool.poetry.group.dev.dependencies]
+autopep8 = "^1.7.0"
+pyinstaller = "^5.4.1"
+six = "^1.16.0"
+pytest = "^7.1.3"
+pytest-cov = "^4.0.0"
[build-system]
-requires = ["hatchling", "cmake>=4.2.0,<5", "polib>=1.2.0,<2", "pybind11", "setuptools>=80.9.0"]
-build-backend = "hatchling.build"
-
-[tool.coverage.report]
-exclude_also = [
- "if sys.platform == \"win32\":",
- "if platform.system\\(\\) == \"Windows\":",
- "if platform.system\\(\\) == \"Linux\":",
- "if platform.system\\(\\) == \"Darwin\":",
-]
-
-[tool.ruff]
-exclude = [
- "**/whisper.cpp",
-]
+requires = ["poetry-core"]
+build-backend = "poetry.core.masonry.api"
diff --git a/pytest.ini b/pytest.ini
deleted file mode 100644
index 701a9fff..00000000
--- a/pytest.ini
+++ /dev/null
@@ -1,12 +0,0 @@
-[pytest]
-log_cli = 1
-log_cli_level = DEBUG
-qt_api=pyqt6
-log_format = %(asctime)s %(levelname)s %(module)s::%(funcName)s %(message)s
-log_date_format = %Y-%m-%d %H:%M:%S
-addopts = -x -s -p no:xdist -p no:pytest_parallel
-timeout = 900
-timeout_method = thread
-testpaths = tests
-markers =
- timeout: set a timeout on a test function.
\ No newline at end of file
diff --git a/readme/README.zh_CN.md b/readme/README.zh_CN.md
deleted file mode 100644
index 49a37e40..00000000
--- a/readme/README.zh_CN.md
+++ /dev/null
@@ -1,53 +0,0 @@
-[[English](../README.md)] <- Click here to View the English page.
-
-# Buzz
-
-[项目文档](https://chidiwilliams.github.io/buzz/zh/docs)
-
-在个人电脑上离线转录和翻译音频。技术模型来源 OpenAI [Whisper](https://github.com/openai/whisper).
-
-
-[](https://github.com/chidiwilliams/buzz/actions/workflows/ci.yml)
-[](https://codecov.io/github/chidiwilliams/buzz)
-
-[](https://GitHub.com/chidiwilliams/buzz/releases/)
-
-## 安装
-
-**PyPI**:
-
-安装 [ffmpeg](https://www.ffmpeg.org/download.html)
-
-安装 Buzz
-
-```shell
-pip install buzz-captions
-python -m buzz
-```
-
-**macOS**:
-
-使用 [brew utility](https://brew.sh/) 安装
-
-```shell
-brew install --cask buzz
-```
-
-或下载并运行在 [Releases ](https://github.com/chidiwilliams/buzz/releases/latest) 页面中的 `.dmg` 文件 .
-
-**Windows**:
-
-下载并运行在 [Releases ](https://github.com/chidiwilliams/buzz/releases/latest) 页面中的 `.exe` 文件。
-
-应用程序未获得签名,当安装时会收到警告弹窗。 选择 `更多信息` -> `仍然运行`.
-
-**Linux**:
-
-```shell
-sudo apt-get install libportaudio2 libcanberra-gtk-module libcanberra-gtk3-module
-sudo snap install buzz
-```
-
-### 最新开发者版本
-
-有关如何获取具有最新功能和错误修复的最新开发版本的信息,请查阅 [FAQ](https://chidiwilliams.github.io/buzz/docs/faq#9-where-can-i-get-latest-development-version).
diff --git a/share/applications/buzz.desktop b/share/applications/buzz.desktop
deleted file mode 100644
index 1e8cf81d..00000000
--- a/share/applications/buzz.desktop
+++ /dev/null
@@ -1,17 +0,0 @@
-[Desktop Entry]
-
-Type=Application
-
-Encoding=UTF-8
-
-Name=Buzz
-
-Comment=Buzz transcribes and translates audio offline on your personal computer.
-
-Path=/opt/buzz
-
-Exec=/opt/buzz/Buzz
-
-Icon=buzz
-
-Terminal=false
diff --git a/share/applications/io.github.chidiwilliams.Buzz.desktop b/share/applications/io.github.chidiwilliams.Buzz.desktop
deleted file mode 100644
index b087112a..00000000
--- a/share/applications/io.github.chidiwilliams.Buzz.desktop
+++ /dev/null
@@ -1,15 +0,0 @@
-[Desktop Entry]
-
-Type=Application
-
-Encoding=UTF-8
-
-Name=Buzz
-
-Comment=Transcribe and translate audio
-
-Exec=run-buzz.sh
-
-Icon=io.github.chidiwilliams.Buzz
-
-Terminal=false
diff --git a/share/icons/io.github.chidiwilliams.Buzz.svg b/share/icons/io.github.chidiwilliams.Buzz.svg
deleted file mode 100644
index d5b67bc0..00000000
--- a/share/icons/io.github.chidiwilliams.Buzz.svg
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/share/metainfo/io.github.chidiwilliams.Buzz.metainfo.xml b/share/metainfo/io.github.chidiwilliams.Buzz.metainfo.xml
deleted file mode 100644
index ee594f28..00000000
--- a/share/metainfo/io.github.chidiwilliams.Buzz.metainfo.xml
+++ /dev/null
@@ -1,142 +0,0 @@
-
-
- io.github.chidiwilliams.Buzz
-
- Buzz
- Transcribe and translate audio
- CC0-1.0
- MIT
- Chidi Williams
-
-
-
- Buzz transcribes and translates audio to text offline using OpenAI's Whisper. Import audio and video files into Buzz and export them as TXT, SRT, or VTT files. Buzz supports Whisper, Whisper.cpp, Faster Whisper, Whisper-compatible models from the Hugging Face repository, and the OpenAI Whisper API.
-
-
- Required permissions in Buzz will let you select audio and video files for transcription, from most common file location on your computer. Network permission is used to download transcription model files. Microphone permission lets you transcribe real time speech.
-
-
- Note: If your system theme is not applied to Buzz, ensure it is in ~/.themes folder. You may need to copy the system themes to this folder cp -r /usr/share/themes/ ~/.themes/ and give Flatpaks access to this folder flatpak override --user --filesystem=~/.themes.
-
-
-
-
- AudioVideo
-
-
- https://github.com/chidiwilliams/buzz/issues
- https://github.com/chidiwilliams/buzz
- https://chidiwilliams.github.io/buzz/docs
- https://github.com/chidiwilliams/buzz
-
-
- #f66151
- #45124d
-
-
-
- keyboard
- pointing
-
- io.github.chidiwilliams.Buzz.desktop
-
-
- https://raw.githubusercontent.com/chidiwilliams/buzz/98ea5b2f1b209e26d8ac49313e23697b88dce01d/share/screenshots/buzz-1-import.png
- File and url import options
-
-
- https://raw.githubusercontent.com/chidiwilliams/buzz/98ea5b2f1b209e26d8ac49313e23697b88dce01d/share/screenshots/buzz-2-main_screen.png
- Main screen with transcription results
-
-
- https://raw.githubusercontent.com/chidiwilliams/buzz/98ea5b2f1b209e26d8ac49313e23697b88dce01d/share/screenshots/buzz-3-preferences.png
- Application preferences
-
-
- https://raw.githubusercontent.com/chidiwilliams/buzz/98ea5b2f1b209e26d8ac49313e23697b88dce01d/share/screenshots/buzz-4-transcript.png
- Transcript with options for further processing and export
-
-
- https://raw.githubusercontent.com/chidiwilliams/buzz/98ea5b2f1b209e26d8ac49313e23697b88dce01d/share/screenshots/buzz-5-live_recording.png
- Live recording transcription and translation options
-
-
-
-
-
-
-
- https://github.com/chidiwilliams/buzz/releases/tag/v1.4.4
-
- Bug fixes and minor improvements.
-
- Fixed Youtube link downloading
- Added option to import folder
- Extra settings for live recordings
- Adjusted live recording batching process to avoid min-word cuts
- Update checker for Windows and Macs
- Added voice activity detection to whisper.cpp
-
-
-
-
- https://github.com/chidiwilliams/buzz/releases/tag/v1.4.3
-
- Fixed support for whisper.cpp on older CPUs and issues in speaker identification.
-
-
-
- https://github.com/chidiwilliams/buzz/releases/tag/v1.4.2
-
- Adding speaker identification on transcriptions and video support for transcription viewer, improvements to transcription table and support for over 1000 of worlds languages via MMS models as well as separate window to show live transcripts on a projector.
- Release details:
-
- Speaker identification on finished transcripts
- Support for video in transcription viewer
- Presentation (projector) window for live transcripts
- Ability to add notes and restart transcriptions in main table
- Adding support for more than 1000 languages via MMS model family when transcribing with Huggingface transcription type
- Adding support for PEFT models when transcribing with Huggingface transcription type
- Adding support for 8bit quantization for Huggingface and faster Whisper transcriptions
- Updated libraries and dependencies to support latest GPUs
- Support for secrets portal for snap packages on Linux
- Ability to specify model to use when transcribing with OpenAI API
- Ability to access application logs from About screen
-
-
-
-
- https://github.com/chidiwilliams/buzz/releases/tag/v1.3.3
-
- This release introduces Vulkan GPU support for whisper.cpp making it significantly faster even on laptops.
- Real-time transcription is possible even with large models on computers with ~5GB RAM video cards. There
- is now an option to separate voice tracks before the audio is transcribed. This can improve transcript
- accuracy for audios with background noises or music. Faster whisper was updated to the latest version
- adding noticeable speed improvement.
- Additional improvements:
-
- Option to switch the UI language from preferences
- Library updates for better Linux compatibility, especially in Flatpak installations
- Option to upload live transcripts to a server
- Search and additional controls in Transcription viewer
- Added UI translation for German, Dutch, Danish and Portuguese (Brazilian)
- Minor bug fixes
-
-
-
-
- https://github.com/chidiwilliams/buzz/releases/tag/v1.2.0
-
- Added support for Dark mode and the Turbo models. More details:
-
- Dark mode support
- Improved support for GPUs and Apple Core ML
- Added support for the Turbo models
- Sliding window mode for live transcriptions
- Bugfixes and other small improvements
- Japanese UI translations
-
-
-
-
-
\ No newline at end of file
diff --git a/share/screenshots/buzz-1-import.png b/share/screenshots/buzz-1-import.png
deleted file mode 100644
index 575fc65c..00000000
Binary files a/share/screenshots/buzz-1-import.png and /dev/null differ
diff --git a/share/screenshots/buzz-2-main_screen.png b/share/screenshots/buzz-2-main_screen.png
deleted file mode 100644
index 97ffb797..00000000
Binary files a/share/screenshots/buzz-2-main_screen.png and /dev/null differ
diff --git a/share/screenshots/buzz-3-preferences.png b/share/screenshots/buzz-3-preferences.png
deleted file mode 100644
index 02c0f2ad..00000000
Binary files a/share/screenshots/buzz-3-preferences.png and /dev/null differ
diff --git a/share/screenshots/buzz-3.2-model-preferences.png b/share/screenshots/buzz-3.2-model-preferences.png
deleted file mode 100644
index d699ffb9..00000000
Binary files a/share/screenshots/buzz-3.2-model-preferences.png and /dev/null differ
diff --git a/share/screenshots/buzz-4-transcript.png b/share/screenshots/buzz-4-transcript.png
deleted file mode 100644
index 5f694dee..00000000
Binary files a/share/screenshots/buzz-4-transcript.png and /dev/null differ
diff --git a/share/screenshots/buzz-5-live_recording.png b/share/screenshots/buzz-5-live_recording.png
deleted file mode 100644
index c41c4e69..00000000
Binary files a/share/screenshots/buzz-5-live_recording.png and /dev/null differ
diff --git a/share/screenshots/buzz-6-resize.png b/share/screenshots/buzz-6-resize.png
deleted file mode 100644
index 4ae3f120..00000000
Binary files a/share/screenshots/buzz-6-resize.png and /dev/null differ
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
deleted file mode 100644
index 8159b602..00000000
--- a/snap/snapcraft.yaml
+++ /dev/null
@@ -1,199 +0,0 @@
-# Development notes:
-# - To build the snap run `snapcraft clean` and `snapcraft pack --verbose`
-# - To install local snap `snap install ./buzz_*.snap --dangerous`
-name: buzz
-base: core24
-version: git
-title: Buzz
-summary: Buzz, offline audio transcription and translation
-website: https://buzzcaptions.com
-source-code: https://github.com/chidiwilliams/buzz
-issues: https://github.com/chidiwilliams/buzz/issues
-contact: https://github.com/chidiwilliams
-description: |
- Buzz transcribes and translates audio to text offline using OpenAI's Whisper.
- Import audio and video files into Buzz and export them as TXT, SRT, or VTT files.
- Buzz supports Whisper, Whisper.cpp, Faster Whisper, Whisper-compatible models
- from the Hugging Face repository, and the OpenAI Whisper API.
-grade: stable
-confinement: strict
-license: MIT
-icon: buzz/assets/buzz.svg
-
-platforms:
- amd64:
-
-parts:
- alsa-pulseaudio:
- plugin: dump
- source: .
- override-pull: |
- mkdir etc -p
- cat > etc/asound.conf < $CRAFT_PART_INSTALL/bin/buzz-launcher
- chmod +x $CRAFT_PART_INSTALL/bin/buzz-launcher
-
- # Copy source files
- cp -r $CRAFT_PART_BUILD/buzz $CRAFT_PART_INSTALL/
- cp -r $CRAFT_PART_BUILD/ctc_forced_aligner $CRAFT_PART_INSTALL/
- cp -r $CRAFT_PART_BUILD/deepmultilingualpunctuation $CRAFT_PART_INSTALL/
- cp -r $CRAFT_PART_BUILD/demucs_repo $CRAFT_PART_INSTALL/
- cp -r $CRAFT_PART_BUILD/whisper_diarization $CRAFT_PART_INSTALL/
-
- # Create desktop file
- mkdir -p $CRAFT_PART_INSTALL/usr/share/applications
- cp $CRAFT_PART_BUILD/buzz.desktop $CRAFT_PART_INSTALL/usr/share/applications/
- stage:
- - '*'
- prime:
- - '*'
-
- gpu-2404:
- after: [ buzz ]
- source: https://github.com/canonical/gpu-snap.git
- plugin: dump
- override-prime: |
- craftctl default
- ${CRAFT_PART_SRC}/bin/gpu-2404-cleanup mesa-2404
- # Workaround for https://bugs.launchpad.net/snapd/+bug/2055273
- mkdir -p "${CRAFT_PRIME}/gpu-2404"
- prime:
- - bin/gpu-2404-wrapper
-
-apps:
- buzz:
- extensions:
- - gnome
- command-chain:
- - bin/gpu-2404-wrapper
- command: snap/command-chain/desktop-launch $SNAP/bin/buzz-launcher
- desktop: usr/share/applications/buzz.desktop
- environment:
- PATH: $SNAP/usr/bin:$SNAP/bin:$PATH
- LD_LIBRARY_PATH: $SNAP/usr/local/lib:$SNAP/lib/python3.12/site-packages/nvidia/cudnn/lib:$SNAP/lib/python3.12/site-packages/PyQt6:$SNAP/lib/python3.12/site-packages/PyQt6/Qt6/lib:$SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/lapack:$SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/blas:$SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/oss4-libsalsa:$SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libproxy:$SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/alsa-lib:$SNAP:$LD_LIBRARY_PATH
- PYTHONPATH: $SNAP:$SNAP/lib/python3.12/site-packages/PyQt6:$SNAP/lib/python3.12/site-packages/PyQt6/Qt6/lib:$SNAP/usr/lib/python3/dist-packages:$SNAP/usr/lib/python3.12/site-packages:$SNAP/usr/local/lib/python3.12/dist-packages:$SNAP/usr/lib/python3.12/dist-packages:$PYTHONPATH
- QT_MEDIA_BACKEND: ffmpeg
- PULSE_LATENCY_MSEC: "30"
- ALSA_CONFIG_PATH: $SNAP/etc/asound.conf
- plugs:
- - x11
- - unity7
- - wayland
- - home
- - network
- - network-bind
- - desktop
- - desktop-legacy
- - gsettings
- - opengl
- - removable-media
- - audio-playback
- - audio-record
- # Fallback for keyring support if secrets portal is missing, user has to connect this manually
- - password-manager-service
-
-layout:
- /usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/alsa-lib:
- bind: $SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/alsa-lib
diff --git a/testdata/audio-long.mp3 b/testdata/audio-long.mp3
deleted file mode 100644
index a9c70887..00000000
Binary files a/testdata/audio-long.mp3 and /dev/null differ
diff --git a/testdata/ggml-tiny.bin b/testdata/ggml-tiny.bin
deleted file mode 100644
index 1351aeba..00000000
Binary files a/testdata/ggml-tiny.bin and /dev/null differ
diff --git a/testdata/whisper-french.mp3 b/testdata/whisper-french.mp3
deleted file mode 100644
index 92ba03df..00000000
Binary files a/testdata/whisper-french.mp3 and /dev/null differ
diff --git a/testdata/whisper-latvian.wav b/testdata/whisper-latvian.wav
deleted file mode 100644
index d62d5937..00000000
Binary files a/testdata/whisper-latvian.wav and /dev/null differ
diff --git a/tests/__init__.py b/tests/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/tests/app_main.py b/tests/app_main.py
deleted file mode 100644
index 27a8f90f..00000000
--- a/tests/app_main.py
+++ /dev/null
@@ -1,21 +0,0 @@
-import os
-from unittest.mock import patch
-from buzz.buzz import main
-
-class TestMain:
- def test_main(self):
- with patch('buzz.widgets.application.Application') as mock_application, \
- patch('buzz.cli.parse_command_line') as mock_parse_command_line, \
- patch('buzz.buzz.sys') as mock_sys, \
- patch('buzz.buzz.user_log_dir', return_value='/tmp/buzz') as mock_log_dir:
-
- mock_application.return_value.exec.return_value = 0
-
- mock_sys.argv = ['buzz.py']
-
- main()
-
- mock_application.assert_called_once_with(mock_sys.argv)
- mock_parse_command_line.assert_called_once_with(mock_application.return_value)
- mock_application.return_value.exec.assert_called_once()
- assert os.path.isdir(mock_log_dir.return_value), "Log dir was not created"
diff --git a/tests/audio.py b/tests/audio.py
deleted file mode 100644
index 76a9b459..00000000
--- a/tests/audio.py
+++ /dev/null
@@ -1,9 +0,0 @@
-import os.path
-
-test_audio_path = os.path.abspath(
- os.path.join(os.path.dirname(__file__), "../testdata/whisper-french.mp3")
-)
-
-test_multibyte_utf8_audio_path = os.path.abspath(
- os.path.join(os.path.dirname(__file__), "../testdata/whisper-latvian.wav")
-)
\ No newline at end of file
diff --git a/tests/cache_test.py b/tests/cache_test.py
deleted file mode 100644
index b94f1d88..00000000
--- a/tests/cache_test.py
+++ /dev/null
@@ -1,31 +0,0 @@
-from buzz.cache import TasksCache
-from buzz.transcriber.transcriber import (
- FileTranscriptionOptions,
- FileTranscriptionTask,
- TranscriptionOptions,
-)
-
-
-class TestTasksCache:
- def test_should_save_and_load(self, tmp_path):
- cache = TasksCache(cache_dir=str(tmp_path))
- tasks = [
- FileTranscriptionTask(
- file_path="1.mp3",
- transcription_options=TranscriptionOptions(),
- file_transcription_options=FileTranscriptionOptions(
- file_paths=["1.mp3"]
- ),
- model_path="",
- ),
- FileTranscriptionTask(
- file_path="2.mp3",
- transcription_options=TranscriptionOptions(),
- file_transcription_options=FileTranscriptionOptions(
- file_paths=["2.mp3"]
- ),
- model_path="",
- ),
- ]
- cache.save(tasks)
- assert cache.load() == tasks
diff --git a/tests/cli_test.py b/tests/cli_test.py
deleted file mode 100644
index 7887acf3..00000000
--- a/tests/cli_test.py
+++ /dev/null
@@ -1,41 +0,0 @@
-import os
-import sys
-from tempfile import mkdtemp
-
-import pytest
-from pytestqt.qtbot import QtBot
-
-from buzz.cli import parse_command_line
-from tests.audio import test_audio_path
-
-
-class TestCLI:
- @pytest.mark.parametrize(
- "qapp_args",
- [
- pytest.param(
- [
- "main.py",
- "add",
- "--task",
- "transcribe",
- "--model-size",
- "tiny",
- "--output-directory",
- mkdtemp(),
- "--txt",
- test_audio_path,
- ],
- )
- ],
- indirect=True,
- )
- def test_cli(self, qapp, qapp_args, qtbot: QtBot):
- output_directory = qapp_args[7]
-
- parse_command_line(qapp)
-
- def output_exists_at_output_directory():
- assert any(file.endswith(".txt") for file in os.listdir(output_directory))
-
- qtbot.wait_until(output_exists_at_output_directory, timeout=5 * 60 * 1000)
diff --git a/tests/conftest.py b/tests/conftest.py
deleted file mode 100644
index a551d91e..00000000
--- a/tests/conftest.py
+++ /dev/null
@@ -1,85 +0,0 @@
-import multiprocessing
-import os
-import platform
-import random
-import string
-
-import pytest
-
-# Set multiprocessing to use 'spawn' instead of 'fork' on Linux
-# This is required because Qt creates threads early, and forking a multi-threaded
-# process can lead to deadlocks. The main application sets this in buzz/buzz.py.
-if platform.system() != "Windows":
- try:
- multiprocessing.set_start_method("spawn", force=True)
- except RuntimeError:
- pass # Already set
-from PyQt6.QtSql import QSqlDatabase
-from _pytest.fixtures import SubRequest
-
-from buzz.db.dao.transcription_dao import TranscriptionDAO
-from buzz.db.dao.transcription_segment_dao import TranscriptionSegmentDAO
-from buzz.db.db import setup_test_db
-from buzz.db.service.transcription_service import TranscriptionService
-from buzz.settings.settings import Settings
-from buzz.settings.shortcuts import Shortcuts
-from buzz.widgets.application import Application
-
-
-@pytest.fixture()
-def db() -> QSqlDatabase:
- db = setup_test_db()
- yield db
- db.close()
- os.remove(db.databaseName())
-
-
-@pytest.fixture()
-def transcription_dao(db, request: SubRequest) -> TranscriptionDAO:
- dao = TranscriptionDAO(db)
- if hasattr(request, "param"):
- transcriptions = request.param
- for transcription in transcriptions:
- dao.insert(transcription)
- return dao
-
-
-@pytest.fixture()
-def transcription_service(
- transcription_dao, transcription_segment_dao
-) -> TranscriptionService:
- return TranscriptionService(transcription_dao, transcription_segment_dao)
-
-
-@pytest.fixture()
-def transcription_segment_dao(db) -> TranscriptionSegmentDAO:
- return TranscriptionSegmentDAO(db)
-
-
-@pytest.fixture(scope="session")
-def qapp_cls():
- return Application
-
-
-@pytest.fixture(scope="session")
-def qapp_args(request):
- if not hasattr(request, "param"):
- return []
-
- return request.param
-
-
-@pytest.fixture(scope="session")
-def settings():
- application = "".join(
- random.choice(string.ascii_letters + string.digits) for _ in range(6)
- )
-
- settings = Settings(application=application)
- yield settings
- settings.clear()
-
-
-@pytest.fixture(scope="session")
-def shortcuts(settings):
- return Shortcuts(settings)
diff --git a/tests/db/dao/transcription_dao_test.py b/tests/db/dao/transcription_dao_test.py
deleted file mode 100644
index 6a0aa1fc..00000000
--- a/tests/db/dao/transcription_dao_test.py
+++ /dev/null
@@ -1,240 +0,0 @@
-import pytest
-from unittest.mock import Mock, patch
-from uuid import UUID, uuid4
-from PyQt6.QtSql import QSqlDatabase, QSqlQuery
-
-from buzz.db.dao.transcription_dao import TranscriptionDAO
-from buzz.db.entity.transcription import Transcription
-
-
-@pytest.fixture
-def db():
- """Create an in-memory SQLite database for testing"""
- db = QSqlDatabase.addDatabase("QSQLITE")
- db.setDatabaseName(":memory:")
- assert db.open()
-
- # Create the transcription table with the new schema
- query = QSqlQuery(db)
- query.exec("""
- CREATE TABLE transcription (
- id TEXT PRIMARY KEY,
- error_message TEXT,
- export_formats TEXT,
- file TEXT,
- output_folder TEXT,
- progress DOUBLE PRECISION DEFAULT 0.0,
- language TEXT,
- model_type TEXT,
- source TEXT,
- status TEXT,
- task TEXT,
- time_ended TIMESTAMP,
- time_queued TIMESTAMP NOT NULL,
- time_started TIMESTAMP,
- url TEXT,
- whisper_model_size TEXT,
- hugging_face_model_id TEXT,
- word_level_timings BOOLEAN DEFAULT FALSE,
- extract_speech BOOLEAN DEFAULT FALSE,
- name TEXT,
- notes TEXT
- )
- """)
-
- yield db
- db.close()
-
-
-@pytest.fixture
-def transcription_dao(db):
- """Create a TranscriptionDAO instance for testing"""
- return TranscriptionDAO(db)
-
-
-@pytest.fixture
-def sample_transcription():
- """Create a sample transcription for testing"""
- return Transcription(
- id=str(uuid4()),
- file="/path/to/test.mp3",
- status="completed",
- time_queued="2023-01-01T00:00:00",
- task="TRANSCRIBE",
- model_type="WHISPER",
- name="Test Transcription",
- notes="This is a test transcription"
- )
-
-
-class TestTranscriptionDAO:
- def test_insert_transcription_with_name_and_notes(self, transcription_dao, sample_transcription):
- """Test inserting a transcription with name and notes fields"""
- transcription_dao.insert(sample_transcription)
-
- # Verify the transcription was inserted
- query = QSqlQuery(transcription_dao.db)
- query.prepare("SELECT * FROM transcription WHERE id = :id")
- query.bindValue(":id", sample_transcription.id)
- assert query.exec()
- assert query.next()
-
- # Check that name and notes were stored
- assert query.value("name") == "Test Transcription"
- assert query.value("notes") == "This is a test transcription"
-
- def test_update_transcription_name(self, transcription_dao, sample_transcription):
- """Test updating transcription name"""
- # Insert the transcription first
- transcription_dao.insert(sample_transcription)
-
- # Update the name
- new_name = "Updated Transcription Name"
- transcription_dao.update_transcription_name(UUID(sample_transcription.id), new_name)
-
- # Verify the name was updated
- query = QSqlQuery(transcription_dao.db)
- query.prepare("SELECT name FROM transcription WHERE id = :id")
- query.bindValue(":id", sample_transcription.id)
- assert query.exec()
- assert query.next()
- assert query.value("name") == new_name
-
- def test_update_transcription_notes(self, transcription_dao, sample_transcription):
- """Test updating transcription notes"""
- # Insert the transcription first
- transcription_dao.insert(sample_transcription)
-
- # Update the notes
- new_notes = "Updated transcription notes with more details"
- transcription_dao.update_transcription_notes(UUID(sample_transcription.id), new_notes)
-
- # Verify the notes were updated
- query = QSqlQuery(transcription_dao.db)
- query.prepare("SELECT notes FROM transcription WHERE id = :id")
- query.bindValue(":id", sample_transcription.id)
- assert query.exec()
- assert query.next()
- assert query.value("notes") == new_notes
-
- def test_update_transcription_name_nonexistent_id(self, transcription_dao):
- """Test updating name for non-existent transcription ID"""
- nonexistent_id = uuid4()
-
- # This should raise an exception
- with pytest.raises(Exception):
- transcription_dao.update_transcription_name(nonexistent_id, "New Name")
-
- def test_update_transcription_notes_nonexistent_id(self, transcription_dao):
- """Test updating notes for non-existent transcription ID"""
- nonexistent_id = uuid4()
-
- # This should raise an exception
- with pytest.raises(Exception):
- transcription_dao.update_transcription_notes(nonexistent_id, "New Notes")
-
- def test_update_transcription_name_empty_string(self, transcription_dao, sample_transcription):
- """Test updating transcription name to empty string"""
- # Insert the transcription first
- transcription_dao.insert(sample_transcription)
-
- # Update the name to empty string
- transcription_dao.update_transcription_name(UUID(sample_transcription.id), "")
-
- # Verify the name was updated to empty string
- query = QSqlQuery(transcription_dao.db)
- query.prepare("SELECT name FROM transcription WHERE id = :id")
- query.bindValue(":id", sample_transcription.id)
- assert query.exec()
- assert query.next()
- assert query.value("name") == ""
-
- def test_update_transcription_notes_empty_string(self, transcription_dao, sample_transcription):
- """Test updating transcription notes to empty string"""
- # Insert the transcription first
- transcription_dao.insert(sample_transcription)
-
- # Update the notes to empty string
- transcription_dao.update_transcription_notes(UUID(sample_transcription.id), "")
-
- # Verify the notes were updated to empty string
- query = QSqlQuery(transcription_dao.db)
- query.prepare("SELECT notes FROM transcription WHERE id = :id")
- query.bindValue(":id", sample_transcription.id)
- assert query.exec()
- assert query.next()
- assert query.value("notes") == ""
-
- def test_update_transcription_name_with_none(self, transcription_dao, sample_transcription):
- """Test updating transcription name to None (should be stored as NULL)"""
- # Insert the transcription first
- transcription_dao.insert(sample_transcription)
-
- # Update the name to None
- transcription_dao.update_transcription_name(UUID(sample_transcription.id), None)
-
- # Verify the name was updated to None (NULL in database)
- query = QSqlQuery(transcription_dao.db)
- query.prepare("SELECT name FROM transcription WHERE id = :id")
- query.bindValue(":id", sample_transcription.id)
- assert query.exec()
- assert query.next()
- # In SQLite, None values are returned as empty strings
- assert query.value("name") == ""
-
- def test_update_transcription_notes_with_none(self, transcription_dao, sample_transcription):
- """Test updating transcription notes to None (should be stored as NULL)"""
- # Insert the transcription first
- transcription_dao.insert(sample_transcription)
-
- # Update the notes to None
- transcription_dao.update_transcription_notes(UUID(sample_transcription.id), None)
-
- # Verify the notes were updated to None (NULL in database)
- query = QSqlQuery(transcription_dao.db)
- query.prepare("SELECT notes FROM transcription WHERE id = :id")
- query.bindValue(":id", sample_transcription.id)
- assert query.exec()
- assert query.next()
- # In SQLite, None values are returned as empty strings
- assert query.value("notes") == ""
-
- def test_insert_transcription_without_name_and_notes(self, transcription_dao):
- """Test inserting a transcription without name and notes (should be NULL)"""
- transcription = Transcription(
- id=str(uuid4()),
- file="/path/to/test.mp3",
- status="completed",
- time_queued="2023-01-01T00:00:00",
- task="TRANSCRIBE",
- model_type="WHISPER"
- # name and notes not provided
- )
-
- transcription_dao.insert(transcription)
-
- # Verify the transcription was inserted with NULL name and notes
- query = QSqlQuery(transcription_dao.db)
- query.prepare("SELECT name, notes FROM transcription WHERE id = :id")
- query.bindValue(":id", transcription.id)
- assert query.exec()
- assert query.next()
-
- # In SQLite, NULL values are returned as empty strings
- assert query.value("name") == ""
- assert query.value("notes") == ""
-
- def test_database_error_handling(self, transcription_dao):
- """Test that database errors are properly handled"""
- # Mock a database error by using an invalid query
- with patch.object(transcription_dao, '_create_query') as mock_create_query:
- mock_query = Mock()
- mock_query.prepare.return_value = True
- mock_query.bindValue.return_value = None
- mock_query.exec.return_value = False
- mock_query.lastError.return_value.text.return_value = "Database error"
- mock_create_query.return_value = mock_query
-
- # This should raise an exception with the database error message
- with pytest.raises(Exception, match="Database error"):
- transcription_dao.update_transcription_name(uuid4(), "Test Name")
diff --git a/tests/db/entity/transcription_test.py b/tests/db/entity/transcription_test.py
deleted file mode 100644
index 1023719c..00000000
--- a/tests/db/entity/transcription_test.py
+++ /dev/null
@@ -1,286 +0,0 @@
-import pytest
-from uuid import uuid4
-
-from buzz.db.entity.transcription import Transcription
-
-
-class TestTranscription:
- def test_transcription_creation_with_name_and_notes(self):
- """Test creating a transcription with name and notes fields"""
- transcription = Transcription(
- id=str(uuid4()),
- file="/path/to/test.mp3",
- status="completed",
- time_queued="2023-01-01T00:00:00",
- task="TRANSCRIBE",
- model_type="WHISPER",
- name="Test Transcription Name",
- notes="This is a test transcription with notes"
- )
-
- assert transcription.name == "Test Transcription Name"
- assert transcription.notes == "This is a test transcription with notes"
-
- def test_transcription_creation_without_name_and_notes(self):
- """Test creating a transcription without name and notes (should be None)"""
- transcription = Transcription(
- id=str(uuid4()),
- file="/path/to/test.mp3",
- status="completed",
- time_queued="2023-01-01T00:00:00",
- task="TRANSCRIBE",
- model_type="WHISPER"
- )
-
- assert transcription.name is None
- assert transcription.notes is None
-
- def test_transcription_creation_with_empty_name_and_notes(self):
- """Test creating a transcription with empty name and notes"""
- transcription = Transcription(
- id=str(uuid4()),
- file="/path/to/test.mp3",
- status="completed",
- time_queued="2023-01-01T00:00:00",
- task="TRANSCRIBE",
- model_type="WHISPER",
- name="",
- notes=""
- )
-
- assert transcription.name == ""
- assert transcription.notes == ""
-
- def test_transcription_name_assignment(self):
- """Test assigning values to name field"""
- transcription = Transcription(
- id=str(uuid4()),
- file="/path/to/test.mp3",
- status="completed",
- time_queued="2023-01-01T00:00:00",
- task="TRANSCRIBE",
- model_type="WHISPER"
- )
-
- # Test assigning a name
- transcription.name = "New Name"
- assert transcription.name == "New Name"
-
- # Test assigning None
- transcription.name = None
- assert transcription.name is None
-
- # Test assigning empty string
- transcription.name = ""
- assert transcription.name == ""
-
- def test_transcription_notes_assignment(self):
- """Test assigning values to notes field"""
- transcription = Transcription(
- id=str(uuid4()),
- file="/path/to/test.mp3",
- status="completed",
- time_queued="2023-01-01T00:00:00",
- task="TRANSCRIBE",
- model_type="WHISPER"
- )
-
- # Test assigning notes
- transcription.notes = "New notes"
- assert transcription.notes == "New notes"
-
- # Test assigning None
- transcription.notes = None
- assert transcription.notes is None
-
- # Test assigning empty string
- transcription.notes = ""
- assert transcription.notes == ""
-
- def test_transcription_with_unicode_name_and_notes(self):
- """Test creating transcription with unicode characters in name and notes"""
- transcription = Transcription(
- id=str(uuid4()),
- file="/path/to/test.mp3",
- status="completed",
- time_queued="2023-01-01T00:00:00",
- task="TRANSCRIBE",
- model_type="WHISPER",
- name="Transcription avec des caractères spéciaux: ñáéíóú",
- notes="Notes avec des caractères spéciaux: ñáéíóú et émojis 🎵🎤"
- )
-
- assert transcription.name == "Transcription avec des caractères spéciaux: ñáéíóú"
- assert transcription.notes == "Notes avec des caractères spéciaux: ñáéíóú et émojis 🎵🎤"
-
- def test_transcription_with_long_name_and_notes(self):
- """Test creating transcription with very long name and notes"""
- long_name = "A" * 1000 # 1000 character name
- long_notes = "B" * 5000 # 5000 character notes
-
- transcription = Transcription(
- id=str(uuid4()),
- file="/path/to/test.mp3",
- status="completed",
- time_queued="2023-01-01T00:00:00",
- task="TRANSCRIBE",
- model_type="WHISPER",
- name=long_name,
- notes=long_notes
- )
-
- assert transcription.name == long_name
- assert transcription.notes == long_notes
- assert len(transcription.name) == 1000
- assert len(transcription.notes) == 5000
-
- def test_transcription_name_with_special_characters(self):
- """Test transcription name with special characters"""
- special_name = "Transcription with special chars: !@#$%^&*()_+-=[]{}|;':\",./<>?"
-
- transcription = Transcription(
- id=str(uuid4()),
- file="/path/to/test.mp3",
- status="completed",
- time_queued="2023-01-01T00:00:00",
- task="TRANSCRIBE",
- model_type="WHISPER",
- name=special_name
- )
-
- assert transcription.name == special_name
-
- def test_transcription_notes_with_newlines(self):
- """Test transcription notes with newlines and special formatting"""
- notes_with_newlines = """This is a multi-line note
-with newlines and special characters:
-- Bullet point 1
-- Bullet point 2
-- Bullet point 3
-
-And some more text after the empty line."""
-
- transcription = Transcription(
- id=str(uuid4()),
- file="/path/to/test.mp3",
- status="completed",
- time_queued="2023-01-01T00:00:00",
- task="TRANSCRIBE",
- model_type="WHISPER",
- notes=notes_with_newlines
- )
-
- assert transcription.notes == notes_with_newlines
- assert "\n" in transcription.notes
-
- def test_transcription_equality_with_name_and_notes(self):
- """Test transcription equality when name and notes are included"""
- transcription_id = str(uuid4())
-
- transcription1 = Transcription(
- id=transcription_id,
- file="/path/to/test.mp3",
- status="completed",
- time_queued="2023-01-01T00:00:00",
- task="TRANSCRIBE",
- model_type="WHISPER",
- name="Test Name",
- notes="Test Notes"
- )
-
- transcription2 = Transcription(
- id=transcription_id,
- file="/path/to/test.mp3",
- status="completed",
- time_queued="2023-01-01T00:00:00",
- task="TRANSCRIBE",
- model_type="WHISPER",
- name="Test Name",
- notes="Test Notes"
- )
-
- # Two transcriptions with same ID should be equal
- assert transcription1 == transcription2
-
- def test_transcription_inequality_with_different_name_and_notes(self):
- """Test transcription inequality when name and notes are different"""
- transcription_id = str(uuid4())
-
- transcription1 = Transcription(
- id=transcription_id,
- file="/path/to/test.mp3",
- status="completed",
- time_queued="2023-01-01T00:00:00",
- task="TRANSCRIBE",
- model_type="WHISPER",
- name="Test Name 1",
- notes="Test Notes 1"
- )
-
- transcription2 = Transcription(
- id=transcription_id,
- file="/path/to/test.mp3",
- status="completed",
- time_queued="2023-01-01T00:00:00",
- task="TRANSCRIBE",
- model_type="WHISPER",
- name="Test Name 2",
- notes="Test Notes 2"
- )
-
- # Two transcriptions with different name/notes should not be equal
- assert transcription1 != transcription2
-
- def test_transcription_id_as_uuid_property(self):
- """Test that id_as_uuid property works with name and notes fields"""
- transcription_id = uuid4()
-
- transcription = Transcription(
- id=str(transcription_id),
- file="/path/to/test.mp3",
- status="completed",
- time_queued="2023-01-01T00:00:00",
- task="TRANSCRIBE",
- model_type="WHISPER",
- name="Test Name",
- notes="Test Notes"
- )
-
- assert transcription.id_as_uuid == transcription_id
- assert isinstance(transcription.id_as_uuid, type(transcription_id))
-
- def test_transcription_string_representation_with_name_and_notes(self):
- """Test string representation of transcription includes name and notes"""
- transcription = Transcription(
- id="test-id-123",
- file="/path/to/test.mp3",
- status="completed",
- time_queued="2023-01-01T00:00:00",
- task="TRANSCRIBE",
- model_type="WHISPER",
- name="Test Transcription",
- notes="Test notes"
- )
-
- str_repr = str(transcription)
- # The string representation should include the ID
- assert "test-id-123" in str_repr
-
- def test_transcription_with_none_values_in_other_fields(self):
- """Test transcription with None values in other fields but valid name and notes"""
- transcription = Transcription(
- id=str(uuid4()),
- file=None,
- url=None,
- status="completed",
- time_queued="2023-01-01T00:00:00",
- task="TRANSCRIBE",
- model_type="WHISPER",
- name="Valid Name",
- notes="Valid Notes"
- )
-
- assert transcription.name == "Valid Name"
- assert transcription.notes == "Valid Notes"
- assert transcription.file is None
- assert transcription.url is None
diff --git a/tests/db/service/transcription_service_test.py b/tests/db/service/transcription_service_test.py
deleted file mode 100644
index 2d267c52..00000000
--- a/tests/db/service/transcription_service_test.py
+++ /dev/null
@@ -1,211 +0,0 @@
-import pytest
-from unittest.mock import Mock, patch
-from uuid import UUID, uuid4
-
-from buzz.db.service.transcription_service import TranscriptionService
-from buzz.db.entity.transcription import Transcription
-
-
-@pytest.fixture
-def mock_transcription_dao():
- """Create a mock TranscriptionDAO for testing"""
- return Mock()
-
-
-@pytest.fixture
-def mock_transcription_segment_dao():
- """Create a mock TranscriptionSegmentDAO for testing"""
- return Mock()
-
-
-@pytest.fixture
-def transcription_service(mock_transcription_dao, mock_transcription_segment_dao):
- """Create a TranscriptionService instance for testing"""
- return TranscriptionService(mock_transcription_dao, mock_transcription_segment_dao)
-
-
-@pytest.fixture
-def sample_transcription():
- """Create a sample transcription for testing"""
- return Transcription(
- id=str(uuid4()),
- file="/path/to/test.mp3",
- status="completed",
- time_queued="2023-01-01T00:00:00",
- task="TRANSCRIBE",
- model_type="WHISPER",
- name="Test Transcription",
- notes="This is a test transcription"
- )
-
-
-class TestTranscriptionService:
- def test_update_transcription_name(self, transcription_service, mock_transcription_dao):
- """Test updating transcription name through service"""
- transcription_id = uuid4()
- new_name = "Updated Transcription Name"
-
- # Call the service method
- transcription_service.update_transcription_name(transcription_id, new_name)
-
- # Verify the DAO method was called with correct parameters
- mock_transcription_dao.update_transcription_name.assert_called_once_with(transcription_id, new_name)
-
- def test_update_transcription_notes(self, transcription_service, mock_transcription_dao):
- """Test updating transcription notes through service"""
- transcription_id = uuid4()
- new_notes = "Updated transcription notes with more details"
-
- # Call the service method
- transcription_service.update_transcription_notes(transcription_id, new_notes)
-
- # Verify the DAO method was called with correct parameters
- mock_transcription_dao.update_transcription_notes.assert_called_once_with(transcription_id, new_notes)
-
- def test_update_transcription_name_with_empty_string(self, transcription_service, mock_transcription_dao):
- """Test updating transcription name to empty string"""
- transcription_id = uuid4()
- empty_name = ""
-
- # Call the service method
- transcription_service.update_transcription_name(transcription_id, empty_name)
-
- # Verify the DAO method was called with empty string
- mock_transcription_dao.update_transcription_name.assert_called_once_with(transcription_id, empty_name)
-
- def test_update_transcription_notes_with_empty_string(self, transcription_service, mock_transcription_dao):
- """Test updating transcription notes to empty string"""
- transcription_id = uuid4()
- empty_notes = ""
-
- # Call the service method
- transcription_service.update_transcription_notes(transcription_id, empty_notes)
-
- # Verify the DAO method was called with empty string
- mock_transcription_dao.update_transcription_notes.assert_called_once_with(transcription_id, empty_notes)
-
- def test_update_transcription_name_with_none(self, transcription_service, mock_transcription_dao):
- """Test updating transcription name to None"""
- transcription_id = uuid4()
-
- # Call the service method
- transcription_service.update_transcription_name(transcription_id, None)
-
- # Verify the DAO method was called with None
- mock_transcription_dao.update_transcription_name.assert_called_once_with(transcription_id, None)
-
- def test_update_transcription_notes_with_none(self, transcription_service, mock_transcription_dao):
- """Test updating transcription notes to None"""
- transcription_id = uuid4()
-
- # Call the service method
- transcription_service.update_transcription_notes(transcription_id, None)
-
- # Verify the DAO method was called with None
- mock_transcription_dao.update_transcription_notes.assert_called_once_with(transcription_id, None)
-
- def test_update_transcription_name_propagates_dao_exception(self, transcription_service, mock_transcription_dao):
- """Test that DAO exceptions are propagated from service"""
- transcription_id = uuid4()
- new_name = "Updated Name"
-
- # Configure the mock to raise an exception
- mock_transcription_dao.update_transcription_name.side_effect = Exception("Database error")
-
- # Call the service method and expect the exception to be raised
- with pytest.raises(Exception, match="Database error"):
- transcription_service.update_transcription_name(transcription_id, new_name)
-
- def test_update_transcription_notes_propagates_dao_exception(self, transcription_service, mock_transcription_dao):
- """Test that DAO exceptions are propagated from service"""
- transcription_id = uuid4()
- new_notes = "Updated notes"
-
- # Configure the mock to raise an exception
- mock_transcription_dao.update_transcription_notes.side_effect = Exception("Database error")
-
- # Call the service method and expect the exception to be raised
- with pytest.raises(Exception, match="Database error"):
- transcription_service.update_transcription_notes(transcription_id, new_notes)
-
- def test_update_transcription_name_with_string_uuid(self, transcription_service, mock_transcription_dao):
- """Test updating transcription name with string UUID (should be converted to UUID)"""
- transcription_id_str = str(uuid4())
- new_name = "Updated Name"
-
- # Call the service method
- transcription_service.update_transcription_name(transcription_id_str, new_name)
-
- # Verify the DAO method was called with UUID object
- mock_transcription_dao.update_transcription_name.assert_called_once()
- call_args = mock_transcription_dao.update_transcription_name.call_args[0]
- assert isinstance(call_args[0], str) # The service should pass the string as-is
- assert call_args[1] == new_name
-
- def test_update_transcription_notes_with_string_uuid(self, transcription_service, mock_transcription_dao):
- """Test updating transcription notes with string UUID (should be converted to UUID)"""
- transcription_id_str = str(uuid4())
- new_notes = "Updated notes"
-
- # Call the service method
- transcription_service.update_transcription_notes(transcription_id_str, new_notes)
-
- # Verify the DAO method was called with UUID object
- mock_transcription_dao.update_transcription_notes.assert_called_once()
- call_args = mock_transcription_dao.update_transcription_notes.call_args[0]
- assert isinstance(call_args[0], str) # The service should pass the string as-is
- assert call_args[1] == new_notes
-
- def test_update_transcription_name_multiple_calls(self, transcription_service, mock_transcription_dao):
- """Test multiple calls to update transcription name"""
- transcription_id = uuid4()
-
- # Make multiple calls
- transcription_service.update_transcription_name(transcription_id, "Name 1")
- transcription_service.update_transcription_name(transcription_id, "Name 2")
- transcription_service.update_transcription_name(transcription_id, "Name 3")
-
- # Verify all calls were made
- assert mock_transcription_dao.update_transcription_name.call_count == 3
-
- # Verify the last call has the correct parameters
- last_call = mock_transcription_dao.update_transcription_name.call_args_list[-1]
- assert last_call[0] == (transcription_id, "Name 3")
-
- def test_update_transcription_notes_multiple_calls(self, transcription_service, mock_transcription_dao):
- """Test multiple calls to update transcription notes"""
- transcription_id = uuid4()
-
- # Make multiple calls
- transcription_service.update_transcription_notes(transcription_id, "Notes 1")
- transcription_service.update_transcription_notes(transcription_id, "Notes 2")
- transcription_service.update_transcription_notes(transcription_id, "Notes 3")
-
- # Verify all calls were made
- assert mock_transcription_dao.update_transcription_notes.call_count == 3
-
- # Verify the last call has the correct parameters
- last_call = mock_transcription_dao.update_transcription_notes.call_args_list[-1]
- assert last_call[0] == (transcription_id, "Notes 3")
-
- def test_update_transcription_name_with_unicode(self, transcription_service, mock_transcription_dao):
- """Test updating transcription name with unicode characters"""
- transcription_id = uuid4()
- unicode_name = "Transcription avec des caractères spéciaux: ñáéíóú"
-
- # Call the service method
- transcription_service.update_transcription_name(transcription_id, unicode_name)
-
- # Verify the DAO method was called with unicode string
- mock_transcription_dao.update_transcription_name.assert_called_once_with(transcription_id, unicode_name)
-
- def test_update_transcription_notes_with_unicode(self, transcription_service, mock_transcription_dao):
- """Test updating transcription notes with unicode characters"""
- transcription_id = uuid4()
- unicode_notes = "Notes avec des caractères spéciaux: ñáéíóú et émojis 🎵🎤"
-
- # Call the service method
- transcription_service.update_transcription_notes(transcription_id, unicode_notes)
-
- # Verify the DAO method was called with unicode string
- mock_transcription_dao.update_transcription_notes.assert_called_once_with(transcription_id, unicode_notes)
diff --git a/tests/gui_test.py b/tests/gui_test.py
deleted file mode 100644
index 295ac769..00000000
--- a/tests/gui_test.py
+++ /dev/null
@@ -1,229 +0,0 @@
-import multiprocessing
-import os
-import platform
-from unittest.mock import Mock, patch
-
-import pytest
-import sounddevice
-from PyQt6.QtCore import Qt
-from PyQt6.QtGui import QKeyEvent
-from PyQt6.QtWidgets import (
- QApplication,
- QMessageBox,
-)
-from pytestqt.qtbot import QtBot
-
-from buzz.locale import _
-from buzz.__version__ import VERSION
-from buzz.widgets.audio_devices_combo_box import AudioDevicesComboBox
-from buzz.widgets.transcriber.advanced_settings_dialog import AdvancedSettingsDialog
-from buzz.widgets.transcriber.hugging_face_search_line_edit import (
- HuggingFaceSearchLineEdit,
-)
-from buzz.widgets.transcriber.languages_combo_box import LanguagesComboBox
-from buzz.widgets.about_dialog import AboutDialog
-from buzz.settings.settings import Settings
-from buzz.transcriber.transcriber import (
- TranscriptionOptions,
-)
-from buzz.widgets.transcriber.transcription_options_group_box import (
- TranscriptionOptionsGroupBox,
-)
-from tests.mock_sounddevice import MockInputStream, mock_query_devices
-from .mock_qt import MockNetworkAccessManager, MockNetworkReply
-
-if platform.system() == "Linux":
- try:
- multiprocessing.set_start_method("spawn", force=True)
- except RuntimeError:
- pass
-
-
-@pytest.fixture(scope="module", autouse=True)
-def audio_setup():
- with patch("sounddevice.query_devices") as query_devices_mock, patch(
- "sounddevice.InputStream", side_effect=MockInputStream
- ), patch("sounddevice.check_input_settings"):
- query_devices_mock.return_value = mock_query_devices
- sounddevice.default.device = 3, 4
- yield
-
-
-class TestLanguagesComboBox:
- def test_should_show_sorted_whisper_languages(self, qtbot):
- languages_combox_box = LanguagesComboBox("en")
- qtbot.add_widget(languages_combox_box)
- assert languages_combox_box.itemText(0) == _("Detect Language")
- assert languages_combox_box.itemText(1) == _("Afrikaans")
-
- def test_should_select_en_as_default_language(self, qtbot):
- languages_combox_box = LanguagesComboBox("en")
- qtbot.add_widget(languages_combox_box)
- assert languages_combox_box.currentText() == _("English")
-
- def test_should_select_detect_language_as_default(self, qtbot):
- languages_combo_box = LanguagesComboBox(None)
- qtbot.add_widget(languages_combo_box)
- assert languages_combo_box.currentText() == _("Detect Language")
-
-
-class TestAudioDevicesComboBox:
- def test_get_devices(self):
- audio_devices_combo_box = AudioDevicesComboBox()
-
- assert audio_devices_combo_box.itemText(0) == "Background Music"
- assert audio_devices_combo_box.itemText(1) == "Background Music (UI Sounds)"
- assert audio_devices_combo_box.itemText(2) == "BlackHole 2ch"
- assert audio_devices_combo_box.itemText(3) == "MacBook Pro Microphone"
- assert audio_devices_combo_box.itemText(4) == "Null Audio Device"
-
- assert audio_devices_combo_box.currentText() == "MacBook Pro Microphone"
-
- def test_select_default_mic_when_no_default(self):
- sounddevice.default.device = -1, 1
-
- audio_devices_combo_box = AudioDevicesComboBox()
- assert audio_devices_combo_box.currentText() == "Background Music"
-
-
-@pytest.fixture(scope="module", autouse=True)
-def clear_settings():
- settings = Settings()
- settings.clear()
-
-
-class TestAboutDialog:
- def test_should_check_for_updates(self, qtbot: QtBot):
- reply = MockNetworkReply(data={"name": "v" + VERSION})
- manager = MockNetworkAccessManager(reply=reply)
- dialog = AboutDialog(network_access_manager=manager)
- qtbot.add_widget(dialog)
-
- mock_message_box_information = Mock()
- QMessageBox.information = mock_message_box_information
-
- with qtbot.wait_signal(dialog.network_access_manager.finished):
- dialog.check_updates_button.click()
-
- mock_message_box_information.assert_called_with(
- dialog, "", _("You're up to date!")
- )
-
-
-class TestAdvancedSettingsDialog:
- def test_should_update_advanced_settings(self, qtbot: QtBot):
- dialog = AdvancedSettingsDialog(
- transcription_options=TranscriptionOptions(
- initial_prompt="prompt",
- enable_llm_translation=False,
- llm_model="",
- llm_prompt=""
- )
- )
- qtbot.add_widget(dialog)
-
- transcription_options_mock = Mock()
- dialog.transcription_options_changed.connect(transcription_options_mock)
-
- assert dialog.windowTitle() == _("Advanced Settings")
- assert dialog.initial_prompt_text_edit.toPlainText() == "prompt"
- assert dialog.enable_llm_translation_checkbox.isChecked() is False
- assert dialog.llm_model_line_edit.text() == "gpt-4.1-mini"
- assert dialog.llm_prompt_text_edit.toPlainText() == _("Please translate each text sent to you from English to Spanish. Translation will be used in an automated system, please do not add any comments or notes, just the translation.")
-
- dialog.initial_prompt_text_edit.setPlainText("new prompt")
- dialog.enable_llm_translation_checkbox.setChecked(True)
- dialog.llm_model_line_edit.setText("model")
- dialog.llm_prompt_text_edit.setPlainText("Please translate this text")
-
- assert transcription_options_mock.call_args[0][0].initial_prompt == "new prompt"
- assert transcription_options_mock.call_args[0][0].enable_llm_translation is True
- assert transcription_options_mock.call_args[0][0].llm_model == "model"
- assert transcription_options_mock.call_args[0][0].llm_prompt == "Please translate this text"
-
-
-@pytest.mark.skipif(
- platform.system() == "Linux" and os.environ.get("XDG_SESSION_TYPE") == "wayland",
- reason="Skipping on Wayland sessions due to Qt popup issues"
-)
-class TestHuggingFaceSearchLineEdit:
- def test_should_update_selected_model_on_type(self, qtbot: QtBot):
- widget = HuggingFaceSearchLineEdit(
- default_value="",
- network_access_manager=self.network_access_manager()
- )
- qtbot.add_widget(widget)
-
- mock_model_selected = Mock()
- widget.model_selected.connect(mock_model_selected)
-
- self._set_text_and_wait_response(qtbot, widget)
- mock_model_selected.assert_called_with("openai/whisper-tiny")
-
- def test_should_show_list_of_models(self, qtbot: QtBot):
- widget = HuggingFaceSearchLineEdit(
- default_value="",
- network_access_manager=self.network_access_manager()
- )
- qtbot.add_widget(widget)
-
- self._set_text_and_wait_response(qtbot, widget)
-
- assert widget.popup.count() > 0
- assert "openai/whisper-tiny" in widget.popup.item(0).text()
-
- def test_should_select_model_from_list(self, qtbot: QtBot):
- widget = HuggingFaceSearchLineEdit(
- default_value="",
- network_access_manager=self.network_access_manager()
- )
- qtbot.add_widget(widget)
-
- mock_model_selected = Mock()
- widget.model_selected.connect(mock_model_selected)
-
- self._set_text_and_wait_response(qtbot, widget)
-
- # press down arrow and enter to select next item
- QApplication.sendEvent(
- widget.popup,
- QKeyEvent(
- QKeyEvent.Type.KeyPress, Qt.Key.Key_Down, Qt.KeyboardModifier.NoModifier
- ),
- )
- QApplication.sendEvent(
- widget.popup,
- QKeyEvent(
- QKeyEvent.Type.KeyPress,
- Qt.Key.Key_Enter,
- Qt.KeyboardModifier.NoModifier,
- ),
- )
-
- mock_model_selected.assert_called_with("openai/whisper-tiny.en")
-
- @staticmethod
- def network_access_manager():
- reply = MockNetworkReply(
- data=[{"id": "openai/whisper-tiny"}, {"id": "openai/whisper-tiny.en"}]
- )
- return MockNetworkAccessManager(reply=reply)
-
- @staticmethod
- def _set_text_and_wait_response(qtbot: QtBot, widget: HuggingFaceSearchLineEdit):
- with qtbot.wait_signal(widget.network_manager.finished):
- widget.setText("openai/whisper-tiny")
- widget.textEdited.emit("openai/whisper-tiny")
-
-
-class TestTranscriptionOptionsGroupBox:
- def test_should_update_model_type(self, qtbot):
- widget = TranscriptionOptionsGroupBox()
- qtbot.add_widget(widget)
-
- mock_transcription_options_changed = Mock()
- widget.transcription_options_changed.connect(mock_transcription_options_changed)
-
- widget.model_type_combo_box.setCurrentIndex(1)
-
- mock_transcription_options_changed.assert_called()
diff --git a/tests/mock_qt.py b/tests/mock_qt.py
deleted file mode 100644
index 2e2dfc28..00000000
--- a/tests/mock_qt.py
+++ /dev/null
@@ -1,92 +0,0 @@
-import json
-from typing import Optional
-
-from PyQt6.QtCore import QByteArray, QObject, pyqtSignal
-from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest
-
-
-class MockNetworkReply(QNetworkReply):
- def __init__(self, data: object, _: Optional[QObject] = None) -> None:
- self.data = data
-
- def readAll(self) -> "QByteArray":
- return QByteArray(json.dumps(self.data).encode("utf-8"))
-
- def error(self) -> "QNetworkReply.NetworkError":
- return QNetworkReply.NetworkError.NoError
-
- def deleteLater(self) -> None:
- pass
-
-
-class MockNetworkAccessManager(QNetworkAccessManager):
- finished = pyqtSignal(object)
- reply: MockNetworkReply
-
- def __init__(
- self, reply: MockNetworkReply, parent: Optional[QObject] = None
- ) -> None:
- super().__init__(parent)
- self.reply = reply
-
- def get(self, _: "QNetworkRequest") -> "QNetworkReply":
- self.finished.emit(self.reply)
- return self.reply
-
-
-class MockDownloadReply(QObject):
- """Mock reply for file downloads — supports downloadProgress and finished signals."""
- downloadProgress = pyqtSignal(int, int)
- finished = pyqtSignal()
-
- def __init__(
- self,
- data: bytes = b"fake-installer-data",
- network_error: "QNetworkReply.NetworkError" = QNetworkReply.NetworkError.NoError,
- error_string: str = "",
- parent: Optional[QObject] = None,
- ) -> None:
- super().__init__(parent)
- self._data = data
- self._network_error = network_error
- self._error_string = error_string
- self._aborted = False
-
- def readAll(self) -> QByteArray:
- return QByteArray(self._data)
-
- def error(self) -> "QNetworkReply.NetworkError":
- return self._network_error
-
- def errorString(self) -> str:
- return self._error_string
-
- def abort(self) -> None:
- self._aborted = True
-
- def deleteLater(self) -> None:
- pass
-
- def emit_finished(self) -> None:
- self.finished.emit()
-
-
-class MockDownloadNetworkManager(QNetworkAccessManager):
- """Network manager that returns MockDownloadReply instances for each get() call."""
-
- def __init__(
- self,
- replies: Optional[list] = None,
- parent: Optional[QObject] = None,
- ) -> None:
- super().__init__(parent)
- self._replies = list(replies) if replies else []
- self._index = 0
-
- def get(self, _: "QNetworkRequest") -> "MockDownloadReply":
- if self._index < len(self._replies):
- reply = self._replies[self._index]
- else:
- reply = MockDownloadReply()
- self._index += 1
- return reply
diff --git a/tests/mock_sounddevice.py b/tests/mock_sounddevice.py
deleted file mode 100644
index fecdda15..00000000
--- a/tests/mock_sounddevice.py
+++ /dev/null
@@ -1,181 +0,0 @@
-import os
-from threading import Thread, Event
-from typing import Callable, Any
-
-import numpy as np
-
-from buzz import whisper_audio
-
-mock_query_devices = [
- {
- "name": "Background Music",
- "index": 0,
- "hostapi": 0,
- "max_input_channels": 2,
- "max_output_channels": 2,
- "default_low_input_latency": 0.01,
- "default_low_output_latency": 0.008,
- "default_high_input_latency": 0.1,
- "default_high_output_latency": 0.064,
- "default_samplerate": 8000.0,
- },
- {
- "name": "Background Music (UI Sounds)",
- "index": 1,
- "hostapi": 0,
- "max_input_channels": 2,
- "max_output_channels": 2,
- "default_low_input_latency": 0.01,
- "default_low_output_latency": 0.008,
- "default_high_input_latency": 0.1,
- "default_high_output_latency": 0.064,
- "default_samplerate": 8000.0,
- },
- {
- "name": "BlackHole 2ch",
- "index": 2,
- "hostapi": 0,
- "max_input_channels": 2,
- "max_output_channels": 2,
- "default_low_input_latency": 0.01,
- "default_low_output_latency": 0.0013333333333333333,
- "default_high_input_latency": 0.1,
- "default_high_output_latency": 0.010666666666666666,
- "default_samplerate": 48000.0,
- },
- {
- "name": "MacBook Pro Microphone",
- "index": 3,
- "hostapi": 0,
- "max_input_channels": 1,
- "max_output_channels": 0,
- "default_low_input_latency": 0.034520833333333334,
- "default_low_output_latency": 0.01,
- "default_high_input_latency": 0.043854166666666666,
- "default_high_output_latency": 0.1,
- "default_samplerate": 48000.0,
- },
- {
- "name": "MacBook Pro Speakers",
- "index": 4,
- "hostapi": 0,
- "max_input_channels": 0,
- "max_output_channels": 2,
- "default_low_input_latency": 0.01,
- "default_low_output_latency": 0.0070416666666666666,
- "default_high_input_latency": 0.1,
- "default_high_output_latency": 0.016375,
- "default_samplerate": 48000.0,
- },
- {
- "name": "Null Audio Device",
- "index": 5,
- "hostapi": 0,
- "max_input_channels": 2,
- "max_output_channels": 2,
- "default_low_input_latency": 0.01,
- "default_low_output_latency": 0.0014512471655328798,
- "default_high_input_latency": 0.1,
- "default_high_output_latency": 0.011609977324263039,
- "default_samplerate": 44100.0,
- },
- {
- "name": "Multi-Output Device",
- "index": 6,
- "hostapi": 0,
- "max_input_channels": 0,
- "max_output_channels": 2,
- "default_low_input_latency": 0.01,
- "default_low_output_latency": 0.0033333333333333335,
- "default_high_input_latency": 0.1,
- "default_high_output_latency": 0.012666666666666666,
- "default_samplerate": 48000.0,
- },
-]
-
-
-class MockInputStream:
- thread: Thread
- samplerate = whisper_audio.SAMPLE_RATE
-
- def __init__(
- self,
- callback: Callable[[np.ndarray, int, Any, Any], None],
- *args,
- **kwargs,
- ):
- self._stop_event = Event()
- self.callback = callback
-
- # Pre-load audio on the calling (main) thread to avoid calling
- # subprocess.run (fork) from a background thread on macOS, which
- # can cause a segfault when Qt is running.
- sample_rate = whisper_audio.SAMPLE_RATE
- file_path = os.path.join(
- os.path.dirname(__file__), "../testdata/whisper-french.mp3"
- )
- self._audio = whisper_audio.load_audio(file_path, sr=sample_rate)
-
- self.thread = Thread(target=self.target)
-
- def start(self):
- self.thread.start()
-
- def target(self):
- sample_rate = whisper_audio.SAMPLE_RATE
- audio = self._audio
-
- chunk_duration_secs = 1
-
- seek = 0
- num_samples_in_chunk = chunk_duration_secs * sample_rate
-
- while not self._stop_event.is_set():
- self._stop_event.wait(timeout=chunk_duration_secs)
- if self._stop_event.is_set():
- break
- chunk = audio[seek : seek + num_samples_in_chunk]
- try:
- self.callback(chunk, 0, None, None)
- except RuntimeError:
- # Qt object was deleted between the stop-event check and
- # the callback invocation; treat it as a stop signal.
- break
- seek += num_samples_in_chunk
-
- # loop back around
- if seek + num_samples_in_chunk > audio.size:
- seek = 0
-
- def stop(self):
- self._stop_event.set()
- if self.thread.is_alive():
- self.thread.join(timeout=5)
-
- def close(self):
- self.stop()
-
- def __enter__(self):
- self.start()
-
- def __exit__(self, exc_type, exc_val, exc_tb):
- self.stop()
-
-
-class MockSoundDevice:
- def __init__(self):
- self.devices = mock_query_devices
-
- def InputStream(self, *args, **kwargs):
- return MockInputStream(*args, **kwargs)
-
- def query_devices(self, device=None):
- if device is None:
- return self.devices
- else:
- return next((d for d in self.devices if d['index'] == device), None)
-
- def check_input_settings(self, device=None, samplerate=None):
- device_info = self.query_devices(device)
- if device_info and samplerate and samplerate != device_info['default_samplerate']:
- raise ValueError('Invalid sample rate for device')
\ No newline at end of file
diff --git a/tests/model_loader.py b/tests/model_loader.py
deleted file mode 100644
index 7a6599a1..00000000
--- a/tests/model_loader.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from buzz.model_loader import TranscriptionModel, ModelDownloader
-
-
-def get_model_path(transcription_model: TranscriptionModel) -> str:
- path = transcription_model.get_local_model_path()
- if path is not None:
- return path
-
- model_loader = ModelDownloader(model=transcription_model)
- model_path = ""
-
- def on_load_model(path: str):
- nonlocal model_path
- model_path = path
-
- model_loader.signals.finished.connect(on_load_model)
- model_loader.run()
- return model_path
diff --git a/tests/model_loader_test.py b/tests/model_loader_test.py
deleted file mode 100644
index 9275768d..00000000
--- a/tests/model_loader_test.py
+++ /dev/null
@@ -1,769 +0,0 @@
-import io
-import os
-import threading
-import time
-import pytest
-from unittest.mock import patch, MagicMock, call
-
-from buzz.model_loader import (
- ModelDownloader,
- HuggingfaceDownloadMonitor,
- TranscriptionModel,
- ModelType,
- WhisperModelSize,
- map_language_to_mms,
- is_mms_model,
- get_expected_whisper_model_size,
- get_whisper_file_path,
- WHISPER_MODEL_SIZES,
- WHISPER_CPP_REPO_ID,
- WHISPER_CPP_LUMII_REPO_ID,
-)
-
-
-class TestModelLoader:
- @pytest.mark.parametrize(
- "model",
- [
- TranscriptionModel(
- model_type=ModelType.HUGGING_FACE,
- hugging_face_model_id="RaivisDejus/whisper-tiny-lv",
- ),
- ],
- )
- def test_download_model(self, model: TranscriptionModel):
- model_loader = ModelDownloader(model=model)
- model_loader.run()
-
- model_path = model.get_local_model_path()
-
- assert model_path is not None, "Model path is None"
- assert os.path.isdir(model_path), "Model path is not a directory"
- assert len(os.listdir(model_path)) > 0, "Model directory is empty"
-
-
-class TestMapLanguageToMms:
- def test_empty_returns_english(self):
- assert map_language_to_mms("") == "eng"
-
- def test_two_letter_known_code(self):
- assert map_language_to_mms("en") == "eng"
- assert map_language_to_mms("fr") == "fra"
- assert map_language_to_mms("lv") == "lav"
-
- def test_three_letter_code_returned_as_is(self):
- assert map_language_to_mms("eng") == "eng"
- assert map_language_to_mms("fra") == "fra"
-
- def test_unknown_two_letter_code_returned_as_is(self):
- assert map_language_to_mms("xx") == "xx"
-
- @pytest.mark.parametrize(
- "code,expected",
- [
- ("de", "deu"),
- ("es", "spa"),
- ("ja", "jpn"),
- ("zh", "cmn"),
- ("ar", "ara"),
- ],
- )
- def test_various_language_codes(self, code, expected):
- assert map_language_to_mms(code) == expected
-
-
-class TestIsMmsModel:
- def test_empty_string(self):
- assert is_mms_model("") is False
-
- def test_mms_in_model_id(self):
- assert is_mms_model("facebook/mms-1b-all") is True
-
- def test_mms_case_insensitive(self):
- assert is_mms_model("facebook/MMS-1b-all") is True
-
- def test_non_mms_model(self):
- assert is_mms_model("openai/whisper-tiny") is False
-
-
-class TestWhisperModelSize:
- def test_to_faster_whisper_model_size_large(self):
- assert WhisperModelSize.LARGE.to_faster_whisper_model_size() == "large-v1"
-
- def test_to_faster_whisper_model_size_tiny(self):
- assert WhisperModelSize.TINY.to_faster_whisper_model_size() == "tiny"
-
- def test_to_faster_whisper_model_size_largev3(self):
- assert WhisperModelSize.LARGEV3.to_faster_whisper_model_size() == "large-v3"
-
- def test_to_whisper_cpp_model_size_large(self):
- assert WhisperModelSize.LARGE.to_whisper_cpp_model_size() == "large-v1"
-
- def test_to_whisper_cpp_model_size_tiny(self):
- assert WhisperModelSize.TINY.to_whisper_cpp_model_size() == "tiny"
-
- def test_str(self):
- assert str(WhisperModelSize.TINY) == "Tiny"
- assert str(WhisperModelSize.LARGE) == "Large"
- assert str(WhisperModelSize.LARGEV3TURBO) == "Large-v3-turbo"
- assert str(WhisperModelSize.CUSTOM) == "Custom"
-
-
-class TestModelType:
- def test_supports_initial_prompt(self):
- assert ModelType.WHISPER.supports_initial_prompt is True
- assert ModelType.WHISPER_CPP.supports_initial_prompt is True
- assert ModelType.OPEN_AI_WHISPER_API.supports_initial_prompt is True
- assert ModelType.FASTER_WHISPER.supports_initial_prompt is True
- assert ModelType.HUGGING_FACE.supports_initial_prompt is False
-
- @pytest.mark.parametrize(
- "platform_system,platform_machine,expected_faster_whisper",
- [
- ("Linux", "x86_64", True),
- ("Windows", "AMD64", True),
- ("Darwin", "arm64", True),
- ("Darwin", "x86_64", False), # Faster Whisper not available on macOS x86_64
- ],
- )
- def test_is_available(self, platform_system, platform_machine, expected_faster_whisper):
- with patch("platform.system", return_value=platform_system), \
- patch("platform.machine", return_value=platform_machine):
- # These should always be available
- assert ModelType.WHISPER.is_available() is True
- assert ModelType.HUGGING_FACE.is_available() is True
- assert ModelType.OPEN_AI_WHISPER_API.is_available() is True
- assert ModelType.WHISPER_CPP.is_available() is True
-
- # Faster Whisper depends on platform
- assert ModelType.FASTER_WHISPER.is_available() == expected_faster_whisper
-
- def test_is_manually_downloadable(self):
- assert ModelType.WHISPER.is_manually_downloadable() is True
- assert ModelType.WHISPER_CPP.is_manually_downloadable() is True
- assert ModelType.FASTER_WHISPER.is_manually_downloadable() is True
- assert ModelType.HUGGING_FACE.is_manually_downloadable() is False
- assert ModelType.OPEN_AI_WHISPER_API.is_manually_downloadable() is False
-
-
-class TestTranscriptionModel:
- def test_str_whisper(self):
- model = TranscriptionModel(
- model_type=ModelType.WHISPER, whisper_model_size=WhisperModelSize.TINY
- )
- assert str(model) == "Whisper (Tiny)"
-
- def test_str_whisper_cpp(self):
- model = TranscriptionModel(
- model_type=ModelType.WHISPER_CPP, whisper_model_size=WhisperModelSize.BASE
- )
- assert str(model) == "Whisper.cpp (Base)"
-
- def test_str_hugging_face(self):
- model = TranscriptionModel(
- model_type=ModelType.HUGGING_FACE,
- hugging_face_model_id="openai/whisper-tiny",
- )
- assert str(model) == "Hugging Face (openai/whisper-tiny)"
-
- def test_str_faster_whisper(self):
- model = TranscriptionModel(
- model_type=ModelType.FASTER_WHISPER,
- whisper_model_size=WhisperModelSize.SMALL,
- )
- assert str(model) == "Faster Whisper (Small)"
-
- def test_str_openai_api(self):
- model = TranscriptionModel(model_type=ModelType.OPEN_AI_WHISPER_API)
- assert str(model) == "OpenAI Whisper API"
-
- def test_default(self):
- model = TranscriptionModel.default()
- assert model.model_type in list(ModelType)
- assert model.model_type.is_available() is True
-
- def test_get_local_model_path_openai_api(self):
- model = TranscriptionModel(model_type=ModelType.OPEN_AI_WHISPER_API)
- assert model.get_local_model_path() == ""
-
-
-class TestGetExpectedWhisperModelSize:
- def test_known_sizes(self):
- assert get_expected_whisper_model_size(WhisperModelSize.TINY) == 72 * 1024 * 1024
- assert get_expected_whisper_model_size(WhisperModelSize.LARGE) == 2870 * 1024 * 1024
-
- def test_unknown_size_returns_none(self):
- assert get_expected_whisper_model_size(WhisperModelSize.CUSTOM) is None
- assert get_expected_whisper_model_size(WhisperModelSize.LUMII) is None
-
- def test_all_defined_sizes_have_values(self):
- for size in WHISPER_MODEL_SIZES:
- assert WHISPER_MODEL_SIZES[size] > 0
-
-
-class TestGetWhisperFilePath:
- def test_custom_size(self):
- path = get_whisper_file_path(WhisperModelSize.CUSTOM)
- assert path.endswith("custom")
- assert "whisper" in path
-
- def test_tiny_size(self):
- path = get_whisper_file_path(WhisperModelSize.TINY)
- assert "whisper" in path
- assert path.endswith(".pt")
-
-
-class TestTranscriptionModelIsDeletable:
- def test_whisper_model_not_downloaded(self):
- model = TranscriptionModel(model_type=ModelType.WHISPER, whisper_model_size=WhisperModelSize.TINY)
- with patch.object(model, 'get_local_model_path', return_value=None):
- assert model.is_deletable() is False
-
- def test_whisper_model_downloaded(self):
- model = TranscriptionModel(model_type=ModelType.WHISPER, whisper_model_size=WhisperModelSize.TINY)
- with patch.object(model, 'get_local_model_path', return_value="/some/path/model.pt"):
- assert model.is_deletable() is True
-
- def test_openai_api_not_deletable(self):
- model = TranscriptionModel(model_type=ModelType.OPEN_AI_WHISPER_API)
- assert model.is_deletable() is False
-
- def test_hugging_face_not_deletable(self):
- model = TranscriptionModel(
- model_type=ModelType.HUGGING_FACE,
- hugging_face_model_id="openai/whisper-tiny"
- )
- assert model.is_deletable() is False
-
-
-class TestTranscriptionModelGetLocalModelPath:
- def test_whisper_cpp_file_not_exists(self):
- model = TranscriptionModel(model_type=ModelType.WHISPER_CPP, whisper_model_size=WhisperModelSize.TINY)
- with patch('os.path.exists', return_value=False), \
- patch('os.path.isfile', return_value=False):
- assert model.get_local_model_path() is None
-
- def test_whisper_file_not_exists(self):
- model = TranscriptionModel(model_type=ModelType.WHISPER, whisper_model_size=WhisperModelSize.TINY)
- with patch('os.path.exists', return_value=False):
- assert model.get_local_model_path() is None
-
- def test_whisper_file_too_small(self):
- model = TranscriptionModel(model_type=ModelType.WHISPER, whisper_model_size=WhisperModelSize.TINY)
- with patch('os.path.exists', return_value=True), \
- patch('os.path.isfile', return_value=True), \
- patch('os.path.getsize', return_value=1024): # 1KB, much smaller than expected
- assert model.get_local_model_path() is None
-
- def test_whisper_file_valid(self):
- model = TranscriptionModel(model_type=ModelType.WHISPER, whisper_model_size=WhisperModelSize.TINY)
- expected_size = 72 * 1024 * 1024 # 72MB
- with patch('os.path.exists', return_value=True), \
- patch('os.path.isfile', return_value=True), \
- patch('os.path.getsize', return_value=expected_size):
- result = model.get_local_model_path()
- assert result is not None
-
- def test_faster_whisper_not_found(self):
- model = TranscriptionModel(model_type=ModelType.FASTER_WHISPER, whisper_model_size=WhisperModelSize.TINY)
- with patch('buzz.model_loader.download_faster_whisper_model', side_effect=FileNotFoundError):
- assert model.get_local_model_path() is None
-
- def test_hugging_face_not_found(self):
- model = TranscriptionModel(
- model_type=ModelType.HUGGING_FACE,
- hugging_face_model_id="some/model"
- )
- import huggingface_hub
- with patch.object(huggingface_hub, 'snapshot_download', side_effect=FileNotFoundError):
- assert model.get_local_model_path() is None
-
-
-class TestTranscriptionModelOpenPath:
- def test_open_path_linux(self):
- with patch('sys.platform', 'linux'), \
- patch('subprocess.call') as mock_call:
- TranscriptionModel.open_path("/some/path")
- mock_call.assert_called_once_with(['xdg-open', '/some/path'])
-
- def test_open_path_darwin(self):
- with patch('sys.platform', 'darwin'), \
- patch('subprocess.call') as mock_call:
- TranscriptionModel.open_path("/some/path")
- mock_call.assert_called_once_with(['open', '/some/path'])
-
-
-class TestTranscriptionModelOpenFileLocation:
- def test_whisper_opens_parent_directory(self):
- model = TranscriptionModel(model_type=ModelType.WHISPER, whisper_model_size=WhisperModelSize.TINY)
- with patch.object(model, 'get_local_model_path', return_value="/some/path/model.pt"), \
- patch.object(TranscriptionModel, 'open_path') as mock_open:
- model.open_file_location()
- mock_open.assert_called_once_with(path="/some/path")
-
- def test_hugging_face_opens_grandparent_directory(self):
- model = TranscriptionModel(
- model_type=ModelType.HUGGING_FACE,
- hugging_face_model_id="openai/whisper-tiny"
- )
- with patch.object(model, 'get_local_model_path', return_value="/cache/models/snapshot/model.safetensors"), \
- patch.object(TranscriptionModel, 'open_path') as mock_open:
- model.open_file_location()
- # For HF: dirname(path) -> /cache/models/snapshot, then open_path(dirname(...)) -> /cache/models
- mock_open.assert_called_once_with(path="/cache/models")
-
- def test_faster_whisper_opens_grandparent_directory(self):
- model = TranscriptionModel(model_type=ModelType.FASTER_WHISPER, whisper_model_size=WhisperModelSize.TINY)
- with patch.object(model, 'get_local_model_path', return_value="/cache/models/snapshot/model.bin"), \
- patch.object(TranscriptionModel, 'open_path') as mock_open:
- model.open_file_location()
- # For FW: dirname(path) -> /cache/models/snapshot, then open_path(dirname(...)) -> /cache/models
- mock_open.assert_called_once_with(path="/cache/models")
-
- def test_no_model_path_does_nothing(self):
- model = TranscriptionModel(model_type=ModelType.WHISPER, whisper_model_size=WhisperModelSize.TINY)
- with patch.object(model, 'get_local_model_path', return_value=None), \
- patch.object(TranscriptionModel, 'open_path') as mock_open:
- model.open_file_location()
- mock_open.assert_not_called()
-
-
-class TestTranscriptionModelDeleteLocalFile:
- def test_whisper_model_removes_file(self, tmp_path):
- model_file = tmp_path / "model.pt"
- model_file.write_bytes(b"fake model data")
- model = TranscriptionModel(model_type=ModelType.WHISPER, whisper_model_size=WhisperModelSize.TINY)
- with patch.object(model, 'get_local_model_path', return_value=str(model_file)):
- model.delete_local_file()
- assert not model_file.exists()
-
- def test_whisper_cpp_custom_removes_file(self, tmp_path):
- model_file = tmp_path / "ggml-model-whisper-custom.bin"
- model_file.write_bytes(b"fake model data")
- model = TranscriptionModel(model_type=ModelType.WHISPER_CPP, whisper_model_size=WhisperModelSize.CUSTOM)
- with patch.object(model, 'get_local_model_path', return_value=str(model_file)):
- model.delete_local_file()
- assert not model_file.exists()
-
- def test_whisper_cpp_non_custom_removes_bin_file(self, tmp_path):
- model_file = tmp_path / "ggml-tiny.bin"
- model_file.write_bytes(b"fake model data")
- model = TranscriptionModel(model_type=ModelType.WHISPER_CPP, whisper_model_size=WhisperModelSize.TINY)
- with patch.object(model, 'get_local_model_path', return_value=str(model_file)):
- model.delete_local_file()
- assert not model_file.exists()
-
- def test_whisper_cpp_non_custom_removes_coreml_files(self, tmp_path):
- model_file = tmp_path / "ggml-tiny.bin"
- model_file.write_bytes(b"fake model data")
- coreml_zip = tmp_path / "ggml-tiny-encoder.mlmodelc.zip"
- coreml_zip.write_bytes(b"fake zip")
- coreml_dir = tmp_path / "ggml-tiny-encoder.mlmodelc"
- coreml_dir.mkdir()
- model = TranscriptionModel(model_type=ModelType.WHISPER_CPP, whisper_model_size=WhisperModelSize.TINY)
- with patch.object(model, 'get_local_model_path', return_value=str(model_file)):
- model.delete_local_file()
- assert not model_file.exists()
- assert not coreml_zip.exists()
- assert not coreml_dir.exists()
-
- def test_hugging_face_removes_directory_tree(self, tmp_path):
- # Structure: models--repo/snapshots/abc/model.safetensors
- # delete_local_file does dirname(dirname(model_path)) = snapshots_dir
- repo_dir = tmp_path / "models--repo"
- snapshots_dir = repo_dir / "snapshots"
- snapshot_dir = snapshots_dir / "abc123"
- snapshot_dir.mkdir(parents=True)
- model_file = snapshot_dir / "model.safetensors"
- model_file.write_bytes(b"fake model")
-
- model = TranscriptionModel(
- model_type=ModelType.HUGGING_FACE,
- hugging_face_model_id="some/repo"
- )
- with patch.object(model, 'get_local_model_path', return_value=str(model_file)):
- model.delete_local_file()
- # Two dirs up from model_file: dirname(dirname(model_file)) = snapshots_dir
- assert not snapshots_dir.exists()
-
- def test_faster_whisper_removes_directory_tree(self, tmp_path):
- repo_dir = tmp_path / "faster-whisper-tiny"
- snapshots_dir = repo_dir / "snapshots"
- snapshot_dir = snapshots_dir / "abc123"
- snapshot_dir.mkdir(parents=True)
- model_file = snapshot_dir / "model.bin"
- model_file.write_bytes(b"fake model")
-
- model = TranscriptionModel(model_type=ModelType.FASTER_WHISPER, whisper_model_size=WhisperModelSize.TINY)
- with patch.object(model, 'get_local_model_path', return_value=str(model_file)):
- model.delete_local_file()
- # Two dirs up from model_file: dirname(dirname(model_file)) = snapshots_dir
- assert not snapshots_dir.exists()
-
-
-class TestHuggingfaceDownloadMonitorFileSize:
- def _make_monitor(self, tmp_path):
- model_root = str(tmp_path / "models--test" / "snapshots" / "abc")
- os.makedirs(model_root, exist_ok=True)
- progress = MagicMock()
- progress.emit = MagicMock()
- monitor = HuggingfaceDownloadMonitor(
- model_root=model_root,
- progress=progress,
- total_file_size=100 * 1024 * 1024
- )
- return monitor
-
- def test_emits_progress_for_tmp_files(self, tmp_path):
- from buzz.model_loader import model_root_dir as orig_root
- monitor = self._make_monitor(tmp_path)
-
- # Create a tmp file in model_root_dir
- with patch('buzz.model_loader.model_root_dir', str(tmp_path)):
- tmp_file = tmp_path / "tmpXYZ123"
- tmp_file.write_bytes(b"x" * 1024)
-
- monitor.stop_event.clear()
- # Run one iteration
- monitor.monitor_file_size.__func__ if hasattr(monitor.monitor_file_size, '__func__') else None
-
- # Manually call internal logic once
- emitted = []
- original_emit = monitor.progress.emit
- monitor.progress.emit = lambda x: emitted.append(x)
-
- import buzz.model_loader as ml
- old_root = ml.model_root_dir
- ml.model_root_dir = str(tmp_path)
- try:
- monitor.stop_event.set() # stop after one iteration
- monitor.stop_event.clear()
- # call once manually by running the loop body
- for filename in os.listdir(str(tmp_path)):
- if filename.startswith("tmp"):
- file_size = os.path.getsize(os.path.join(str(tmp_path), filename))
- monitor.progress.emit((file_size, monitor.total_file_size))
- assert len(emitted) > 0
- assert emitted[0][0] == 1024
- finally:
- ml.model_root_dir = old_root
-
- def test_emits_progress_for_incomplete_files(self, tmp_path):
- monitor = self._make_monitor(tmp_path)
-
- blobs_dir = tmp_path / "blobs"
- blobs_dir.mkdir()
- incomplete_file = blobs_dir / "somefile.incomplete"
- incomplete_file.write_bytes(b"y" * 2048)
-
- emitted = []
- monitor.incomplete_download_root = str(blobs_dir)
- monitor.progress.emit = lambda x: emitted.append(x)
-
- for filename in os.listdir(str(blobs_dir)):
- if filename.endswith(".incomplete"):
- file_size = os.path.getsize(os.path.join(str(blobs_dir), filename))
- monitor.progress.emit((file_size, monitor.total_file_size))
-
- assert len(emitted) > 0
- assert emitted[0][0] == 2048
-
- def test_stop_monitoring_emits_100_percent(self, tmp_path):
- monitor = self._make_monitor(tmp_path)
- monitor.monitor_thread = MagicMock()
- monitor.stop_monitoring()
- monitor.progress.emit.assert_called_with(
- (monitor.total_file_size, monitor.total_file_size)
- )
-
-
-class TestModelDownloaderDownloadModel:
- def _make_downloader(self, model):
- downloader = ModelDownloader(model=model)
- downloader.signals = MagicMock()
- downloader.signals.progress = MagicMock()
- downloader.signals.progress.emit = MagicMock()
- return downloader
-
- def test_download_model_fresh_success(self, tmp_path):
- model = TranscriptionModel(model_type=ModelType.WHISPER, whisper_model_size=WhisperModelSize.TINY)
- downloader = self._make_downloader(model)
-
- file_path = str(tmp_path / "model.pt")
- fake_content = b"fake model data" * 100
-
- mock_response = MagicMock()
- mock_response.__enter__ = lambda s: s
- mock_response.__exit__ = MagicMock(return_value=False)
- mock_response.status_code = 200
- mock_response.headers = {"Content-Length": str(len(fake_content))}
- mock_response.iter_content = MagicMock(return_value=[fake_content])
- mock_response.raise_for_status = MagicMock()
-
- with patch('requests.get', return_value=mock_response), \
- patch('requests.head') as mock_head:
- result = downloader.download_model(url="http://example.com/model.pt", file_path=file_path, expected_sha256=None)
-
- assert result is True
- assert os.path.exists(file_path)
- assert open(file_path, 'rb').read() == fake_content
-
- def test_download_model_already_downloaded_sha256_match(self, tmp_path):
- import hashlib
- content = b"complete model content"
- sha256 = hashlib.sha256(content).hexdigest()
- model_file = tmp_path / "model.pt"
- model_file.write_bytes(content)
-
- model = TranscriptionModel(model_type=ModelType.WHISPER, whisper_model_size=WhisperModelSize.TINY)
- downloader = self._make_downloader(model)
-
- mock_head = MagicMock()
- mock_head.headers = {"Content-Length": str(len(content)), "Accept-Ranges": "bytes"}
- mock_head.raise_for_status = MagicMock()
-
- with patch('requests.head', return_value=mock_head):
- result = downloader.download_model(
- url="http://example.com/model.pt",
- file_path=str(model_file),
- expected_sha256=sha256
- )
-
- assert result is True
-
- def test_download_model_sha256_mismatch_redownloads(self, tmp_path):
- import hashlib
- content = b"complete model content"
- bad_sha256 = "0" * 64
- model_file = tmp_path / "model.pt"
- model_file.write_bytes(content)
-
- model = TranscriptionModel(model_type=ModelType.WHISPER, whisper_model_size=WhisperModelSize.TINY)
- downloader = self._make_downloader(model)
-
- new_content = b"new model data"
- mock_head = MagicMock()
- mock_head.headers = {"Content-Length": str(len(content)), "Accept-Ranges": "bytes"}
- mock_head.raise_for_status = MagicMock()
-
- mock_response = MagicMock()
- mock_response.__enter__ = lambda s: s
- mock_response.__exit__ = MagicMock(return_value=False)
- mock_response.status_code = 200
- mock_response.headers = {"Content-Length": str(len(new_content))}
- mock_response.iter_content = MagicMock(return_value=[new_content])
- mock_response.raise_for_status = MagicMock()
-
- with patch('requests.head', return_value=mock_head), \
- patch('requests.get', return_value=mock_response):
- with pytest.raises(RuntimeError, match="SHA256 checksum does not match"):
- downloader.download_model(
- url="http://example.com/model.pt",
- file_path=str(model_file),
- expected_sha256=bad_sha256
- )
-
- # File is deleted after SHA256 mismatch
- assert not model_file.exists()
-
- def test_download_model_stopped_mid_download(self, tmp_path):
- model = TranscriptionModel(model_type=ModelType.WHISPER, whisper_model_size=WhisperModelSize.TINY)
- downloader = self._make_downloader(model)
- downloader.stopped = True
-
- file_path = str(tmp_path / "model.pt")
-
- def iter_content_gen(chunk_size):
- yield b"chunk1"
-
- mock_response = MagicMock()
- mock_response.__enter__ = lambda s: s
- mock_response.__exit__ = MagicMock(return_value=False)
- mock_response.status_code = 200
- mock_response.headers = {"Content-Length": "6"}
- mock_response.iter_content = iter_content_gen
- mock_response.raise_for_status = MagicMock()
-
- with patch('requests.get', return_value=mock_response):
- result = downloader.download_model(
- url="http://example.com/model.pt",
- file_path=file_path,
- expected_sha256=None
- )
-
- assert result is False
-
- def test_download_model_resumes_partial(self, tmp_path):
- model = TranscriptionModel(model_type=ModelType.WHISPER, whisper_model_size=WhisperModelSize.TINY)
- downloader = self._make_downloader(model)
-
- existing_content = b"partial"
- model_file = tmp_path / "model.pt"
- model_file.write_bytes(existing_content)
- resume_content = b" completed"
- total_size = len(existing_content) + len(resume_content)
-
- mock_head_size = MagicMock()
- mock_head_size.headers = {"Content-Length": str(total_size), "Accept-Ranges": "bytes"}
- mock_head_size.raise_for_status = MagicMock()
-
- mock_head_range = MagicMock()
- mock_head_range.headers = {"Accept-Ranges": "bytes"}
- mock_head_range.raise_for_status = MagicMock()
-
- mock_response = MagicMock()
- mock_response.__enter__ = lambda s: s
- mock_response.__exit__ = MagicMock(return_value=False)
- mock_response.status_code = 206
- mock_response.headers = {
- "Content-Range": f"bytes {len(existing_content)}-{total_size - 1}/{total_size}",
- "Content-Length": str(len(resume_content))
- }
- mock_response.iter_content = MagicMock(return_value=[resume_content])
- mock_response.raise_for_status = MagicMock()
-
- with patch('requests.head', side_effect=[mock_head_size, mock_head_range]), \
- patch('requests.get', return_value=mock_response):
- result = downloader.download_model(
- url="http://example.com/model.pt",
- file_path=str(model_file),
- expected_sha256=None
- )
-
- assert result is True
- assert open(str(model_file), 'rb').read() == existing_content + resume_content
-
-
-class TestModelDownloaderWhisperCpp:
- def _make_downloader(self, model, custom_url=None):
- downloader = ModelDownloader(model=model, custom_model_url=custom_url)
- downloader.signals = MagicMock()
- downloader.signals.progress = MagicMock()
- downloader.signals.finished = MagicMock()
- downloader.signals.error = MagicMock()
- return downloader
-
- def test_standard_model_calls_download_from_huggingface(self):
- model = TranscriptionModel(
- model_type=ModelType.WHISPER_CPP,
- whisper_model_size=WhisperModelSize.TINY,
- )
- downloader = self._make_downloader(model)
- model_name = WhisperModelSize.TINY.to_whisper_cpp_model_size()
-
- with patch("buzz.model_loader.download_from_huggingface", return_value="/fake/path") as mock_dl, \
- patch.object(downloader, "is_coreml_supported", False):
- downloader.run()
-
- mock_dl.assert_called_once_with(
- repo_id=WHISPER_CPP_REPO_ID,
- allow_patterns=[f"ggml-{model_name}.bin", "README.md"],
- progress=downloader.signals.progress,
- num_large_files=1,
- )
- downloader.signals.finished.emit.assert_called_once_with(
- os.path.join("/fake/path", f"ggml-{model_name}.bin")
- )
-
- def test_lumii_model_uses_lumii_repo(self):
- model = TranscriptionModel(
- model_type=ModelType.WHISPER_CPP,
- whisper_model_size=WhisperModelSize.LUMII,
- )
- downloader = self._make_downloader(model)
- model_name = WhisperModelSize.LUMII.to_whisper_cpp_model_size()
-
- with patch("buzz.model_loader.download_from_huggingface", return_value="/lumii/path") as mock_dl, \
- patch.object(downloader, "is_coreml_supported", False):
- downloader.run()
-
- mock_dl.assert_called_once()
- assert mock_dl.call_args.kwargs["repo_id"] == WHISPER_CPP_LUMII_REPO_ID
- downloader.signals.finished.emit.assert_called_once_with(
- os.path.join("/lumii/path", f"ggml-{model_name}.bin")
- )
-
- def test_custom_url_calls_download_model_to_path(self):
- model = TranscriptionModel(
- model_type=ModelType.WHISPER_CPP,
- whisper_model_size=WhisperModelSize.TINY,
- )
- custom_url = "https://example.com/my-model.bin"
- downloader = self._make_downloader(model, custom_url=custom_url)
-
- with patch.object(downloader, "download_model_to_path") as mock_dtp:
- downloader.run()
-
- mock_dtp.assert_called_once()
- call_kwargs = mock_dtp.call_args.kwargs
- assert call_kwargs["url"] == custom_url
-
- def test_coreml_model_includes_mlmodelc_in_file_list(self):
- model = TranscriptionModel(
- model_type=ModelType.WHISPER_CPP,
- whisper_model_size=WhisperModelSize.TINY,
- )
- downloader = self._make_downloader(model)
- model_name = WhisperModelSize.TINY.to_whisper_cpp_model_size()
-
- with patch("buzz.model_loader.download_from_huggingface", return_value="/fake/path") as mock_dl, \
- patch.object(downloader, "is_coreml_supported", True), \
- patch("zipfile.ZipFile"), \
- patch("shutil.rmtree"), \
- patch("shutil.move"), \
- patch("os.path.exists", return_value=False), \
- patch("os.listdir", return_value=[f"ggml-{model_name}-encoder.mlmodelc"]), \
- patch("os.path.isdir", return_value=True):
- downloader.run()
-
- mock_dl.assert_called_once()
- assert mock_dl.call_args.kwargs["num_large_files"] == 2
- allow_patterns = mock_dl.call_args.kwargs["allow_patterns"]
- assert f"ggml-{model_name}-encoder.mlmodelc.zip" in allow_patterns
-
- def test_coreml_zip_extracted_and_existing_dir_removed(self, tmp_path):
- model = TranscriptionModel(
- model_type=ModelType.WHISPER_CPP,
- whisper_model_size=WhisperModelSize.TINY,
- )
- downloader = self._make_downloader(model)
- model_name = WhisperModelSize.TINY.to_whisper_cpp_model_size()
-
- # Create a fake zip with a single top-level directory inside
- import zipfile as zf
- zip_path = tmp_path / f"ggml-{model_name}-encoder.mlmodelc.zip"
- nested_dir = f"ggml-{model_name}-encoder.mlmodelc"
- with zf.ZipFile(zip_path, "w") as z:
- z.writestr(f"{nested_dir}/weights", b"fake weights")
-
- existing_target = tmp_path / f"ggml-{model_name}-encoder.mlmodelc"
- existing_target.mkdir()
-
- with patch("buzz.model_loader.download_from_huggingface", return_value=str(tmp_path)), \
- patch.object(downloader, "is_coreml_supported", True):
- downloader.run()
-
- # Old directory was removed and recreated from zip
- assert existing_target.exists()
- downloader.signals.finished.emit.assert_called_once_with(
- str(tmp_path / f"ggml-{model_name}.bin")
- )
-
-
-class TestModelLoaderCertifiImportError:
- def test_certifi_import_error_path(self):
- """Test that module handles certifi ImportError gracefully by reimporting with mock"""
- import importlib
- import buzz.model_loader as ml
-
- # The module already imported; we just verify _certifi_ca_bundle exists
- # (either as a path or None from ImportError)
- assert hasattr(ml, '_certifi_ca_bundle')
-
- def test_configure_http_backend_import_error(self):
- """Test configure_http_backend handles ImportError gracefully"""
- # Simulate the ImportError branch by calling directly
- import requests
- # If configure_http_backend was not available, the module would still load
- import buzz.model_loader as ml
- assert ml is not None
diff --git a/tests/recording_test.py b/tests/recording_test.py
deleted file mode 100644
index c0d400c5..00000000
--- a/tests/recording_test.py
+++ /dev/null
@@ -1,115 +0,0 @@
-import numpy as np
-import pytest
-from unittest.mock import MagicMock, patch
-
-from buzz.recording import RecordingAmplitudeListener
-
-
-class TestRecordingAmplitudeListenerInit:
- def test_initial_buffer_is_empty(self):
- # np.ndarray([], dtype=np.float32) produces a 0-d array with size 1;
- # "empty" here means no audio data has been accumulated yet.
- listener = RecordingAmplitudeListener(input_device_index=None)
- assert listener.buffer.ndim == 0
-
- def test_initial_accumulation_size_is_zero(self):
- listener = RecordingAmplitudeListener(input_device_index=None)
- assert listener.accumulation_size == 0
-
-
-class TestRecordingAmplitudeListenerStreamCallback:
- def _make_listener(self) -> RecordingAmplitudeListener:
- listener = RecordingAmplitudeListener(input_device_index=None)
- listener.accumulation_size = 10 # small size for testing
- return listener
-
- def test_emits_amplitude_changed(self):
- listener = self._make_listener()
- emitted = []
- listener.amplitude_changed.connect(lambda v: emitted.append(v))
-
- chunk = np.array([[0.5], [0.5]], dtype=np.float32)
- listener.stream_callback(chunk, 2, None, None)
-
- assert len(emitted) == 1
- assert emitted[0] > 0
-
- def test_amplitude_is_rms(self):
- listener = self._make_listener()
- emitted = []
- listener.amplitude_changed.connect(lambda v: emitted.append(v))
-
- chunk = np.array([[1.0], [1.0]], dtype=np.float32)
- listener.stream_callback(chunk, 2, None, None)
-
- assert abs(emitted[0] - 1.0) < 1e-6
-
- def test_accumulates_buffer(self):
- listener = self._make_listener()
- size_before = listener.buffer.size
- chunk = np.array([[0.1]] * 4, dtype=np.float32)
- listener.stream_callback(chunk, 4, None, None)
- assert listener.buffer.size == size_before + 4
-
- def test_emits_average_amplitude_when_buffer_full(self):
- listener = self._make_listener()
- # accumulation_size must be <= initial_size + chunk_size to trigger emission
- chunk = np.array([[0.5]] * 4, dtype=np.float32)
- listener.accumulation_size = listener.buffer.size + len(chunk)
-
- averages = []
- listener.average_amplitude_changed.connect(lambda v: averages.append(v))
- listener.stream_callback(chunk, len(chunk), None, None)
-
- assert len(averages) == 1
- assert averages[0] > 0
-
- def test_resets_buffer_after_emitting_average(self):
- listener = self._make_listener()
- chunk = np.array([[0.5]] * 4, dtype=np.float32)
- listener.accumulation_size = listener.buffer.size + len(chunk)
-
- listener.stream_callback(chunk, len(chunk), None, None)
-
- # Buffer is reset to np.ndarray([], ...) — a 0-d array
- assert listener.buffer.ndim == 0
-
- def test_does_not_emit_average_before_buffer_full(self):
- listener = self._make_listener()
- chunk = np.array([[0.5]] * 4, dtype=np.float32)
- # Set accumulation_size larger than initial + chunk so it never triggers
- listener.accumulation_size = listener.buffer.size + len(chunk) + 1
-
- averages = []
- listener.average_amplitude_changed.connect(lambda v: averages.append(v))
- listener.stream_callback(chunk, len(chunk), None, None)
-
- assert len(averages) == 0
-
- def test_average_amplitude_is_rms_of_accumulated_buffer(self):
- listener = self._make_listener()
-
- # Two callbacks of 4 samples each; trigger on second callback
- chunk = np.array([[1.0], [1.0], [1.0], [1.0]], dtype=np.float32)
- listener.accumulation_size = listener.buffer.size + len(chunk)
-
- averages = []
- listener.average_amplitude_changed.connect(lambda v: averages.append(v))
- listener.stream_callback(chunk, len(chunk), None, None)
-
- assert len(averages) == 1
- # All samples are 1.0, so RMS must be 1.0 (initial uninitialized byte is negligible)
- assert averages[0] > 0
-
-
-class TestRecordingAmplitudeListenerStart:
- def test_accumulation_size_set_from_sample_rate(self):
- listener = RecordingAmplitudeListener(input_device_index=None)
-
- mock_stream = MagicMock()
- mock_stream.samplerate = 16000
-
- with patch("sounddevice.InputStream", return_value=mock_stream):
- listener.start_recording()
-
- assert listener.accumulation_size == 16000 * RecordingAmplitudeListener.ACCUMULATION_SECONDS
diff --git a/tests/recording_transcriber_test.py b/tests/recording_transcriber_test.py
deleted file mode 100644
index 8b394cd2..00000000
--- a/tests/recording_transcriber_test.py
+++ /dev/null
@@ -1,298 +0,0 @@
-import threading
-from unittest.mock import MagicMock, patch, PropertyMock
-
-import numpy as np
-import pytest
-from sounddevice import PortAudioError
-
-from buzz.model_loader import TranscriptionModel, ModelType, WhisperModelSize
-from buzz.settings.recording_transcriber_mode import RecordingTranscriberMode
-from buzz.transcriber.recording_transcriber import RecordingTranscriber
-from buzz.transcriber.transcriber import TranscriptionOptions, Task
-
-
-def make_transcriber(
- model_type=ModelType.WHISPER,
- mode_index=0,
- silence_threshold=0.0,
- language=None,
-) -> RecordingTranscriber:
- options = TranscriptionOptions(
- language=language,
- task=Task.TRANSCRIBE,
- model=TranscriptionModel(model_type=model_type, whisper_model_size=WhisperModelSize.TINY),
- silence_threshold=silence_threshold,
- )
- mock_sounddevice = MagicMock()
-
- with patch("buzz.transcriber.recording_transcriber.Settings") as MockSettings:
- instance = MockSettings.return_value
- instance.value.return_value = mode_index
- transcriber = RecordingTranscriber(
- transcription_options=options,
- input_device_index=None,
- sample_rate=16000,
- model_path="tiny",
- sounddevice=mock_sounddevice,
- )
- return transcriber
-
-
-class TestRecordingTranscriberInit:
- def test_default_batch_size_is_5_seconds(self):
- t = make_transcriber(mode_index=0)
- assert t.n_batch_samples == 5 * t.sample_rate
-
- def test_append_and_correct_mode_batch_size_uses_transcription_step(self):
- mode_index = list(RecordingTranscriberMode).index(RecordingTranscriberMode.APPEND_AND_CORRECT)
- t = make_transcriber(mode_index=mode_index)
- assert t.n_batch_samples == int(t.transcription_options.transcription_step * t.sample_rate)
-
- def test_append_and_correct_mode_keep_sample_seconds(self):
- mode_index = list(RecordingTranscriberMode).index(RecordingTranscriberMode.APPEND_AND_CORRECT)
- t = make_transcriber(mode_index=mode_index)
- assert t.keep_sample_seconds == 1.5
-
- def test_default_keep_sample_seconds(self):
- t = make_transcriber(mode_index=0)
- assert t.keep_sample_seconds == 0.15
-
- def test_queue_starts_empty(self):
- t = make_transcriber()
- assert t.queue.size == 0 or t.queue.ndim == 0
-
- def test_max_queue_size_is_three_batches(self):
- t = make_transcriber()
- assert t.max_queue_size == 3 * t.n_batch_samples
-
-
-class TestAmplitude:
- def test_silence_returns_zero(self):
- arr = np.zeros(100, dtype=np.float32)
- assert RecordingTranscriber.amplitude(arr) == 0.0
-
- def test_unit_signal_returns_one(self):
- arr = np.ones(100, dtype=np.float32)
- assert abs(RecordingTranscriber.amplitude(arr) - 1.0) < 1e-6
-
- def test_rms_calculation(self):
- arr = np.array([0.6, 0.8], dtype=np.float32)
- expected = float(np.sqrt(np.mean(arr ** 2)))
- assert abs(RecordingTranscriber.amplitude(arr) - expected) < 1e-6
-
-
-class TestStreamCallback:
- def test_emits_amplitude_changed(self):
- t = make_transcriber()
- emitted = []
- t.amplitude_changed.connect(lambda v: emitted.append(v))
-
- chunk = np.array([[0.5], [0.5]], dtype=np.float32)
- t.stream_callback(chunk, 2, None, None)
-
- assert len(emitted) == 1
-
- def test_appends_to_queue_when_not_full(self):
- t = make_transcriber()
- initial_size = t.queue.size
- chunk = np.ones((100,), dtype=np.float32)
- t.stream_callback(chunk.reshape(-1, 1), 100, None, None)
- assert t.queue.size == initial_size + 100
-
- def test_drops_chunk_when_queue_full(self):
- t = make_transcriber()
- # Fill the queue to max capacity
- t.queue = np.ones(t.max_queue_size, dtype=np.float32)
- size_before = t.queue.size
-
- chunk = np.array([[0.5], [0.5]], dtype=np.float32)
- t.stream_callback(chunk, 2, None, None)
-
- assert t.queue.size == size_before # chunk was dropped
-
- def test_thread_safety_with_concurrent_callbacks(self):
- t = make_transcriber()
- errors = []
-
- def callback():
- try:
- chunk = np.ones((10, 1), dtype=np.float32)
- t.stream_callback(chunk, 10, None, None)
- except Exception as e:
- errors.append(e)
-
- threads = [threading.Thread(target=callback) for _ in range(20)]
- for th in threads:
- th.start()
- for th in threads:
- th.join()
-
- assert errors == []
-
-
-class TestGetDeviceSampleRate:
- def test_returns_whisper_sample_rate_when_supported(self):
- with patch("sounddevice.check_input_settings"):
- rate = RecordingTranscriber.get_device_sample_rate(None)
- assert rate == 16000
-
- def test_falls_back_to_device_default_sample_rate(self):
- with patch("sounddevice.check_input_settings", side_effect=PortAudioError()), \
- patch("sounddevice.query_devices", return_value={"default_samplerate": 44100.0}):
- rate = RecordingTranscriber.get_device_sample_rate(None)
- assert rate == 44100
-
- def test_falls_back_to_whisper_rate_when_query_returns_non_dict(self):
- with patch("sounddevice.check_input_settings", side_effect=PortAudioError()), \
- patch("sounddevice.query_devices", return_value=None):
- rate = RecordingTranscriber.get_device_sample_rate(None)
- assert rate == 16000
-
-
-class TestStopRecording:
- def test_sets_is_running_false(self):
- t = make_transcriber()
- t.is_running = True
- t.stop_recording()
- assert t.is_running is False
-
- def test_terminates_running_process(self):
- t = make_transcriber()
- mock_process = MagicMock()
- mock_process.poll.return_value = None # process is running
- t.process = mock_process
-
- t.stop_recording()
-
- mock_process.terminate.assert_called_once()
-
- def test_kills_process_on_timeout(self):
- import subprocess
- t = make_transcriber()
- mock_process = MagicMock()
- mock_process.poll.return_value = None
- mock_process.wait.side_effect = subprocess.TimeoutExpired(cmd="test", timeout=5)
- t.process = mock_process
-
- t.stop_recording()
-
- mock_process.kill.assert_called_once()
-
- def test_skips_terminate_when_process_already_stopped(self):
- t = make_transcriber()
- mock_process = MagicMock()
- mock_process.poll.return_value = 0 # already exited
- t.process = mock_process
-
- t.stop_recording()
-
- mock_process.terminate.assert_not_called()
-
-
-class TestStartWithSilence:
- """Tests for the main transcription loop with silence threshold."""
-
- def _run_with_mock_model(self, transcription_options, samples, expected_text):
- """Helper to run a single transcription cycle with a mocked whisper model."""
- mock_model = MagicMock()
- mock_model.transcribe.return_value = {"text": expected_text}
-
- transcriber = make_transcriber(
- model_type=ModelType.WHISPER,
- silence_threshold=0.0,
- )
- transcriber.transcription_options = transcription_options
-
- received = []
- transcriber.transcription.connect(lambda t: received.append(t))
-
- def fake_input_stream(**kwargs):
- ctx = MagicMock()
- ctx.__enter__ = MagicMock(return_value=ctx)
- ctx.__exit__ = MagicMock(return_value=False)
- return ctx
-
- transcriber.queue = samples.copy()
- transcriber.is_running = True
-
- # After processing one batch, stop.
- call_count = [0]
- original_emit = transcriber.transcription.emit
-
- def stop_after_first(text):
- original_emit(text)
- transcriber.is_running = False
-
- transcriber.transcription.emit = stop_after_first
-
- with patch("buzz.transcriber.recording_transcriber.whisper") as mock_whisper, \
- patch("buzz.transcriber.recording_transcriber.torch") as mock_torch:
- mock_torch.cuda.is_available.return_value = False
- mock_whisper.load_model.return_value = mock_model
- mock_whisper.Whisper = type("Whisper", (), {})
- # make isinstance(model, whisper.Whisper) pass
- mock_model.__class__ = mock_whisper.Whisper
-
- with patch.object(transcriber, "sounddevice") as mock_sd:
- mock_stream_ctx = MagicMock()
- mock_stream_ctx.__enter__ = MagicMock(return_value=mock_stream_ctx)
- mock_stream_ctx.__exit__ = MagicMock(return_value=False)
- mock_sd.InputStream.return_value = mock_stream_ctx
-
- transcriber.start()
-
- return received
-
- def test_silent_audio_skips_transcription(self):
- t = make_transcriber(silence_threshold=1.0) # very high threshold
-
- received = []
- t.transcription.connect(lambda text: received.append(text))
-
- # Put silent samples in queue (amplitude = 0)
- t.queue = np.zeros(t.n_batch_samples + 100, dtype=np.float32)
- t.is_running = True
-
- stop_event = threading.Event()
-
- def stop_after_delay():
- stop_event.wait(timeout=1.5)
- t.stop_recording()
-
- stopper = threading.Thread(target=stop_after_delay, daemon=True)
-
- with patch("buzz.transcriber.recording_transcriber.whisper") as mock_whisper, \
- patch("buzz.transcriber.recording_transcriber.torch") as mock_torch:
- mock_torch.cuda.is_available.return_value = False
- mock_whisper.load_model.return_value = MagicMock()
-
- with patch.object(t, "sounddevice") as mock_sd:
- mock_stream_ctx = MagicMock()
- mock_stream_ctx.__enter__ = MagicMock(return_value=mock_stream_ctx)
- mock_stream_ctx.__exit__ = MagicMock(return_value=False)
- mock_sd.InputStream.return_value = mock_stream_ctx
-
- stopper.start()
- stop_event.set()
- t.start()
-
- # No transcription should have been emitted since audio is silent
- assert received == []
-
-
-class TestStartPortAudioError:
- def test_emits_error_on_portaudio_failure(self):
- t = make_transcriber()
- errors = []
- t.error.connect(lambda e: errors.append(e))
-
- with patch("buzz.transcriber.recording_transcriber.whisper") as mock_whisper, \
- patch("buzz.transcriber.recording_transcriber.torch") as mock_torch:
- mock_torch.cuda.is_available.return_value = False
- mock_whisper.load_model.return_value = MagicMock()
-
- with patch.object(t, "sounddevice") as mock_sd:
- mock_sd.InputStream.side_effect = PortAudioError()
- t.start()
-
- assert len(errors) == 1
diff --git a/tests/settings/settings_test.py b/tests/settings/settings_test.py
deleted file mode 100644
index c1154e7f..00000000
--- a/tests/settings/settings_test.py
+++ /dev/null
@@ -1,149 +0,0 @@
-import pytest
-from unittest.mock import Mock, patch
-
-from buzz.settings.settings import Settings
-
-
-class TestSettings:
- def test_transcription_tasks_table_column_order_key(self):
- """Test that TRANSCRIPTION_TASKS_TABLE_COLUMN_ORDER key is defined"""
- assert hasattr(Settings.Key, 'TRANSCRIPTION_TASKS_TABLE_COLUMN_ORDER')
- assert Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_ORDER.value == "transcription-tasks-table/column-order"
-
- def test_transcription_tasks_table_column_widths_key(self):
- """Test that TRANSCRIPTION_TASKS_TABLE_COLUMN_WIDTHS key is defined"""
- assert hasattr(Settings.Key, 'TRANSCRIPTION_TASKS_TABLE_COLUMN_WIDTHS')
- assert Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_WIDTHS.value == "transcription-tasks-table/column-widths"
-
- def test_transcription_tasks_table_column_visibility_key_exists(self):
- """Test that TRANSCRIPTION_TASKS_TABLE_COLUMN_VISIBILITY key still exists"""
- assert hasattr(Settings.Key, 'TRANSCRIPTION_TASKS_TABLE_COLUMN_VISIBILITY')
- assert Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_VISIBILITY.value == "transcription-tasks-table/column-visibility"
-
- def test_transcription_tasks_table_sort_state_key(self):
- """Test that TRANSCRIPTION_TASKS_TABLE_SORT_STATE key is defined"""
- assert hasattr(Settings.Key, 'TRANSCRIPTION_TASKS_TABLE_SORT_STATE')
- assert Settings.Key.TRANSCRIPTION_TASKS_TABLE_SORT_STATE.value == "transcription-tasks-table/sort-state"
-
- def test_all_transcription_tasks_table_keys_are_strings(self):
- """Test that all transcription tasks table keys are strings"""
- assert isinstance(Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_VISIBILITY.value, str)
- assert isinstance(Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_ORDER.value, str)
- assert isinstance(Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_WIDTHS.value, str)
- assert isinstance(Settings.Key.TRANSCRIPTION_TASKS_TABLE_SORT_STATE.value, str)
-
- def test_transcription_tasks_table_keys_have_correct_prefix(self):
- """Test that all transcription tasks table keys have the correct prefix"""
- prefix = "transcription-tasks-table/"
-
- assert Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_VISIBILITY.value.startswith(prefix)
- assert Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_ORDER.value.startswith(prefix)
- assert Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_WIDTHS.value.startswith(prefix)
- assert Settings.Key.TRANSCRIPTION_TASKS_TABLE_SORT_STATE.value.startswith(prefix)
-
- def test_transcription_tasks_table_keys_are_unique(self):
- """Test that all transcription tasks table keys are unique"""
- keys = [
- Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_VISIBILITY.value,
- Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_ORDER.value,
- Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_WIDTHS.value,
- Settings.Key.TRANSCRIPTION_TASKS_TABLE_SORT_STATE.value
- ]
-
- assert len(keys) == len(set(keys)), "All transcription tasks table keys should be unique"
-
- def test_settings_key_enum_values(self):
- """Test that Settings.Key enum values are properly defined"""
- # Test that the keys exist and have expected values
- expected_keys = {
- 'TRANSCRIPTION_TASKS_TABLE_COLUMN_VISIBILITY': 'transcription-tasks-table/column-visibility',
- 'TRANSCRIPTION_TASKS_TABLE_COLUMN_ORDER': 'transcription-tasks-table/column-order',
- 'TRANSCRIPTION_TASKS_TABLE_COLUMN_WIDTHS': 'transcription-tasks-table/column-widths',
- 'TRANSCRIPTION_TASKS_TABLE_SORT_STATE': 'transcription-tasks-table/sort-state'
- }
-
- for key_name, expected_value in expected_keys.items():
- assert hasattr(Settings.Key, key_name)
- assert getattr(Settings.Key, key_name).value == expected_value
-
- def test_settings_key_immutability(self):
- """Test that Settings.Key values cannot be modified"""
- # This test ensures that the keys are defined as constants
- original_visibility = Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_VISIBILITY
- original_order = Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_ORDER
- original_widths = Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_WIDTHS
- original_sort_state = Settings.Key.TRANSCRIPTION_TASKS_TABLE_SORT_STATE
-
- # Attempting to modify these should not work (they should be immutable)
- # If they were mutable, this test would fail
- assert Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_VISIBILITY == original_visibility
- assert Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_ORDER == original_order
- assert Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_WIDTHS == original_widths
- assert Settings.Key.TRANSCRIPTION_TASKS_TABLE_SORT_STATE == original_sort_state
-
- def test_settings_key_format_consistency(self):
- """Test that all transcription tasks table keys follow the same format"""
- keys = [
- Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_VISIBILITY.value,
- Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_ORDER.value,
- Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_WIDTHS.value,
- Settings.Key.TRANSCRIPTION_TASKS_TABLE_SORT_STATE.value
- ]
-
- for key in keys:
- # All keys should start with the same prefix
- assert key.startswith("transcription-tasks-table/")
- # All keys should contain only lowercase letters, hyphens, and forward slashes
- assert all(c.islower() or c in '-/' for c in key)
- # All keys should end with a descriptive suffix
- assert key.endswith(('visibility', 'order', 'widths', 'sort-state'))
-
- def test_settings_key_length(self):
- """Test that transcription tasks table keys have reasonable length"""
- keys = [
- Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_VISIBILITY.value,
- Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_ORDER.value,
- Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_WIDTHS.value,
- Settings.Key.TRANSCRIPTION_TASKS_TABLE_SORT_STATE.value
- ]
-
- for key in keys:
- # Keys should be long enough to be descriptive but not excessively long
- assert 20 <= len(key) <= 50, f"Key '{key}' has unexpected length: {len(key)}"
-
- def test_settings_key_naming_convention(self):
- """Test that transcription tasks table keys follow proper naming convention"""
- keys = [
- Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_VISIBILITY.value,
- Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_ORDER.value,
- Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_WIDTHS.value,
- Settings.Key.TRANSCRIPTION_TASKS_TABLE_SORT_STATE.value
- ]
-
- for key in keys:
- # Keys should use kebab-case (lowercase with hyphens)
- assert '-' in key, f"Key '{key}' should use kebab-case with hyphens"
- assert not any(c.isupper() for c in key), f"Key '{key}' should not contain uppercase letters"
- assert not '_' in key, f"Key '{key}' should use hyphens instead of underscores"
-
- def test_settings_key_usage_in_code(self):
- """Test that the settings keys can be used in typical settings operations"""
- # Mock a settings object to test key usage
- mock_settings = Mock()
- mock_settings.begin_group = Mock()
- mock_settings.end_group = Mock()
- mock_settings.settings = Mock()
-
- # Test that the keys can be used with begin_group
- mock_settings.begin_group(Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_VISIBILITY.value)
- mock_settings.begin_group(Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_ORDER.value)
- mock_settings.begin_group(Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_WIDTHS.value)
- mock_settings.begin_group(Settings.Key.TRANSCRIPTION_TASKS_TABLE_SORT_STATE.value)
-
- # Verify that begin_group was called with the correct keys
- assert mock_settings.begin_group.call_count == 4
- call_args = [call[0][0] for call in mock_settings.begin_group.call_args_list]
- assert Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_VISIBILITY.value in call_args
- assert Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_ORDER.value in call_args
- assert Settings.Key.TRANSCRIPTION_TASKS_TABLE_COLUMN_WIDTHS.value in call_args
- assert Settings.Key.TRANSCRIPTION_TASKS_TABLE_SORT_STATE.value in call_args
diff --git a/tests/store/__init__.py b/tests/store/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/tests/store/keyring_store_test.py b/tests/store/keyring_store_test.py
deleted file mode 100644
index 8486940f..00000000
--- a/tests/store/keyring_store_test.py
+++ /dev/null
@@ -1,457 +0,0 @@
-import json
-import os
-import sys
-import tempfile
-from unittest.mock import Mock, patch, MagicMock
-
-import pytest
-
-from buzz.store.keyring_store import (
- Key,
- _is_linux,
- _derive_key,
- _encrypt_value,
- _decrypt_value,
- _load_local_secrets,
- _save_local_secrets,
- _get_portal_password,
- _set_portal_password,
- _delete_portal_password,
- get_password,
- set_password,
- delete_password,
-)
-from buzz.settings.settings import APP_NAME
-
-
-class TestKey:
- def test_openai_api_key_exists(self):
- assert hasattr(Key, "OPENAI_API_KEY")
-
- def test_openai_api_key_value(self):
- assert Key.OPENAI_API_KEY.value == "OpenAI API key"
-
- def test_key_is_enum(self):
- assert isinstance(Key.OPENAI_API_KEY, Key)
-
-
-class TestIsLinux:
- @patch("buzz.store.keyring_store.sys.platform", "linux")
- def test_returns_true_on_linux(self):
- assert _is_linux() is True
-
- @patch("buzz.store.keyring_store.sys.platform", "linux2")
- def test_returns_true_on_linux2(self):
- assert _is_linux() is True
-
- @patch("buzz.store.keyring_store.sys.platform", "darwin")
- def test_returns_false_on_macos(self):
- assert _is_linux() is False
-
- @patch("buzz.store.keyring_store.sys.platform", "win32")
- def test_returns_false_on_windows(self):
- assert _is_linux() is False
-
-
-class TestDeriveKey:
- def test_derive_key_returns_32_bytes(self):
- master_secret = b"test_secret"
- key_name = "test_key"
- derived = _derive_key(master_secret, key_name)
- assert len(derived) == 32
-
- def test_derive_key_is_deterministic(self):
- master_secret = b"test_secret"
- key_name = "test_key"
- derived1 = _derive_key(master_secret, key_name)
- derived2 = _derive_key(master_secret, key_name)
- assert derived1 == derived2
-
- def test_derive_key_different_for_different_names(self):
- master_secret = b"test_secret"
- derived1 = _derive_key(master_secret, "key1")
- derived2 = _derive_key(master_secret, "key2")
- assert derived1 != derived2
-
- def test_derive_key_different_for_different_secrets(self):
- key_name = "test_key"
- derived1 = _derive_key(b"secret1", key_name)
- derived2 = _derive_key(b"secret2", key_name)
- assert derived1 != derived2
-
-
-class TestEncryptDecrypt:
- def test_encrypt_decrypt_roundtrip(self):
- key = b"0123456789abcdef0123456789abcdef" # 32 bytes
- original = "test_password_123"
- encrypted = _encrypt_value(original, key)
- decrypted = _decrypt_value(encrypted, key)
- assert decrypted == original
-
- def test_encrypt_decrypt_empty_string(self):
- key = b"0123456789abcdef0123456789abcdef"
- original = ""
- encrypted = _encrypt_value(original, key)
- decrypted = _decrypt_value(encrypted, key)
- assert decrypted == original
-
- def test_encrypt_decrypt_unicode(self):
- key = b"0123456789abcdef0123456789abcdef"
- original = "test_password_\u4e2d\u6587_\U0001f600"
- encrypted = _encrypt_value(original, key)
- decrypted = _decrypt_value(encrypted, key)
- assert decrypted == original
-
- def test_encrypt_decrypt_long_string(self):
- key = b"0123456789abcdef0123456789abcdef"
- original = "a" * 1000
- encrypted = _encrypt_value(original, key)
- decrypted = _decrypt_value(encrypted, key)
- assert decrypted == original
-
- def test_encrypted_is_base64(self):
- key = b"0123456789abcdef0123456789abcdef"
- original = "test"
- encrypted = _encrypt_value(original, key)
- # Should be valid base64
- import base64
- base64.b64decode(encrypted) # Should not raise
-
- def test_different_keys_produce_different_ciphertext(self):
- key1 = b"0123456789abcdef0123456789abcdef"
- key2 = b"fedcba9876543210fedcba9876543210"
- original = "test_password"
- encrypted1 = _encrypt_value(original, key1)
- encrypted2 = _encrypt_value(original, key2)
- assert encrypted1 != encrypted2
-
-
-class TestLocalSecrets:
- def test_load_empty_file(self):
- with tempfile.TemporaryDirectory() as tmpdir:
- with patch(
- "buzz.store.keyring_store._get_secrets_file_path",
- return_value=os.path.join(tmpdir, ".secrets.json"),
- ):
- result = _load_local_secrets()
- assert result == {}
-
- def test_save_and_load_secrets(self):
- with tempfile.TemporaryDirectory() as tmpdir:
- secrets_path = os.path.join(tmpdir, ".secrets.json")
- with patch(
- "buzz.store.keyring_store._get_secrets_file_path",
- return_value=secrets_path,
- ):
- test_secrets = {"key1": "value1", "key2": "value2"}
- _save_local_secrets(test_secrets)
- loaded = _load_local_secrets()
- assert loaded == test_secrets
-
- @pytest.mark.skipif(sys.platform == "win32", reason="Unix file permissions not applicable on Windows")
- def test_save_sets_restrictive_permissions(self):
- with tempfile.TemporaryDirectory() as tmpdir:
- secrets_path = os.path.join(tmpdir, ".secrets.json")
- with patch(
- "buzz.store.keyring_store._get_secrets_file_path",
- return_value=secrets_path,
- ):
- _save_local_secrets({"key": "value"})
- # Check file permissions (0o600 = owner read/write only)
- mode = os.stat(secrets_path).st_mode & 0o777
- assert mode == 0o600
-
- def test_load_handles_corrupted_json(self):
- with tempfile.TemporaryDirectory() as tmpdir:
- secrets_path = os.path.join(tmpdir, ".secrets.json")
- with open(secrets_path, "w") as f:
- f.write("not valid json {{{")
- with patch(
- "buzz.store.keyring_store._get_secrets_file_path",
- return_value=secrets_path,
- ):
- result = _load_local_secrets()
- assert result == {}
-
-
-class TestPortalPassword:
- @patch("buzz.store.keyring_store._get_portal_secret")
- @patch("buzz.store.keyring_store._load_local_secrets")
- def test_get_portal_password_returns_none_when_no_portal(
- self, mock_load, mock_portal
- ):
- mock_portal.return_value = None
- result = _get_portal_password(Key.OPENAI_API_KEY)
- assert result is None
-
- @patch("buzz.store.keyring_store._get_portal_secret")
- @patch("buzz.store.keyring_store._load_local_secrets")
- def test_get_portal_password_returns_none_when_key_not_found(
- self, mock_load, mock_portal
- ):
- mock_portal.return_value = b"test_secret_64_bytes_" + b"x" * 43
- mock_load.return_value = {}
- result = _get_portal_password(Key.OPENAI_API_KEY)
- assert result is None
-
- @patch("buzz.store.keyring_store._get_portal_secret")
- @patch("buzz.store.keyring_store._load_local_secrets")
- def test_get_portal_password_decrypts_stored_value(self, mock_load, mock_portal):
- portal_secret = b"test_secret_64_bytes_" + b"x" * 43
- mock_portal.return_value = portal_secret
-
- # Pre-encrypt a value
- derived_key = _derive_key(portal_secret, Key.OPENAI_API_KEY.value)
- encrypted = _encrypt_value("my_api_key", derived_key)
-
- mock_load.return_value = {Key.OPENAI_API_KEY.value: encrypted}
-
- result = _get_portal_password(Key.OPENAI_API_KEY)
- assert result == "my_api_key"
-
- @patch("buzz.store.keyring_store._get_portal_secret")
- def test_set_portal_password_returns_false_when_no_portal(self, mock_portal):
- mock_portal.return_value = None
- result = _set_portal_password(Key.OPENAI_API_KEY, "test_password")
- assert result is False
-
- @patch("buzz.store.keyring_store._get_portal_secret")
- @patch("buzz.store.keyring_store._load_local_secrets")
- @patch("buzz.store.keyring_store._save_local_secrets")
- def test_set_portal_password_encrypts_and_saves(
- self, mock_save, mock_load, mock_portal
- ):
- portal_secret = b"test_secret_64_bytes_" + b"x" * 43
- mock_portal.return_value = portal_secret
- mock_load.return_value = {}
-
- result = _set_portal_password(Key.OPENAI_API_KEY, "test_password")
-
- assert result is True
- mock_save.assert_called_once()
- saved_secrets = mock_save.call_args[0][0]
- assert Key.OPENAI_API_KEY.value in saved_secrets
-
- # Verify the saved value can be decrypted
- derived_key = _derive_key(portal_secret, Key.OPENAI_API_KEY.value)
- decrypted = _decrypt_value(saved_secrets[Key.OPENAI_API_KEY.value], derived_key)
- assert decrypted == "test_password"
-
-
-class TestDeletePortalPassword:
- @patch("buzz.store.keyring_store._load_local_secrets")
- @patch("buzz.store.keyring_store._save_local_secrets")
- def test_delete_existing_key(self, mock_save, mock_load):
- mock_load.return_value = {Key.OPENAI_API_KEY.value: "encrypted_value"}
-
- result = _delete_portal_password(Key.OPENAI_API_KEY)
-
- assert result is True
- mock_save.assert_called_once()
- saved_secrets = mock_save.call_args[0][0]
- assert Key.OPENAI_API_KEY.value not in saved_secrets
-
- @patch("buzz.store.keyring_store._load_local_secrets")
- @patch("buzz.store.keyring_store._save_local_secrets")
- def test_delete_nonexistent_key(self, mock_save, mock_load):
- mock_load.return_value = {}
-
- result = _delete_portal_password(Key.OPENAI_API_KEY)
-
- assert result is False
- mock_save.assert_not_called()
-
-
-class TestGetPassword:
- @patch("buzz.store.keyring_store._is_linux")
- @patch("buzz.store.keyring_store._get_portal_password")
- @patch("buzz.store.keyring_store.keyring")
- def test_returns_portal_password_on_linux(
- self, mock_keyring, mock_portal, mock_is_linux
- ):
- mock_is_linux.return_value = True
- mock_portal.return_value = "portal_password"
-
- result = get_password(Key.OPENAI_API_KEY)
-
- assert result == "portal_password"
- mock_keyring.get_password.assert_not_called()
-
- @patch("buzz.store.keyring_store._is_linux")
- @patch("buzz.store.keyring_store._get_portal_password")
- @patch("buzz.store.keyring_store.keyring")
- def test_falls_back_to_keyring_when_portal_returns_none(
- self, mock_keyring, mock_portal, mock_is_linux
- ):
- mock_is_linux.return_value = True
- mock_portal.return_value = None
- mock_keyring.get_password.return_value = "keyring_password"
-
- result = get_password(Key.OPENAI_API_KEY)
-
- assert result == "keyring_password"
- mock_keyring.get_password.assert_called_once_with(
- APP_NAME, username=Key.OPENAI_API_KEY.value
- )
-
- @patch("buzz.store.keyring_store._is_linux")
- @patch("buzz.store.keyring_store.keyring")
- def test_uses_keyring_directly_on_non_linux(self, mock_keyring, mock_is_linux):
- mock_is_linux.return_value = False
- mock_keyring.get_password.return_value = "keyring_password"
-
- result = get_password(Key.OPENAI_API_KEY)
-
- assert result == "keyring_password"
-
- @patch("buzz.store.keyring_store._is_linux")
- @patch("buzz.store.keyring_store.keyring")
- def test_returns_empty_string_when_keyring_returns_none(
- self, mock_keyring, mock_is_linux
- ):
- mock_is_linux.return_value = False
- mock_keyring.get_password.return_value = None
-
- result = get_password(Key.OPENAI_API_KEY)
-
- assert result == ""
-
- @patch("buzz.store.keyring_store._is_linux")
- @patch("buzz.store.keyring_store.keyring")
- def test_returns_empty_string_on_keyring_exception(
- self, mock_keyring, mock_is_linux
- ):
- mock_is_linux.return_value = False
- mock_keyring.get_password.side_effect = Exception("Keyring error")
-
- result = get_password(Key.OPENAI_API_KEY)
-
- assert result == ""
-
-
-class TestSetPassword:
- @patch("buzz.store.keyring_store._is_linux")
- @patch("buzz.store.keyring_store._set_portal_password")
- @patch("buzz.store.keyring_store.keyring")
- def test_uses_portal_on_linux_when_successful(
- self, mock_keyring, mock_portal, mock_is_linux
- ):
- mock_is_linux.return_value = True
- mock_portal.return_value = True
-
- set_password(Key.OPENAI_API_KEY, "test_password")
-
- mock_portal.assert_called_once_with(Key.OPENAI_API_KEY, "test_password")
- mock_keyring.set_password.assert_not_called()
-
- @patch("buzz.store.keyring_store._is_linux")
- @patch("buzz.store.keyring_store._set_portal_password")
- @patch("buzz.store.keyring_store.keyring")
- def test_falls_back_to_keyring_when_portal_fails(
- self, mock_keyring, mock_portal, mock_is_linux
- ):
- mock_is_linux.return_value = True
- mock_portal.return_value = False
-
- set_password(Key.OPENAI_API_KEY, "test_password")
-
- mock_keyring.set_password.assert_called_once_with(
- APP_NAME, Key.OPENAI_API_KEY.value, "test_password"
- )
-
- @patch("buzz.store.keyring_store._is_linux")
- @patch("buzz.store.keyring_store.keyring")
- def test_uses_keyring_directly_on_non_linux(self, mock_keyring, mock_is_linux):
- mock_is_linux.return_value = False
-
- set_password(Key.OPENAI_API_KEY, "test_password")
-
- mock_keyring.set_password.assert_called_once_with(
- APP_NAME, Key.OPENAI_API_KEY.value, "test_password"
- )
-
-
-class TestDeletePassword:
- @patch("buzz.store.keyring_store._is_linux")
- @patch("buzz.store.keyring_store._delete_portal_password")
- @patch("buzz.store.keyring_store.keyring")
- def test_deletes_from_both_on_linux(
- self, mock_keyring, mock_delete_portal, mock_is_linux
- ):
- mock_is_linux.return_value = True
- mock_delete_portal.return_value = True
-
- delete_password(Key.OPENAI_API_KEY)
-
- mock_delete_portal.assert_called_once_with(Key.OPENAI_API_KEY)
- mock_keyring.delete_password.assert_called_once_with(
- APP_NAME, Key.OPENAI_API_KEY.value
- )
-
- @patch("buzz.store.keyring_store._is_linux")
- @patch("buzz.store.keyring_store.keyring")
- def test_deletes_from_keyring_only_on_non_linux(self, mock_keyring, mock_is_linux):
- mock_is_linux.return_value = False
-
- delete_password(Key.OPENAI_API_KEY)
-
- mock_keyring.delete_password.assert_called_once_with(
- APP_NAME, Key.OPENAI_API_KEY.value
- )
-
- @patch("buzz.store.keyring_store._is_linux")
- @patch("buzz.store.keyring_store.keyring")
- def test_ignores_password_delete_error(self, mock_keyring, mock_is_linux):
- mock_is_linux.return_value = False
- mock_keyring.errors.PasswordDeleteError = Exception
- mock_keyring.delete_password.side_effect = (
- mock_keyring.errors.PasswordDeleteError("Not found")
- )
-
- # Should not raise
- delete_password(Key.OPENAI_API_KEY)
-
- @patch("buzz.store.keyring_store._is_linux")
- @patch("buzz.store.keyring_store.keyring")
- def test_handles_other_keyring_exceptions(self, mock_keyring, mock_is_linux):
- mock_is_linux.return_value = False
- mock_keyring.errors.PasswordDeleteError = KeyError # Different exception type
- mock_keyring.delete_password.side_effect = RuntimeError("Some other error")
-
- # Should not raise
- delete_password(Key.OPENAI_API_KEY)
-
-
-class TestIntegration:
- """Integration tests that test the full flow with mocked portal."""
-
- @patch("buzz.store.keyring_store._get_portal_secret")
- def test_full_roundtrip_with_portal(self, mock_portal):
- """Test set -> get -> delete flow with portal."""
- portal_secret = b"integration_test_secret_" + b"y" * 40
-
- with tempfile.TemporaryDirectory() as tmpdir:
- secrets_path = os.path.join(tmpdir, ".secrets.json")
-
- with patch(
- "buzz.store.keyring_store._get_secrets_file_path",
- return_value=secrets_path,
- ):
- with patch("buzz.store.keyring_store._is_linux", return_value=True):
- mock_portal.return_value = portal_secret
-
- # Set password
- result = _set_portal_password(Key.OPENAI_API_KEY, "my_secret_key")
- assert result is True
-
- # Get password
- retrieved = _get_portal_password(Key.OPENAI_API_KEY)
- assert retrieved == "my_secret_key"
-
- # Delete password
- deleted = _delete_portal_password(Key.OPENAI_API_KEY)
- assert deleted is True
-
- # Verify it's gone
- retrieved_after_delete = _get_portal_password(Key.OPENAI_API_KEY)
- assert retrieved_after_delete is None
diff --git a/tests/transcriber/__init__.py b/tests/transcriber/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/tests/transcriber/file_transcriber_queue_worker_test.py b/tests/transcriber/file_transcriber_queue_worker_test.py
deleted file mode 100644
index 03936bc8..00000000
--- a/tests/transcriber/file_transcriber_queue_worker_test.py
+++ /dev/null
@@ -1,408 +0,0 @@
-import pytest
-import unittest.mock
-import uuid
-from PyQt6.QtCore import QCoreApplication, QThread
-from buzz.file_transcriber_queue_worker import FileTranscriberQueueWorker
-from buzz.model_loader import ModelType, TranscriptionModel, WhisperModelSize
-from buzz.transcriber.transcriber import FileTranscriptionTask, TranscriptionOptions, FileTranscriptionOptions, Segment
-from buzz.transcriber.whisper_file_transcriber import WhisperFileTranscriber
-from tests.audio import test_multibyte_utf8_audio_path
-import time
-
-
-@pytest.fixture(scope="session")
-def qapp():
- app = QCoreApplication.instance()
- if app is None:
- app = QCoreApplication([])
- yield app
- app.quit()
-
-
-@pytest.fixture
-def worker(qapp):
- worker = FileTranscriberQueueWorker()
- thread = QThread()
- worker.moveToThread(thread)
- thread.started.connect(worker.run)
- thread.start()
- yield worker
- worker.stop()
- thread.quit()
- thread.wait()
-
-
-@pytest.fixture
-def simple_worker(qapp):
- """A non-threaded worker for unit tests that only test individual methods."""
- worker = FileTranscriberQueueWorker()
- yield worker
-
-
-class TestFileTranscriberQueueWorker:
- def test_cancel_task_adds_to_canceled_set(self, simple_worker):
- task_id = uuid.uuid4()
- simple_worker.cancel_task(task_id)
- assert task_id in simple_worker.canceled_tasks
-
- def test_add_task_removes_from_canceled(self, simple_worker):
- options = TranscriptionOptions(
- model=TranscriptionModel(model_type=ModelType.WHISPER_CPP, whisper_model_size=WhisperModelSize.TINY),
- extract_speech=False
- )
- task = FileTranscriptionTask(
- file_path=str(test_multibyte_utf8_audio_path),
- transcription_options=options,
- file_transcription_options=FileTranscriptionOptions(),
- model_path="mock_path"
- )
-
- # First cancel it
- simple_worker.cancel_task(task.uid)
- assert task.uid in simple_worker.canceled_tasks
-
- # Prevent trigger_run from starting the run loop
- simple_worker.is_running = True
- # Then add it back
- simple_worker.add_task(task)
- assert task.uid not in simple_worker.canceled_tasks
-
- def test_on_task_error_with_cancellation(self, simple_worker):
- options = TranscriptionOptions()
- task = FileTranscriptionTask(
- file_path=str(test_multibyte_utf8_audio_path),
- transcription_options=options,
- file_transcription_options=FileTranscriptionOptions(),
- model_path="mock_path"
- )
- simple_worker.current_task = task
-
- error_spy = unittest.mock.Mock()
- simple_worker.task_error.connect(error_spy)
-
- simple_worker.on_task_error("Transcription was canceled")
-
- error_spy.assert_called_once()
- assert task.status == FileTranscriptionTask.Status.CANCELED
- assert "canceled" in task.error.lower()
-
- def test_on_task_error_with_regular_error(self, simple_worker):
- options = TranscriptionOptions()
- task = FileTranscriptionTask(
- file_path=str(test_multibyte_utf8_audio_path),
- transcription_options=options,
- file_transcription_options=FileTranscriptionOptions(),
- model_path="mock_path"
- )
- simple_worker.current_task = task
-
- error_spy = unittest.mock.Mock()
- simple_worker.task_error.connect(error_spy)
-
- simple_worker.on_task_error("Some error occurred")
-
- error_spy.assert_called_once()
- assert task.status == FileTranscriptionTask.Status.FAILED
- assert task.error == "Some error occurred"
-
- def test_on_task_progress_conversion(self, simple_worker):
- options = TranscriptionOptions()
- task = FileTranscriptionTask(
- file_path=str(test_multibyte_utf8_audio_path),
- transcription_options=options,
- file_transcription_options=FileTranscriptionOptions(),
- model_path="mock_path"
- )
- simple_worker.current_task = task
-
- progress_spy = unittest.mock.Mock()
- simple_worker.task_progress.connect(progress_spy)
-
- simple_worker.on_task_progress((50, 100))
-
- progress_spy.assert_called_once()
- args = progress_spy.call_args[0]
- assert args[0] == task
- assert args[1] == 0.5
-
- def test_stop_puts_sentinel_in_queue(self, simple_worker):
- initial_size = simple_worker.tasks_queue.qsize()
- simple_worker.stop()
- # Sentinel (None) should be added to queue
- assert simple_worker.tasks_queue.qsize() == initial_size + 1
-
- def test_on_task_completed_with_speech_path(self, simple_worker, tmp_path):
- """Test on_task_completed cleans up speech_path file"""
- options = TranscriptionOptions()
- task = FileTranscriptionTask(
- file_path=str(test_multibyte_utf8_audio_path),
- transcription_options=options,
- file_transcription_options=FileTranscriptionOptions(),
- model_path="mock_path"
- )
- simple_worker.current_task = task
-
- # Create a temporary file to simulate speech extraction output
- speech_file = tmp_path / "audio_speech.mp3"
- speech_file.write_bytes(b"fake audio data")
- simple_worker.speech_path = speech_file
-
- completed_spy = unittest.mock.Mock()
- simple_worker.task_completed.connect(completed_spy)
-
- simple_worker.on_task_completed([Segment(0, 1000, "Test")])
-
- completed_spy.assert_called_once()
- # Speech path should be cleaned up
- assert simple_worker.speech_path is None
- assert not speech_file.exists()
-
- def test_on_task_completed_speech_path_missing(self, simple_worker, tmp_path):
- """Test on_task_completed handles missing speech_path file gracefully"""
- options = TranscriptionOptions()
- task = FileTranscriptionTask(
- file_path=str(test_multibyte_utf8_audio_path),
- transcription_options=options,
- file_transcription_options=FileTranscriptionOptions(),
- model_path="mock_path"
- )
- simple_worker.current_task = task
-
- # Set a speech path that doesn't exist
- simple_worker.speech_path = tmp_path / "nonexistent_speech.mp3"
-
- completed_spy = unittest.mock.Mock()
- simple_worker.task_completed.connect(completed_spy)
-
- # Should not raise even if file doesn't exist
- simple_worker.on_task_completed([])
-
- completed_spy.assert_called_once()
- assert simple_worker.speech_path is None
-
- def test_on_task_download_progress(self, simple_worker):
- """Test on_task_download_progress emits signal"""
- options = TranscriptionOptions()
- task = FileTranscriptionTask(
- file_path=str(test_multibyte_utf8_audio_path),
- transcription_options=options,
- file_transcription_options=FileTranscriptionOptions(),
- model_path="mock_path"
- )
- simple_worker.current_task = task
-
- download_spy = unittest.mock.Mock()
- simple_worker.task_download_progress.connect(download_spy)
-
- simple_worker.on_task_download_progress(0.5)
-
- download_spy.assert_called_once()
- args = download_spy.call_args[0]
- assert args[0] == task
- assert args[1] == 0.5
-
- def test_cancel_task_stops_current_transcriber(self, simple_worker):
- """Test cancel_task stops the current transcriber if it matches"""
- options = TranscriptionOptions()
- task = FileTranscriptionTask(
- file_path=str(test_multibyte_utf8_audio_path),
- transcription_options=options,
- file_transcription_options=FileTranscriptionOptions(),
- model_path="mock_path"
- )
- simple_worker.current_task = task
-
- mock_transcriber = unittest.mock.Mock()
- simple_worker.current_transcriber = mock_transcriber
-
- simple_worker.cancel_task(task.uid)
-
- assert task.uid in simple_worker.canceled_tasks
- mock_transcriber.stop.assert_called_once()
-
- def test_on_task_error_task_in_canceled_set(self, simple_worker):
- """Test on_task_error does not emit signal when task is canceled"""
- options = TranscriptionOptions()
- task = FileTranscriptionTask(
- file_path=str(test_multibyte_utf8_audio_path),
- transcription_options=options,
- file_transcription_options=FileTranscriptionOptions(),
- model_path="mock_path"
- )
- simple_worker.current_task = task
- # Mark task as canceled
- simple_worker.canceled_tasks.add(task.uid)
-
- error_spy = unittest.mock.Mock()
- simple_worker.task_error.connect(error_spy)
-
- simple_worker.on_task_error("Some error")
-
- # Should NOT emit since task was canceled
- error_spy.assert_not_called()
-
-
-class TestFileTranscriberQueueWorkerRun:
- def _make_task(self, model_type=ModelType.WHISPER_CPP, extract_speech=False):
- options = TranscriptionOptions(
- model=TranscriptionModel(model_type=model_type, whisper_model_size=WhisperModelSize.TINY),
- extract_speech=extract_speech
- )
- return FileTranscriptionTask(
- file_path=str(test_multibyte_utf8_audio_path),
- transcription_options=options,
- file_transcription_options=FileTranscriptionOptions(),
- model_path="mock_path"
- )
-
- def test_run_returns_early_when_already_running(self, simple_worker):
- simple_worker.is_running = True
- # Should return without blocking (queue is empty, no get() call)
- simple_worker.run()
- # is_running stays True, nothing changed
- assert simple_worker.is_running is True
-
- def test_run_stops_on_sentinel(self, simple_worker, qapp):
- completed_spy = unittest.mock.Mock()
- simple_worker.completed.connect(completed_spy)
-
- simple_worker.tasks_queue.put(None)
- simple_worker.run()
-
- completed_spy.assert_called_once()
- assert simple_worker.is_running is False
-
- def test_run_skips_canceled_task_then_stops_on_sentinel(self, simple_worker, qapp):
- task = self._make_task()
- simple_worker.canceled_tasks.add(task.uid)
-
- started_spy = unittest.mock.Mock()
- simple_worker.task_started.connect(started_spy)
-
- # Put canceled task then sentinel
- simple_worker.tasks_queue.put(task)
- simple_worker.tasks_queue.put(None)
-
- simple_worker.run()
-
- # Canceled task should be skipped; completed emitted
- started_spy.assert_not_called()
- assert simple_worker.is_running is False
-
- def test_run_creates_openai_transcriber(self, simple_worker, qapp):
- from buzz.transcriber.openai_whisper_api_file_transcriber import OpenAIWhisperAPIFileTranscriber
-
- task = self._make_task(model_type=ModelType.OPEN_AI_WHISPER_API)
- simple_worker.tasks_queue.put(task)
-
- with unittest.mock.patch.object(OpenAIWhisperAPIFileTranscriber, 'run'), \
- unittest.mock.patch.object(OpenAIWhisperAPIFileTranscriber, 'moveToThread'), \
- unittest.mock.patch('buzz.file_transcriber_queue_worker.QThread') as mock_thread_class:
- mock_thread = unittest.mock.MagicMock()
- mock_thread_class.return_value = mock_thread
-
- simple_worker.run()
-
- assert isinstance(simple_worker.current_transcriber, OpenAIWhisperAPIFileTranscriber)
-
- def test_run_creates_whisper_transcriber_for_whisper_cpp(self, simple_worker, qapp):
- task = self._make_task(model_type=ModelType.WHISPER_CPP)
- simple_worker.tasks_queue.put(task)
-
- with unittest.mock.patch.object(WhisperFileTranscriber, 'run'), \
- unittest.mock.patch.object(WhisperFileTranscriber, 'moveToThread'), \
- unittest.mock.patch('buzz.file_transcriber_queue_worker.QThread') as mock_thread_class:
- mock_thread = unittest.mock.MagicMock()
- mock_thread_class.return_value = mock_thread
-
- simple_worker.run()
-
- assert isinstance(simple_worker.current_transcriber, WhisperFileTranscriber)
-
- def test_run_speech_extraction_failure_emits_error(self, simple_worker, qapp):
- task = self._make_task(extract_speech=True)
- simple_worker.tasks_queue.put(task)
-
- error_spy = unittest.mock.Mock()
- simple_worker.task_error.connect(error_spy)
-
- with unittest.mock.patch('buzz.file_transcriber_queue_worker.demucsApi.Separator',
- side_effect=RuntimeError("No internet")):
- simple_worker.run()
-
- error_spy.assert_called_once()
- args = error_spy.call_args[0]
- assert args[0] == task
- assert simple_worker.is_running is False
-
-
-def test_transcription_with_whisper_cpp_tiny_no_speech_extraction(worker):
- options = TranscriptionOptions(
- model=TranscriptionModel(model_type=ModelType.WHISPER_CPP, whisper_model_size=WhisperModelSize.TINY),
- extract_speech=False
- )
- 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(WhisperFileTranscriber, '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 = 10 # seconds
- start_time = time.time()
- while not completed_spy.called and (time.time() - start_time) < timeout:
- QCoreApplication.processEvents()
- time.sleep(0.1)
-
- completed_spy.assert_called_once()
- args, kwargs = completed_spy.call_args
- assert args[0] == task
- assert len(args[1]) > 0
- assert args[1][0]["text"] == "Test transcription."
-
-
-def test_transcription_with_whisper_cpp_tiny_with_speech_extraction(worker):
- options = TranscriptionOptions(
- model=TranscriptionModel(model_type=ModelType.WHISPER_CPP, whisper_model_size=WhisperModelSize.TINY),
- extract_speech=True
- )
- 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('demucs.api.Separator') as mock_separator_class, \
- unittest.mock.patch('demucs.api.save_audio') as mock_save_audio, \
- unittest.mock.patch.object(WhisperFileTranscriber, '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"})
- mock_separator_instance.samplerate = 44100
- mock_separator_class.return_value = mock_separator_instance
-
- mock_run.side_effect = lambda: worker.current_transcriber.completed.emit([
- {"start": 0, "end": 1000, "text": "Test transcription with speech extraction."}
- ])
-
- completed_spy = unittest.mock.Mock()
- worker.task_completed.connect(completed_spy)
- worker.add_task(task)
-
- # Wait for the signal to be emitted
- timeout = 10 # seconds
- start_time = time.time()
- while not completed_spy.called and (time.time() - start_time) < timeout:
- QCoreApplication.processEvents()
- time.sleep(0.1)
-
- mock_separator_class.assert_called_once()
- mock_save_audio.assert_called_once()
- completed_spy.assert_called_once()
- 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."
\ No newline at end of file
diff --git a/tests/transcriber/openai_whisper_api_file_transcriber_test.py b/tests/transcriber/openai_whisper_api_file_transcriber_test.py
deleted file mode 100644
index 44168466..00000000
--- a/tests/transcriber/openai_whisper_api_file_transcriber_test.py
+++ /dev/null
@@ -1,128 +0,0 @@
-import os
-from unittest.mock import patch, Mock
-
-import pytest
-
-from buzz.transcriber.openai_whisper_api_file_transcriber import (
- OpenAIWhisperAPIFileTranscriber,
- append_segment,
-)
-from buzz.transcriber.transcriber import (
- FileTranscriptionTask,
- TranscriptionOptions,
- FileTranscriptionOptions,
- Segment,
-)
-
-from openai.types.audio import Transcription, Translation
-
-
-class TestAppendSegment:
- def test_valid_utf8(self):
- result = []
- success = append_segment(result, b"Hello world", 100, 200)
- assert success is True
- assert len(result) == 1
- assert result[0].start == 1000 # 100 centiseconds to ms
- assert result[0].end == 2000 # 200 centiseconds to ms
- assert result[0].text == "Hello world"
-
- def test_empty_bytes(self):
- result = []
- success = append_segment(result, b"", 100, 200)
- assert success is True
- assert len(result) == 0
-
- def test_invalid_utf8(self):
- result = []
- # Invalid UTF-8 sequence
- success = append_segment(result, b"\xff\xfe", 100, 200)
- assert success is False
- assert len(result) == 0
-
- def test_multibyte_utf8(self):
- result = []
- success = append_segment(result, "Привет".encode("utf-8"), 50, 150)
- assert success is True
- assert len(result) == 1
- assert result[0].text == "Привет"
-
-
-class TestGetValue:
- def test_get_value_from_dict(self):
- obj = {"key": "value", "number": 42}
- assert OpenAIWhisperAPIFileTranscriber.get_value(obj, "key") == "value"
- assert OpenAIWhisperAPIFileTranscriber.get_value(obj, "number") == 42
-
- def test_get_value_from_object(self):
- class TestObj:
- key = "value"
- number = 42
-
- obj = TestObj()
- assert OpenAIWhisperAPIFileTranscriber.get_value(obj, "key") == "value"
- assert OpenAIWhisperAPIFileTranscriber.get_value(obj, "number") == 42
-
- def test_get_value_missing_key_dict(self):
- obj = {"key": "value"}
- assert OpenAIWhisperAPIFileTranscriber.get_value(obj, "missing") is None
- assert OpenAIWhisperAPIFileTranscriber.get_value(obj, "missing", "default") == "default"
-
- def test_get_value_missing_attribute_object(self):
- class TestObj:
- key = "value"
-
- obj = TestObj()
- assert OpenAIWhisperAPIFileTranscriber.get_value(obj, "missing") is None
- assert OpenAIWhisperAPIFileTranscriber.get_value(obj, "missing", "default") == "default"
-
-
-class TestOpenAIWhisperAPIFileTranscriber:
- @pytest.fixture
- def mock_openai_client(self):
- with patch(
- "buzz.transcriber.openai_whisper_api_file_transcriber.OpenAI"
- ) as mock:
- return_value = {
- "text": "",
- "segments": [{"start": 0, "end": 6.56, "text": "Hello"}],
- }
- mock.return_value.audio.transcriptions.create.return_value = Transcription(
- **return_value
- )
- mock.return_value.audio.translations.create.return_value = Translation(
- **return_value
- )
- yield mock
-
- def test_transcribe(self, mock_openai_client):
- file_path = os.path.join(
- os.path.dirname(os.path.realpath(__file__)),
- "../../testdata/whisper-french.mp3",
- )
- transcriber = OpenAIWhisperAPIFileTranscriber(
- task=FileTranscriptionTask(
- file_path=file_path,
- transcription_options=(
- TranscriptionOptions(
- openai_access_token=os.getenv("OPENAI_ACCESS_TOKEN"),
- )
- ),
- file_transcription_options=(
- FileTranscriptionOptions(file_paths=[file_path])
- ),
- model_path="",
- )
- )
- mock_completed = Mock()
- transcriber.completed.connect(mock_completed)
- transcriber.run()
-
- mock_openai_client.return_value.audio.transcriptions.create.assert_called()
-
- called_segments = mock_completed.call_args[0][0]
-
- assert len(called_segments) == 1
- assert called_segments[0].start == 0
- assert called_segments[0].end == 6560
- assert called_segments[0].text == "Hello"
diff --git a/tests/transcriber/recording_transcriber_test.py b/tests/transcriber/recording_transcriber_test.py
deleted file mode 100644
index 3ceef3cc..00000000
--- a/tests/transcriber/recording_transcriber_test.py
+++ /dev/null
@@ -1,551 +0,0 @@
-import os
-import sys
-import time
-import numpy as np
-from unittest.mock import Mock, patch, MagicMock
-
-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
-from buzz.settings.recording_transcriber_mode import RecordingTranscriberMode
-from tests.mock_sounddevice import MockSoundDevice
-from tests.model_loader import get_model_path
-
-
-class TestAmplitude:
- def test_symmetric_array(self):
- arr = np.array([1.0, -1.0, 2.0, -2.0])
- amplitude = RecordingTranscriber.amplitude(arr)
- # RMS: sqrt(mean([1, 1, 4, 4])) = sqrt(2.5) ≈ 1.5811
- assert abs(amplitude - np.sqrt(2.5)) < 1e-6
-
- def test_asymmetric_array(self):
- arr = np.array([1.0, 2.0, 3.0, -1.0])
- amplitude = RecordingTranscriber.amplitude(arr)
- # RMS: sqrt(mean([1, 4, 9, 1])) = sqrt(3.75) ≈ 1.9365
- assert abs(amplitude - np.sqrt(3.75)) < 1e-6
-
- def test_all_zeros(self):
- arr = np.array([0.0, 0.0, 0.0])
- amplitude = RecordingTranscriber.amplitude(arr)
- assert amplitude == 0.0
-
- def test_all_positive(self):
- arr = np.array([1.0, 2.0, 3.0, 4.0])
- amplitude = RecordingTranscriber.amplitude(arr)
- # RMS: sqrt(mean([1, 4, 9, 16])) = sqrt(7.5) ≈ 2.7386
- assert abs(amplitude - np.sqrt(7.5)) < 1e-6
-
- def test_all_negative(self):
- arr = np.array([-1.0, -2.0, -3.0, -4.0])
- amplitude = RecordingTranscriber.amplitude(arr)
- # RMS is symmetric: same as all_positive
- assert abs(amplitude - np.sqrt(7.5)) < 1e-6
-
- def test_returns_float(self):
- arr = np.array([0.5], dtype=np.float32)
- amplitude = RecordingTranscriber.amplitude(arr)
- assert isinstance(amplitude, float)
-
-
-class TestGetDeviceSampleRate:
- def test_returns_default_16khz_when_supported(self):
- with patch("sounddevice.check_input_settings"):
- rate = RecordingTranscriber.get_device_sample_rate(None)
- assert rate == 16000
-
- def test_falls_back_to_device_default(self):
- import sounddevice
- from sounddevice import PortAudioError
-
- def raise_error(*args, **kwargs):
- raise PortAudioError("Device doesn't support 16000")
-
- device_info = {"default_samplerate": 44100}
- with patch("sounddevice.check_input_settings", side_effect=raise_error), \
- patch("sounddevice.query_devices", return_value=device_info):
- rate = RecordingTranscriber.get_device_sample_rate(0)
- assert rate == 44100
-
- def test_returns_default_when_query_fails(self):
- from sounddevice import PortAudioError
-
- def raise_error(*args, **kwargs):
- raise PortAudioError("Device doesn't support 16000")
-
- with patch("sounddevice.check_input_settings", side_effect=raise_error), \
- patch("sounddevice.query_devices", return_value=None):
- rate = RecordingTranscriber.get_device_sample_rate(0)
- assert rate == 16000
-
-
-class TestRecordingTranscriber:
-
- def test_should_transcribe(self, qtbot):
- with (patch("sounddevice.check_input_settings")):
- thread = QThread()
-
- transcription_model = TranscriptionModel(
- model_type=ModelType.WHISPER_CPP, whisper_model_size=WhisperModelSize.TINY
- )
-
- model_path = get_model_path(transcription_model)
-
- model_exe_path = os.path.join(APP_BASE_DIR, "whisper_cpp", "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
- ),
- input_device_index=0,
- sample_rate=16_000,
- model_path=model_path,
- sounddevice=MockSoundDevice(),
- )
- transcriber.moveToThread(thread)
-
- thread.started.connect(transcriber.start)
-
- transcriptions = []
-
- def on_transcription(text):
- transcriptions.append(text)
-
- transcriber.transcription.connect(on_transcription)
-
- thread.start()
- try:
- qtbot.waitUntil(lambda: len(transcriptions) == 3, timeout=120_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)
- finally:
- # Ensure cleanup runs even if waitUntil times out
- transcriber.stop_recording()
- time.sleep(10)
-
- thread.quit()
- thread.wait()
-
- # Ensure process is cleaned up
- if transcriber.process and transcriber.process.poll() is None:
- transcriber.process.terminate()
- try:
- transcriber.process.wait(timeout=2)
- except:
- pass
-
- # Process pending events to ensure cleanup
- from PyQt6.QtCore import QCoreApplication
- QCoreApplication.processEvents()
- time.sleep(0.1)
-
-
-class TestRecordingTranscriberInit:
- def test_init_default_mode(self):
- transcription_options = TranscriptionOptions(
- model=TranscriptionModel(model_type=ModelType.WHISPER_CPP),
- language="en",
- task=Task.TRANSCRIBE,
- )
-
- with patch("sounddevice.check_input_settings"):
- transcriber = RecordingTranscriber(
- transcription_options=transcription_options,
- input_device_index=0,
- sample_rate=16000,
- model_path="/fake/path",
- sounddevice=MockSoundDevice(),
- )
-
- assert transcriber.transcription_options == transcription_options
- assert transcriber.input_device_index == 0
- assert transcriber.sample_rate == 16000
- assert transcriber.model_path == "/fake/path"
- assert transcriber.n_batch_samples == 5 * 16000
- assert transcriber.keep_sample_seconds == 0.15
- assert transcriber.is_running is False
- assert transcriber.openai_client is None
-
- def test_init_append_and_correct_mode(self):
- transcription_options = TranscriptionOptions(
- model=TranscriptionModel(model_type=ModelType.WHISPER_CPP),
- language="en",
- task=Task.TRANSCRIBE,
- )
-
- with patch("sounddevice.check_input_settings"), \
- patch("buzz.transcriber.recording_transcriber.Settings") as mock_settings_class:
- # Mock settings to return APPEND_AND_CORRECT mode (index 2 in the enum)
- mock_settings_instance = MagicMock()
- mock_settings_class.return_value = mock_settings_instance
- # Return 2 for APPEND_AND_CORRECT mode (it's the third item in the enum)
- mock_settings_instance.value.return_value = 2
-
- transcriber = RecordingTranscriber(
- transcription_options=transcription_options,
- input_device_index=0,
- sample_rate=16000,
- model_path="/fake/path",
- sounddevice=MockSoundDevice(),
- )
-
- # APPEND_AND_CORRECT mode should use smaller batch size and longer keep duration
- assert transcriber.n_batch_samples == int(transcription_options.transcription_step * 16000)
- assert transcriber.keep_sample_seconds == 1.5
-
- def test_init_stores_silence_threshold(self):
- transcription_options = TranscriptionOptions(
- model=TranscriptionModel(model_type=ModelType.WHISPER_CPP),
- language="en",
- task=Task.TRANSCRIBE,
- silence_threshold=0.01,
- )
-
- with patch("sounddevice.check_input_settings"):
- transcriber = RecordingTranscriber(
- transcription_options=transcription_options,
- input_device_index=0,
- sample_rate=16000,
- model_path="/fake/path",
- sounddevice=MockSoundDevice(),
- )
-
- assert transcriber.transcription_options.silence_threshold == 0.01
-
- def test_init_uses_default_sample_rate_when_none(self):
- transcription_options = TranscriptionOptions(
- model=TranscriptionModel(model_type=ModelType.WHISPER_CPP),
- language="en",
- task=Task.TRANSCRIBE,
- )
-
- with patch("sounddevice.check_input_settings"):
- transcriber = RecordingTranscriber(
- transcription_options=transcription_options,
- input_device_index=0,
- sample_rate=None,
- model_path="/fake/path",
- sounddevice=MockSoundDevice(),
- )
-
- # Should use default whisper sample rate
- assert transcriber.sample_rate == 16000
-
-
-class TestStreamCallback:
- def test_stream_callback_adds_to_queue(self):
- transcription_options = TranscriptionOptions(
- model=TranscriptionModel(model_type=ModelType.WHISPER_CPP),
- language="en",
- task=Task.TRANSCRIBE,
- )
-
- with patch("sounddevice.check_input_settings"):
- transcriber = RecordingTranscriber(
- transcription_options=transcription_options,
- input_device_index=0,
- sample_rate=16000,
- model_path="/fake/path",
- sounddevice=MockSoundDevice(),
- )
-
- # Create test audio data
- in_data = np.array([[0.1], [0.2], [0.3], [0.4]], dtype=np.float32)
-
- initial_size = transcriber.queue.size
- transcriber.stream_callback(in_data, 4, None, None)
-
- # Queue should have grown by 4 samples
- assert transcriber.queue.size == initial_size + 4
-
- def test_stream_callback_emits_amplitude_changed(self):
- transcription_options = TranscriptionOptions(
- model=TranscriptionModel(model_type=ModelType.WHISPER_CPP),
- language="en",
- task=Task.TRANSCRIBE,
- )
-
- with patch("sounddevice.check_input_settings"):
- transcriber = RecordingTranscriber(
- transcription_options=transcription_options,
- input_device_index=0,
- sample_rate=16000,
- model_path="/fake/path",
- sounddevice=MockSoundDevice(),
- )
-
- # Mock the amplitude_changed signal
- amplitude_values = []
- transcriber.amplitude_changed.connect(lambda amp: amplitude_values.append(amp))
-
- # Create test audio data
- in_data = np.array([[0.1], [0.2], [0.3], [0.4]], dtype=np.float32)
- transcriber.stream_callback(in_data, 4, None, None)
-
- # Should have emitted one amplitude value
- assert len(amplitude_values) == 1
- assert amplitude_values[0] > 0
-
- def test_stream_callback_drops_data_when_queue_full(self):
- transcription_options = TranscriptionOptions(
- model=TranscriptionModel(model_type=ModelType.WHISPER_CPP),
- language="en",
- task=Task.TRANSCRIBE,
- )
-
- with patch("sounddevice.check_input_settings"):
- transcriber = RecordingTranscriber(
- transcription_options=transcription_options,
- input_device_index=0,
- sample_rate=16000,
- model_path="/fake/path",
- sounddevice=MockSoundDevice(),
- )
-
- # Fill the queue beyond max_queue_size
- transcriber.queue = np.ones(transcriber.max_queue_size, dtype=np.float32)
- initial_size = transcriber.queue.size
-
- # Try to add more data
- in_data = np.array([[0.1], [0.2]], dtype=np.float32)
- transcriber.stream_callback(in_data, 2, None, None)
-
- # Queue should not have grown (data was dropped)
- assert transcriber.queue.size == initial_size
-
-
-class TestStopRecording:
- def test_stop_recording_sets_is_running_false(self):
- transcription_options = TranscriptionOptions(
- model=TranscriptionModel(model_type=ModelType.WHISPER_CPP),
- language="en",
- task=Task.TRANSCRIBE,
- )
-
- with patch("sounddevice.check_input_settings"):
- transcriber = RecordingTranscriber(
- transcription_options=transcription_options,
- input_device_index=0,
- sample_rate=16000,
- model_path="/fake/path",
- sounddevice=MockSoundDevice(),
- )
-
- transcriber.is_running = True
- transcriber.stop_recording()
-
- assert transcriber.is_running is False
-
- def test_stop_recording_terminates_process(self):
- transcription_options = TranscriptionOptions(
- model=TranscriptionModel(model_type=ModelType.WHISPER_CPP),
- language="en",
- task=Task.TRANSCRIBE,
- )
-
- with patch("sounddevice.check_input_settings"):
- transcriber = RecordingTranscriber(
- transcription_options=transcription_options,
- input_device_index=0,
- sample_rate=16000,
- model_path="/fake/path",
- sounddevice=MockSoundDevice(),
- )
-
- # Mock a running process
- mock_process = MagicMock()
- mock_process.poll.return_value = None # Process is running
- transcriber.process = mock_process
-
- transcriber.stop_recording()
-
- # Process should have been terminated and waited
- mock_process.terminate.assert_called_once()
- mock_process.wait.assert_called_once_with(timeout=5)
-
- def test_stop_recording_skips_terminated_process(self):
- transcription_options = TranscriptionOptions(
- model=TranscriptionModel(model_type=ModelType.WHISPER_CPP),
- language="en",
- task=Task.TRANSCRIBE,
- )
-
- with patch("sounddevice.check_input_settings"):
- transcriber = RecordingTranscriber(
- transcription_options=transcription_options,
- input_device_index=0,
- sample_rate=16000,
- model_path="/fake/path",
- sounddevice=MockSoundDevice(),
- )
-
- # Mock an already terminated process
- mock_process = MagicMock()
- mock_process.poll.return_value = 0 # Process already terminated
- transcriber.process = mock_process
-
- transcriber.stop_recording()
-
- # terminate and wait should not be called
- mock_process.terminate.assert_not_called()
- mock_process.wait.assert_not_called()
-
-
-class TestStartLocalWhisperServer:
- def test_start_local_whisper_server_creates_openai_client(self):
- transcription_options = TranscriptionOptions(
- model=TranscriptionModel(model_type=ModelType.WHISPER_CPP),
- language="en",
- task=Task.TRANSCRIBE,
- )
-
- with patch("sounddevice.check_input_settings"), \
- patch("subprocess.Popen") as mock_popen, \
- patch("time.sleep"):
-
- # Mock a successful process
- mock_process = MagicMock()
- mock_process.poll.return_value = None # Process is running
- mock_popen.return_value = mock_process
-
- transcriber = RecordingTranscriber(
- transcription_options=transcription_options,
- input_device_index=0,
- sample_rate=16000,
- model_path="/fake/path",
- sounddevice=MockSoundDevice(),
- )
-
- try:
- transcriber.is_running = True
- transcriber.start_local_whisper_server()
-
- # Should have created an OpenAI client
- assert transcriber.openai_client is not None
- assert transcriber.process is not None
- finally:
- # Clean up to prevent QThread warnings
- transcriber.is_running = False
- transcriber.process = None
-
- def test_start_local_whisper_server_with_language(self):
- transcription_options = TranscriptionOptions(
- model=TranscriptionModel(model_type=ModelType.WHISPER_CPP),
- language="fr",
- task=Task.TRANSCRIBE,
- )
-
- with patch("sounddevice.check_input_settings"), \
- patch("subprocess.Popen") as mock_popen, \
- patch("time.sleep"):
-
- mock_process = MagicMock()
- mock_process.poll.return_value = None
- mock_popen.return_value = mock_process
-
- transcriber = RecordingTranscriber(
- transcription_options=transcription_options,
- input_device_index=0,
- sample_rate=16000,
- model_path="/fake/path",
- sounddevice=MockSoundDevice(),
- )
-
- try:
- transcriber.is_running = True
- transcriber.start_local_whisper_server()
-
- # Check that the language was passed to the command
- call_args = mock_popen.call_args
- cmd = call_args[0][0]
- assert "--language" in cmd
- assert "fr" in cmd
- finally:
- transcriber.is_running = False
- transcriber.process = None
-
- def test_start_local_whisper_server_auto_language(self):
- transcription_options = TranscriptionOptions(
- model=TranscriptionModel(model_type=ModelType.WHISPER_CPP),
- language=None,
- task=Task.TRANSCRIBE,
- )
-
- with patch("sounddevice.check_input_settings"), \
- patch("subprocess.Popen") as mock_popen, \
- patch("time.sleep"):
-
- mock_process = MagicMock()
- mock_process.poll.return_value = None
- mock_popen.return_value = mock_process
-
- transcriber = RecordingTranscriber(
- transcription_options=transcription_options,
- input_device_index=0,
- sample_rate=16000,
- model_path="/fake/path",
- sounddevice=MockSoundDevice(),
- )
-
- try:
- transcriber.is_running = True
- transcriber.start_local_whisper_server()
-
- # Check that auto language was used
- call_args = mock_popen.call_args
- cmd = call_args[0][0]
- assert "--language" in cmd
- assert "auto" in cmd
- finally:
- transcriber.is_running = False
- transcriber.process = None
-
- def test_start_local_whisper_server_handles_failure(self):
- transcription_options = TranscriptionOptions(
- model=TranscriptionModel(model_type=ModelType.WHISPER_CPP),
- language="en",
- task=Task.TRANSCRIBE,
- )
-
- with patch("sounddevice.check_input_settings"), \
- patch("subprocess.Popen") as mock_popen, \
- patch("time.sleep"):
-
- # Mock a failed process
- mock_process = MagicMock()
- mock_process.poll.return_value = 1 # Process terminated with error
- mock_process.stderr.read.return_value = b"Error loading model"
- mock_popen.return_value = mock_process
-
- transcriber = RecordingTranscriber(
- transcription_options=transcription_options,
- input_device_index=0,
- sample_rate=16000,
- model_path="/fake/path",
- sounddevice=MockSoundDevice(),
- )
-
- transcriptions = []
- transcriber.transcription.connect(lambda text: transcriptions.append(text))
-
- try:
- transcriber.is_running = True
- transcriber.start_local_whisper_server()
-
- # Should not have created a client when server failed
- assert transcriber.openai_client is None
- # Should have emitted starting and error messages
- assert len(transcriptions) >= 1
- # First message should be about starting Whisper.cpp
- assert "Whisper" in transcriptions[0]
- finally:
- transcriber.is_running = False
- transcriber.process = None
diff --git a/tests/transcriber/transcriber_test.py b/tests/transcriber/transcriber_test.py
deleted file mode 100644
index 0a6dd80c..00000000
--- a/tests/transcriber/transcriber_test.py
+++ /dev/null
@@ -1,43 +0,0 @@
-import pathlib
-
-import pytest
-
-from buzz.transcriber.file_transcriber import write_output, to_timestamp
-from buzz.transcriber.transcriber import (
- OutputFormat,
- Segment,
-)
-
-
-class TestToTimestamp:
- def test_to_timestamp(self):
- assert to_timestamp(0) == "00:00:00.000"
- assert to_timestamp(123456789) == "34:17:36.789"
-
-
-@pytest.mark.parametrize(
- "output_format,output_text",
- [
- (OutputFormat.TXT, "Bien venue dans "),
- (
- OutputFormat.SRT,
- "1\n00:00:00,040 --> 00:00:00,299\nBien\n\n2\n00:00:00,299 --> 00:00:00,329\nvenue dans\n\n",
- ),
- (
- OutputFormat.VTT,
- "WEBVTT\n\n00:00:00.040 --> 00:00:00.299\nBien\n\n00:00:00.299 --> 00:00:00.329\nvenue dans\n\n",
- ),
- ],
-)
-def test_write_output(
- tmp_path: pathlib.Path, output_format: OutputFormat, output_text: str
-):
- output_file_path = tmp_path / "whisper.txt"
- segments = [Segment(40, 299, "Bien"), Segment(299, 329, "venue dans")]
-
- write_output(
- path=str(output_file_path), segments=segments, output_format=output_format
- )
-
- with open(output_file_path, encoding="utf-8") as output_file:
- assert output_text == output_file.read()
diff --git a/tests/transcriber/transformers_whisper_test.py b/tests/transcriber/transformers_whisper_test.py
deleted file mode 100644
index ec09034e..00000000
--- a/tests/transcriber/transformers_whisper_test.py
+++ /dev/null
@@ -1,106 +0,0 @@
-import os
-import sys
-import platform
-from unittest.mock import patch
-
-import pytest
-
-from buzz.transformers_whisper import TransformersTranscriber, is_intel_mac, is_peft_model
-
-
-class TestIsIntelMac:
- @pytest.mark.parametrize(
- "sys_platform,machine,expected",
- [
- ("linux", "x86_64", False),
- ("win32", "x86_64", False),
- ("darwin", "arm64", False),
- ("darwin", "x86_64", True),
- ("darwin", "i386", False),
- ],
- )
- def test_is_intel_mac(self, sys_platform, machine, expected):
- with patch("buzz.transformers_whisper.sys.platform", sys_platform), \
- patch("buzz.transformers_whisper.platform.machine", return_value=machine):
- assert is_intel_mac() == expected
-
-
-class TestIsPeftModel:
- @pytest.mark.parametrize(
- "model_id,expected",
- [
- ("openai/whisper-tiny-peft", True),
- ("user/model-PEFT", True),
- ("openai/whisper-tiny", False),
- ("facebook/mms-1b-all", False),
- ("", False),
- ],
- )
- def test_peft_detection(self, model_id, expected):
- assert is_peft_model(model_id) == expected
-
-
-class TestGetPeftRepoId:
- def test_repo_id_returned_as_is(self):
- transcriber = TransformersTranscriber("user/whisper-tiny-peft")
- with patch("os.path.exists", return_value=False):
- assert transcriber._get_peft_repo_id() == "user/whisper-tiny-peft"
-
- def test_linux_cache_path(self):
- linux_path = "/home/user/.cache/Buzz/models/models--user--whisper-peft/snapshots/abc123"
- transcriber = TransformersTranscriber(linux_path)
- with patch("os.path.exists", return_value=True), \
- patch("buzz.transformers_whisper.os.sep", "/"):
- assert transcriber._get_peft_repo_id() == "user/whisper-peft"
-
- def test_windows_cache_path(self):
- windows_path = r"C:\Users\user\.cache\Buzz\models\models--user--whisper-peft\snapshots\abc123"
- transcriber = TransformersTranscriber(windows_path)
- with patch("os.path.exists", return_value=True), \
- patch("buzz.transformers_whisper.os.sep", "\\"):
- assert transcriber._get_peft_repo_id() == "user/whisper-peft"
-
- def test_fallback_returns_model_id(self):
- transcriber = TransformersTranscriber("some-local-model")
- with patch("os.path.exists", return_value=True):
- assert transcriber._get_peft_repo_id() == "some-local-model"
-
-
-class TestGetMmsRepoId:
- """Tests for TransformersTranscriber._get_mms_repo_id method."""
-
- def test_repo_id_returned_as_is(self):
- """Test that a HuggingFace repo ID is returned unchanged."""
- transcriber = TransformersTranscriber("facebook/mms-1b-all")
- with patch("os.path.exists", return_value=False):
- assert transcriber._get_mms_repo_id() == "facebook/mms-1b-all"
-
- def test_linux_cache_path(self):
- """Test extraction from Linux-style cache path."""
- linux_path = "/home/user/.cache/Buzz/models/models--facebook--mms-1b-all/snapshots/abc123"
- transcriber = TransformersTranscriber(linux_path)
- with patch("os.path.exists", return_value=True), \
- patch("buzz.transformers_whisper.os.sep", "/"):
- assert transcriber._get_mms_repo_id() == "facebook/mms-1b-all"
-
- def test_windows_cache_path(self):
- """Test extraction from Windows-style cache path."""
- windows_path = r"C:\Users\user\.cache\Buzz\models\models--facebook--mms-1b-all\snapshots\abc123"
- transcriber = TransformersTranscriber(windows_path)
- with patch("os.path.exists", return_value=True), \
- patch("buzz.transformers_whisper.os.sep", "\\"):
- assert transcriber._get_mms_repo_id() == "facebook/mms-1b-all"
-
- def test_fallback_returns_model_id(self):
- """Test that model_id is returned as fallback when pattern not matched."""
- transcriber = TransformersTranscriber("some-local-model")
- with patch("os.path.exists", return_value=True):
- assert transcriber._get_mms_repo_id() == "some-local-model"
-
- def test_nested_org_name(self):
- """Test extraction with different org/model names."""
- linux_path = "/home/user/.cache/Buzz/models/models--openai--whisper-large-v3/snapshots/xyz"
- transcriber = TransformersTranscriber(linux_path)
- with patch("os.path.exists", return_value=True), \
- patch("buzz.transformers_whisper.os.sep", "/"):
- assert transcriber._get_mms_repo_id() == "openai/whisper-large-v3"
diff --git a/tests/transcriber/whisper_cpp_test.py b/tests/transcriber/whisper_cpp_test.py
deleted file mode 100644
index cabc9fe7..00000000
--- a/tests/transcriber/whisper_cpp_test.py
+++ /dev/null
@@ -1,240 +0,0 @@
-from unittest.mock import patch, MagicMock, mock_open
-import json
-
-from buzz.model_loader import TranscriptionModel, ModelType, WhisperModelSize
-from buzz.transcriber.transcriber import (
- TranscriptionOptions,
- Task,
- FileTranscriptionTask,
- FileTranscriptionOptions,
-)
-from buzz.transcriber.whisper_cpp import WhisperCpp
-from tests.audio import test_audio_path, test_multibyte_utf8_audio_path
-from tests.model_loader import get_model_path
-
-
-class TestWhisperCpp:
- def test_transcribe(self):
- transcription_options = TranscriptionOptions(
- language="fr",
- task=Task.TRANSCRIBE,
- word_level_timings=False,
- model=TranscriptionModel(
- model_type=ModelType.WHISPER_CPP,
- whisper_model_size=WhisperModelSize.TINY,
- ),
- )
- model_path = get_model_path(transcription_options.model)
-
- task = FileTranscriptionTask(
- transcription_options=transcription_options,
- file_transcription_options=FileTranscriptionOptions(),
- model_path=model_path,
- file_path=test_audio_path,
- )
-
- segments = WhisperCpp.transcribe(task=task)
-
- # Combine all segment texts
- full_text = " ".join(segment.text for segment in segments)
- assert "Bien venu" in full_text or "bienvenu" in full_text.lower()
-
- def test_transcribe_word_level_timestamps(self):
- transcription_options = TranscriptionOptions(
- language="lv",
- task=Task.TRANSCRIBE,
- word_level_timings=True,
- model=TranscriptionModel(
- model_type=ModelType.WHISPER_CPP,
- whisper_model_size=WhisperModelSize.TINY,
- ),
- )
- model_path = get_model_path(transcription_options.model)
-
- task = FileTranscriptionTask(
- transcription_options=transcription_options,
- file_transcription_options=FileTranscriptionOptions(),
- model_path=model_path,
- file_path=test_multibyte_utf8_audio_path,
- )
-
- segments = WhisperCpp.transcribe(task=task)
-
- assert "Mani" in segments[0].text
- assert "uzstrau" or "ustrau" in segments[1].text
- assert "laikabstāk" in segments[2].text
-
- def test_transcribe_chinese_multibyte_word_level_timestamps(self):
- """Test that Chinese characters split across multiple tokens are properly combined.
-
- Chinese character 闻 (U+95FB) is encoded as UTF-8 bytes E9 97 BB.
- Whisper.cpp may split this into separate tokens, e.g.:
- - Token 1: bytes E9 97 (incomplete)
- - Token 2: byte BB (completes the character)
-
- The code should combine these bytes and output 闻 as a single segment.
- """
- # Mock JSON data simulating whisper.cpp output with split Chinese characters
- # The character 闻 is split into two tokens: \xe9\x97 and \xbb
- # The character 新 is a complete token
- # Together they form 新闻 (news)
- mock_json_data = {
- "transcription": [
- {
- "offsets": {"from": 0, "to": 5000},
- "text": "", # Not used in word-level processing
- "tokens": [
- {
- "text": "[_BEG_]",
- "offsets": {"from": 0, "to": 0},
- },
- {
- # 新 - complete character (UTF-8: E6 96 B0)
- # When read as latin-1: \xe6\x96\xb0
- "text": "\xe6\x96\xb0",
- "offsets": {"from": 100, "to": 200},
- },
- {
- # First two bytes of 闻 (UTF-8: E9 97 BB)
- # When read as latin-1: \xe9\x97
- "text": "\xe9\x97",
- "offsets": {"from": 200, "to": 300},
- },
- {
- # Last byte of 闻
- # When read as latin-1: \xbb
- "text": "\xbb",
- "offsets": {"from": 300, "to": 400},
- },
- {
- "text": "[_TT_500]",
- "offsets": {"from": 500, "to": 500},
- },
- ],
- }
- ]
- }
-
- # Convert to JSON string using latin-1 compatible encoding
- # We write bytes directly since the real file is read with latin-1
- json_bytes = json.dumps(mock_json_data, ensure_ascii=False).encode("latin-1")
-
- transcription_options = TranscriptionOptions(
- language="zh",
- task=Task.TRANSCRIBE,
- word_level_timings=True,
- model=TranscriptionModel(
- model_type=ModelType.WHISPER_CPP,
- whisper_model_size=WhisperModelSize.TINY,
- ),
- )
-
- task = FileTranscriptionTask(
- transcription_options=transcription_options,
- file_transcription_options=FileTranscriptionOptions(),
- model_path="/fake/model/path",
- file_path="/fake/audio.wav",
- )
-
- # Mock subprocess.Popen to simulate whisper-cli execution
- mock_process = MagicMock()
- mock_process.stderr.readline.side_effect = [""]
- mock_process.wait.return_value = None
- mock_process.returncode = 0
-
- with patch("buzz.transcriber.whisper_cpp.subprocess.Popen", return_value=mock_process):
- with patch("buzz.transcriber.whisper_cpp.os.path.exists", return_value=True):
- with patch("builtins.open", mock_open(read_data=json_bytes.decode("latin-1"))):
- segments = WhisperCpp.transcribe(task=task)
-
- # Should have 2 segments: 新 and 闻 (each character separate)
- assert len(segments) == 2
- assert segments[0].text == "新"
- assert segments[1].text == "闻"
-
- # Verify timestamps
- assert segments[0].start == 100
- assert segments[0].end == 200
- # 闻 spans from token at 200 to token ending at 400
- assert segments[1].start == 200
- assert segments[1].end == 400
-
- def test_transcribe_chinese_mixed_complete_and_split_chars(self):
- """Test a mix of complete and split Chinese characters."""
- # 大家好 - "Hello everyone"
- # 大 (E5 A4 A7) - complete token
- # 家 (E5 AE B6) - split into E5 AE and B6
- # 好 (E5 A5 BD) - complete token
- mock_json_data = {
- "transcription": [
- {
- "offsets": {"from": 0, "to": 5000},
- "text": "", # Not used in word-level processing
- "tokens": [
- {
- "text": "[_BEG_]",
- "offsets": {"from": 0, "to": 0},
- },
- {
- # 大 - complete
- "text": "\xe5\xa4\xa7",
- "offsets": {"from": 100, "to": 200},
- },
- {
- # First two bytes of 家
- "text": "\xe5\xae",
- "offsets": {"from": 200, "to": 250},
- },
- {
- # Last byte of 家
- "text": "\xb6",
- "offsets": {"from": 250, "to": 300},
- },
- {
- # 好 - complete
- "text": "\xe5\xa5\xbd",
- "offsets": {"from": 300, "to": 400},
- },
- ],
- }
- ]
- }
-
- json_bytes = json.dumps(mock_json_data, ensure_ascii=False).encode("latin-1")
-
- transcription_options = TranscriptionOptions(
- language="zh",
- task=Task.TRANSCRIBE,
- word_level_timings=True,
- model=TranscriptionModel(
- model_type=ModelType.WHISPER_CPP,
- whisper_model_size=WhisperModelSize.TINY,
- ),
- )
-
- task = FileTranscriptionTask(
- transcription_options=transcription_options,
- file_transcription_options=FileTranscriptionOptions(),
- model_path="/fake/model/path",
- file_path="/fake/audio.wav",
- )
-
- mock_process = MagicMock()
- mock_process.stderr.readline.side_effect = [""]
- mock_process.wait.return_value = None
- mock_process.returncode = 0
-
- with patch("buzz.transcriber.whisper_cpp.subprocess.Popen", return_value=mock_process):
- with patch("buzz.transcriber.whisper_cpp.os.path.exists", return_value=True):
- with patch("builtins.open", mock_open(read_data=json_bytes.decode("latin-1"))):
- segments = WhisperCpp.transcribe(task=task)
-
- # Should have 3 segments: 大, 家, 好
- assert len(segments) == 3
- assert segments[0].text == "大"
- assert segments[1].text == "家"
- assert segments[2].text == "好"
-
- # Combined text
- full_text = "".join(s.text for s in segments)
- assert full_text == "大家好"
\ No newline at end of file
diff --git a/tests/transcriber/whisper_file_transcriber_test.py b/tests/transcriber/whisper_file_transcriber_test.py
deleted file mode 100644
index 25dad30c..00000000
--- a/tests/transcriber/whisper_file_transcriber_test.py
+++ /dev/null
@@ -1,431 +0,0 @@
-import glob
-import logging
-import os
-import platform
-import shutil
-import tempfile
-import time
-from typing import List
-from unittest.mock import Mock
-
-import pytest
-from pytestqt.qtbot import QtBot
-
-from buzz.model_loader import TranscriptionModel, ModelType, WhisperModelSize
-from buzz.transcriber.transcriber import (
- OutputFormat,
- get_output_file_path,
- FileTranscriptionTask,
- TranscriptionOptions,
- Task,
- FileTranscriptionOptions,
- Segment,
-)
-from buzz.transcriber.whisper_file_transcriber import (
- WhisperFileTranscriber,
- check_file_has_audio_stream,
- PROGRESS_REGEX,
-)
-from tests.audio import test_audio_path
-from tests.model_loader import get_model_path
-
-
-class TestCheckFileHasAudioStream:
- def test_valid_audio_file(self):
- # Should not raise exception for valid audio file
- check_file_has_audio_stream(test_audio_path)
-
- def test_missing_file(self):
- with pytest.raises(ValueError, match="File not found"):
- check_file_has_audio_stream("/nonexistent/path/to/file.mp3")
-
- def test_invalid_media_file(self):
- # Create a temporary text file (not a valid media file)
- temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".mp3")
- try:
- temp_file.write(b"This is not a valid media file")
- temp_file.close()
- with pytest.raises(ValueError, match="Invalid media file"):
- check_file_has_audio_stream(temp_file.name)
- finally:
- os.unlink(temp_file.name)
-
-
-class TestProgressRegex:
- def test_integer_percentage(self):
- match = PROGRESS_REGEX.search("Progress: 50%")
- assert match is not None
- assert match.group() == "50%"
-
- def test_decimal_percentage(self):
- match = PROGRESS_REGEX.search("Progress: 75.5%")
- assert match is not None
- assert match.group() == "75.5%"
-
- def test_no_match(self):
- match = PROGRESS_REGEX.search("No percentage here")
- assert match is None
-
- def test_extract_percentage_value(self):
- line = "Transcription progress: 85%"
- match = PROGRESS_REGEX.search(line)
- assert match is not None
- percentage = int(match.group().strip("%"))
- assert percentage == 85
-
-
-class TestWhisperFileTranscriber:
- @pytest.mark.parametrize(
- "file_path,output_format,expected_file_path",
- [
- pytest.param(
- "/a/b/c.mp4",
- OutputFormat.SRT,
- "/a/b/c-translate--Whisper-tiny.srt",
- marks=pytest.mark.skipif(platform.system() == "Windows", reason=""),
- ),
- pytest.param(
- "C:\\a\\b\\c.mp4",
- OutputFormat.SRT,
- "C:\\a\\b\\c-translate--Whisper-tiny.srt",
- marks=pytest.mark.skipif(platform.system() != "Windows", reason=""),
- ),
- ],
- )
- def test_default_output_file(
- self,
- file_path: str,
- output_format: OutputFormat,
- expected_file_path: str,
- ):
- file_path = get_output_file_path(
- file_path=file_path,
- language=None,
- task=Task.TRANSLATE,
- model=TranscriptionModel(
- model_type=ModelType.WHISPER,
- whisper_model_size=WhisperModelSize.TINY,
- ),
- output_format=output_format,
- output_directory="",
- export_file_name_template="{{ input_file_name }}-{{ task }}-{{ language }}-{{ model_type }}-{{ model_size }}",
- )
- assert file_path == expected_file_path
-
- @pytest.mark.parametrize(
- "file_path,expected_starts_with",
- [
- pytest.param(
- "/a/b/c.mp4",
- "/a/b/c (Translated on ",
- marks=pytest.mark.skipif(platform.system() == "Windows", reason=""),
- ),
- pytest.param(
- "C:\\a\\b\\c.mp4",
- "C:\\a\\b\\c (Translated on ",
- marks=pytest.mark.skipif(platform.system() != "Windows", reason=""),
- ),
- ],
- )
- def test_default_output_file_with_date(
- self, file_path: str, expected_starts_with: str
- ):
- export_file_name_template = (
- "{{ input_file_name }} (Translated on {{ date_time }})"
- )
- srt = get_output_file_path(
- file_path=file_path,
- language=None,
- task=Task.TRANSLATE,
- model=TranscriptionModel(
- model_type=ModelType.WHISPER,
- whisper_model_size=WhisperModelSize.TINY,
- ),
- output_format=OutputFormat.TXT,
- output_directory="",
- export_file_name_template=export_file_name_template,
- )
-
- assert srt.startswith(expected_starts_with)
- assert srt.endswith(".txt")
-
- srt = get_output_file_path(
- file_path=file_path,
- language=None,
- task=Task.TRANSLATE,
- model=TranscriptionModel(
- model_type=ModelType.WHISPER,
- whisper_model_size=WhisperModelSize.TINY,
- ),
- output_format=OutputFormat.SRT,
- output_directory="",
- export_file_name_template=export_file_name_template,
- )
- assert srt.startswith(expected_starts_with)
- assert srt.endswith(".srt")
-
- @pytest.mark.parametrize(
- "word_level_timings,extract_speech,expected_segments,model",
- [
- (
- False,
- False,
- [
- Segment(
- 0,
- 8400,
- " Bienvenue dans Passe-Relle. Un podcast pensé pour évêiller",
- )
- ],
- TranscriptionModel(
- model_type=ModelType.WHISPER,
- whisper_model_size=WhisperModelSize.TINY,
- ),
- ),
- (
- True,
- True,
- [Segment(40, 299, " Bien"), Segment(299, 329, "venue dans")],
- TranscriptionModel(
- model_type=ModelType.WHISPER,
- whisper_model_size=WhisperModelSize.TINY,
- ),
- ),
- (
- False,
- False,
- [
- Segment(
- 0,
- 8517,
- " Bienvenue dans Passe-Relle. Un podcast pensé pour évêyer la curiosité des apprenances "
- "et des apprenances de français.",
- )
- ],
- TranscriptionModel(
- model_type=ModelType.HUGGING_FACE,
- hugging_face_model_id="openai/whisper-tiny",
- ),
- ),
- pytest.param(
- False,
- False,
- [
- Segment(
- start=0,
- end=8400,
- text=" Bienvenue dans Passrel, un podcast pensé pour éveiller la curiosité des apprenances et des apprenances de français.",
- )
- ],
- TranscriptionModel(
- model_type=ModelType.FASTER_WHISPER,
- whisper_model_size=WhisperModelSize.TINY,
- ),
- marks=pytest.mark.skipif(
- platform.system() == "Darwin" and platform.machine() == "x86_64",
- reason="Error with libiomp5 already initialized on GH action runner: https://github.com/chidiwilliams/buzz/actions/runs/4657331262/jobs/8241832087",
- ),
- ),
- ],
- )
- def test_transcribe_from_file(
- self,
- qtbot: QtBot,
- word_level_timings: bool,
- extract_speech: bool,
- expected_segments: List[Segment],
- model: TranscriptionModel,
- ):
- mock_progress = Mock()
- mock_completed = Mock()
- transcription_options = TranscriptionOptions(
- language="fr",
- task=Task.TRANSCRIBE,
- word_level_timings=word_level_timings,
- extract_speech=extract_speech,
- model=model,
- )
- model_path = get_model_path(transcription_options.model)
- file_path = os.path.abspath(test_audio_path)
- file_transcription_options = FileTranscriptionOptions(file_paths=[file_path])
-
- transcriber = WhisperFileTranscriber(
- task=FileTranscriptionTask(
- transcription_options=transcription_options,
- file_transcription_options=file_transcription_options,
- file_path=file_path,
- model_path=model_path,
- )
- )
- transcriber.progress.connect(mock_progress)
- transcriber.completed.connect(mock_completed)
- with qtbot.wait_signal(
- transcriber.progress, timeout=10 * 6000
- ), qtbot.wait_signal(transcriber.completed, timeout=10 * 6000):
- transcriber.run()
-
- # Reports progress at 0, 0 <= progress <= 100, and 100
- assert mock_progress.call_count >= 2
- assert mock_progress.call_args_list[0][0][0] == (0, 100)
-
- mock_completed.assert_called()
- segments = mock_completed.call_args[0][0]
- assert len(segments) >= 0
- for i, expected_segment in enumerate(segments):
- assert segments[i].start >= 0
- assert segments[i].end > 0
- 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"
- )
-
- mock_progress = Mock()
- mock_completed = Mock()
- transcription_options = TranscriptionOptions()
- model_path = get_model_path(transcription_options.model)
- file_transcription_options = FileTranscriptionOptions(url=url)
-
- transcriber = WhisperFileTranscriber(
- task=FileTranscriptionTask(
- transcription_options=transcription_options,
- file_transcription_options=file_transcription_options,
- model_path=model_path,
- url=url,
- source=FileTranscriptionTask.Source.URL_IMPORT,
- )
- )
- transcriber.progress.connect(mock_progress)
- transcriber.completed.connect(mock_completed)
- with qtbot.wait_signal(
- transcriber.progress, timeout=10 * 6000
- ), qtbot.wait_signal(transcriber.completed, timeout=10 * 6000):
- transcriber.run()
-
- # Reports progress at 0, 0 <= progress <= 100, and 100
- assert mock_progress.call_count >= 2
- assert mock_progress.call_args_list[0][0][0] == (0, 100)
-
- mock_completed.assert_called()
- segments = mock_completed.call_args[0][0]
- assert len(segments) >= 0
- for i, expected_segment in enumerate(segments):
- assert segments[i].start >= 0
- assert segments[i].end > 0
- 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)
-
- file_transcription_options = FileTranscriptionOptions(
- file_paths=[file_path],
- output_formats={OutputFormat.TXT},
- )
- transcription_options = TranscriptionOptions()
- model_path = get_model_path(transcription_options.model)
-
- output_directory = tempfile.mkdtemp()
- transcriber = WhisperFileTranscriber(
- task=FileTranscriptionTask(
- model_path=model_path,
- transcription_options=transcription_options,
- file_transcription_options=file_transcription_options,
- file_path=file_path,
- output_directory=output_directory,
- source=FileTranscriptionTask.Source.FOLDER_WATCH,
- )
- )
- with qtbot.wait_signal(transcriber.completed, timeout=10 * 6000):
- transcriber.run()
-
- assert not os.path.isfile(file_path)
- assert os.path.isfile(
- os.path.join(output_directory, os.path.basename(file_path))
- )
- assert len(glob.glob("*.txt", root_dir=output_directory)) > 0
-
- transcriber.stop()
- time.sleep(3)
-
- def test_transcribe_from_folder_watch_source_deletes_file(self, qtbot):
- file_path = tempfile.mktemp(suffix=".mp3")
- shutil.copy(test_audio_path, file_path)
-
- file_transcription_options = FileTranscriptionOptions(
- file_paths=[file_path],
- output_formats={OutputFormat.TXT},
- )
- transcription_options = TranscriptionOptions()
- model_path = get_model_path(transcription_options.model)
-
- output_directory = tempfile.mkdtemp()
- transcriber = WhisperFileTranscriber(
- task=FileTranscriptionTask(
- model_path=model_path,
- transcription_options=transcription_options,
- file_transcription_options=file_transcription_options,
- file_path=file_path,
- original_file_path=file_path,
- output_directory=output_directory,
- source=FileTranscriptionTask.Source.FOLDER_WATCH,
- delete_source_file=True,
- )
- )
- with qtbot.wait_signal(transcriber.completed, timeout=10 * 6000):
- transcriber.run()
-
- assert not os.path.isfile(file_path)
- assert not os.path.isfile(
- os.path.join(output_directory, os.path.basename(file_path))
- )
- 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")
- if os.path.exists(output_file_path):
- os.remove(output_file_path)
-
- file_transcription_options = FileTranscriptionOptions(
- file_paths=[test_audio_path]
- )
- transcription_options = TranscriptionOptions(
- language="fr",
- task=Task.TRANSCRIBE,
- word_level_timings=False,
- model=TranscriptionModel(
- model_type=ModelType.WHISPER_CPP,
- whisper_model_size=WhisperModelSize.TINY,
- ),
- )
- model_path = get_model_path(transcription_options.model)
-
- transcriber = WhisperFileTranscriber(
- task=FileTranscriptionTask(
- model_path=model_path,
- transcription_options=transcription_options,
- file_transcription_options=file_transcription_options,
- file_path=test_audio_path,
- )
- )
- transcriber.run()
- time.sleep(1)
- transcriber.stop()
-
- # Assert that file was not created
- assert os.path.isfile(output_file_path) is False
-
- time.sleep(3)
\ No newline at end of file
diff --git a/tests/transformers_transcriber_test.py b/tests/transformers_transcriber_test.py
deleted file mode 100644
index dabf1714..00000000
--- a/tests/transformers_transcriber_test.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import platform
-import pytest
-
-from buzz.transformers_whisper import TransformersTranscriber
-from tests.audio import test_audio_path
-
-
-class TestTransformersTranscriber:
- @pytest.mark.skipif(
- platform.system() == "Darwin",
- reason="Not supported on Darwin",
- )
- def test_should_transcribe(self):
- model = TransformersTranscriber("openai/whisper-tiny")
- result = model.transcribe(
- audio=test_audio_path, language="fr", task="transcribe"
- )
-
- assert "Bienvenue dans Passrel" in result["text"]
diff --git a/tests/translator_test.py b/tests/translator_test.py
deleted file mode 100644
index 6380dc51..00000000
--- a/tests/translator_test.py
+++ /dev/null
@@ -1,170 +0,0 @@
-import time
-import pytest
-from queue import Empty
-from unittest.mock import Mock, patch, create_autospec
-
-from PyQt6.QtCore import QThread
-
-from buzz.translator import Translator
-from buzz.transcriber.transcriber import TranscriptionOptions
-from buzz.widgets.transcriber.advanced_settings_dialog import AdvancedSettingsDialog
-from buzz.locale import _
-
-
-class TestParseBatchResponse:
- def test_simple_batch(self):
- response = "[1] Hello\n[2] World"
- result = Translator._parse_batch_response(response, 2)
- assert len(result) == 2
- assert result[0] == "Hello"
- assert result[1] == "World"
-
- def test_missing_entries_fallback(self):
- response = "[1] Hello\n[3] World"
- result = Translator._parse_batch_response(response, 3)
- assert len(result) == 3
- assert result[0] == "Hello"
- assert result[1] == ""
- assert result[2] == "World"
-
- def test_multiline_entries(self):
- response = "[1] This is a long\nmultiline translation\n[2] Short"
- result = Translator._parse_batch_response(response, 2)
- assert len(result) == 2
- assert "multiline" in result[0]
- assert result[1] == "Short"
-
- def test_single_item_batch(self):
- response = "[1] Single translation"
- result = Translator._parse_batch_response(response, 1)
- assert len(result) == 1
- assert result[0] == "Single translation"
-
- def test_empty_response(self):
- response = ""
- result = Translator._parse_batch_response(response, 2)
- assert len(result) == 2
- assert result[0] == ""
- assert result[1] == ""
-
- def test_whitespace_handling(self):
- response = "[1] Hello with spaces \n[2] World "
- result = Translator._parse_batch_response(response, 2)
- assert result[0] == "Hello with spaces"
- assert result[1] == "World"
-
- def test_out_of_order_entries(self):
- response = "[2] Second\n[1] First"
- result = Translator._parse_batch_response(response, 2)
- assert result[0] == "First"
- assert result[1] == "Second"
-
-
-class TestTranslator:
- @patch('buzz.translator.OpenAI', autospec=True)
- @patch('buzz.translator.queue.Queue', autospec=True)
- def test_start(self, mock_queue, mock_openai, qtbot):
- def side_effect(*args, **kwargs):
- if side_effect.call_count <= 1:
- side_effect.call_count += 1
- return ("Hello, how are you?", 1)
-
- # Finally return sentinel to stop
- return None
-
- side_effect.call_count = 0
-
- mock_queue.get.side_effect = side_effect
- mock_queue.get_nowait.side_effect = Empty
- mock_chat = Mock()
- mock_openai.return_value.chat = mock_chat
- mock_chat.completions.create.return_value = Mock(
- choices=[Mock(message=Mock(content="AI Translated: Hello, how are you?"))]
- )
-
- transcription_options = TranscriptionOptions(
- enable_llm_translation=False,
- llm_model="llama3",
- llm_prompt="Please translate this text:",
- )
- translator = Translator(
- transcription_options,
- AdvancedSettingsDialog(
- transcription_options=transcription_options, parent=None
- )
- )
- translator.queue = mock_queue
-
- translator.start()
-
- mock_queue.get.assert_called()
- mock_chat.completions.create.assert_called()
-
- translator.stop()
-
- @patch('buzz.translator.OpenAI', autospec=True)
- def test_translator(self, mock_openai, qtbot):
-
- self.on_next_translation_called = False
-
- def on_next_translation(text: str):
- self.on_next_translation_called = True
- assert text.startswith("AI Translated:")
-
- mock_chat = Mock()
- mock_openai.return_value.chat = mock_chat
- mock_chat.completions.create.return_value = Mock(
- choices=[Mock(message=Mock(content="AI Translated: Hello, how are you?"))]
- )
-
- self.translation_thread = QThread()
- self.transcription_options = TranscriptionOptions(
- enable_llm_translation=False,
- llm_model="llama3",
- llm_prompt="Please translate this text:",
- )
-
- self.translator = Translator(
- self.transcription_options,
- AdvancedSettingsDialog(
- transcription_options=self.transcription_options, parent=None
- )
- )
-
- self.translator.moveToThread(self.translation_thread)
-
- self.translation_thread.started.connect(self.translator.start)
- self.translation_thread.finished.connect(
- self.translation_thread.deleteLater
- )
-
- self.translator.finished.connect(self.translation_thread.quit)
- self.translator.finished.connect(self.translator.deleteLater)
-
- self.translator.translation.connect(on_next_translation)
-
- self.translation_thread.start()
-
- time.sleep(1) # Give thread time to start
-
- self.translator.enqueue("Hello, how are you?")
-
- def translation_signal_received():
- assert self.on_next_translation_called
-
- qtbot.wait_until(translation_signal_received, timeout=60 * 1000)
-
- if self.translator is not None:
- self.translator.stop()
-
- if self.translation_thread is not None:
- self.translation_thread.quit()
- # Wait for the thread to actually finish before cleanup
- self.translation_thread.wait()
- # Process pending events to ensure deleteLater() is handled
- from PyQt6.QtCore import QCoreApplication
- QCoreApplication.processEvents()
- time.sleep(0.1) # Give time for cleanup
-
- # Note: translator and translation_thread will be automatically deleted
- # via the deleteLater() connections set up earlier
diff --git a/tests/update_checker_test.py b/tests/update_checker_test.py
deleted file mode 100644
index 021935b0..00000000
--- a/tests/update_checker_test.py
+++ /dev/null
@@ -1,202 +0,0 @@
-import platform
-from datetime import datetime, timedelta
-from unittest.mock import patch
-
-import pytest
-from pytestqt.qtbot import QtBot
-
-from buzz.__version__ import VERSION
-from buzz.settings.settings import Settings
-from buzz.update_checker import UpdateChecker, UpdateInfo
-from tests.mock_qt import MockNetworkAccessManager, MockNetworkReply
-
-
-VERSION_INFO = {
- "version": "99.0.0",
- "release_notes": "Some fixes.",
- "download_urls": {
- "windows_x64": ["https://example.com/Buzz-99.0.0.exe"],
- "macos_arm": ["https://example.com/Buzz-99.0.0-arm.dmg"],
- "macos_x86": ["https://example.com/Buzz-99.0.0-x86.dmg"],
- },
-}
-
-
-@pytest.fixture()
-def checker(settings: Settings) -> UpdateChecker:
- reply = MockNetworkReply(data=VERSION_INFO)
- manager = MockNetworkAccessManager(reply=reply)
- return UpdateChecker(settings=settings, network_manager=manager)
-
-
-class TestShouldCheckForUpdates:
- def test_returns_false_on_linux(self, checker: UpdateChecker):
- with patch.object(platform, "system", return_value="Linux"):
- assert checker.should_check_for_updates() is False
-
- def test_returns_true_on_windows_first_run(self, checker: UpdateChecker, settings: Settings):
- settings.set_value(Settings.Key.LAST_UPDATE_CHECK, "")
- with patch.object(platform, "system", return_value="Windows"):
- assert checker.should_check_for_updates() is True
-
- def test_returns_true_on_macos_first_run(self, checker: UpdateChecker, settings: Settings):
- settings.set_value(Settings.Key.LAST_UPDATE_CHECK, "")
- with patch.object(platform, "system", return_value="Darwin"):
- assert checker.should_check_for_updates() is True
-
- def test_returns_false_when_checked_recently(
- self, checker: UpdateChecker, settings: Settings
- ):
- recent = (datetime.now() - timedelta(days=2)).isoformat()
- settings.set_value(Settings.Key.LAST_UPDATE_CHECK, recent)
-
- with patch.object(platform, "system", return_value="Windows"):
- assert checker.should_check_for_updates() is False
-
- def test_returns_true_when_check_is_overdue(
- self, checker: UpdateChecker, settings: Settings
- ):
- old = (datetime.now() - timedelta(days=10)).isoformat()
- settings.set_value(Settings.Key.LAST_UPDATE_CHECK, old)
-
- with patch.object(platform, "system", return_value="Windows"):
- assert checker.should_check_for_updates() is True
-
- def test_returns_true_on_invalid_date_in_settings(
- self, checker: UpdateChecker, settings: Settings
- ):
- settings.set_value(Settings.Key.LAST_UPDATE_CHECK, "not-a-date")
-
- with patch.object(platform, "system", return_value="Windows"):
- assert checker.should_check_for_updates() is True
-
-
-class TestIsNewerVersion:
- def test_newer_major(self, checker: UpdateChecker):
- with patch("buzz.update_checker.VERSION", "1.0.0"):
- assert checker._is_newer_version("2.0.0") is True
-
- def test_newer_minor(self, checker: UpdateChecker):
- with patch("buzz.update_checker.VERSION", "1.0.0"):
- assert checker._is_newer_version("1.1.0") is True
-
- def test_newer_patch(self, checker: UpdateChecker):
- with patch("buzz.update_checker.VERSION", "1.0.0"):
- assert checker._is_newer_version("1.0.1") is True
-
- def test_same_version(self, checker: UpdateChecker):
- with patch("buzz.update_checker.VERSION", "1.0.0"):
- assert checker._is_newer_version("1.0.0") is False
-
- def test_older_version(self, checker: UpdateChecker):
- with patch("buzz.update_checker.VERSION", "2.0.0"):
- assert checker._is_newer_version("1.9.9") is False
-
- def test_different_segment_count(self, checker: UpdateChecker):
- with patch("buzz.update_checker.VERSION", "1.0"):
- assert checker._is_newer_version("1.0.1") is True
-
- def test_invalid_version_returns_false(self, checker: UpdateChecker):
- with patch("buzz.update_checker.VERSION", "1.0.0"):
- assert checker._is_newer_version("not-a-version") is False
-
-
-class TestGetDownloadUrl:
- def test_windows_returns_windows_urls(self, checker: UpdateChecker):
- with patch.object(platform, "system", return_value="Windows"):
- urls = checker._get_download_url(VERSION_INFO["download_urls"])
- assert urls == ["https://example.com/Buzz-99.0.0.exe"]
-
- def test_macos_arm_returns_arm_urls(self, checker: UpdateChecker):
- with patch.object(platform, "system", return_value="Darwin"), \
- patch.object(platform, "machine", return_value="arm64"):
- urls = checker._get_download_url(VERSION_INFO["download_urls"])
- assert urls == ["https://example.com/Buzz-99.0.0-arm.dmg"]
-
- def test_macos_x86_returns_x86_urls(self, checker: UpdateChecker):
- with patch.object(platform, "system", return_value="Darwin"), \
- patch.object(platform, "machine", return_value="x86_64"):
- urls = checker._get_download_url(VERSION_INFO["download_urls"])
- assert urls == ["https://example.com/Buzz-99.0.0-x86.dmg"]
-
- def test_linux_returns_empty(self, checker: UpdateChecker):
- with patch.object(platform, "system", return_value="Linux"):
- urls = checker._get_download_url(VERSION_INFO["download_urls"])
- assert urls == []
-
- def test_wraps_plain_string_in_list(self, checker: UpdateChecker):
- with patch.object(platform, "system", return_value="Windows"):
- urls = checker._get_download_url({"windows_x64": "https://example.com/a.exe"})
- assert urls == ["https://example.com/a.exe"]
-
-
-class TestCheckForUpdates:
- def _make_checker(self, settings: Settings, version_data: dict) -> UpdateChecker:
- settings.set_value(Settings.Key.LAST_UPDATE_CHECK, "")
- reply = MockNetworkReply(data=version_data)
- manager = MockNetworkAccessManager(reply=reply)
- return UpdateChecker(settings=settings, network_manager=manager)
-
- def test_emits_update_available_when_newer_version(self, settings: Settings):
- received = []
- checker = self._make_checker(settings, VERSION_INFO)
- checker.update_available.connect(lambda info: received.append(info))
-
- with patch.object(platform, "system", return_value="Windows"), \
- patch.object(platform, "machine", return_value="x86_64"), \
- patch("buzz.update_checker.VERSION", "1.0.0"):
- checker.check_for_updates()
-
- assert len(received) == 1
- update_info: UpdateInfo = received[0]
- assert update_info.version == "99.0.0"
- assert update_info.release_notes == "Some fixes."
- assert update_info.download_urls == ["https://example.com/Buzz-99.0.0.exe"]
-
- def test_does_not_emit_when_version_is_current(self, settings: Settings):
- received = []
- checker = self._make_checker(settings, {**VERSION_INFO, "version": VERSION})
- checker.update_available.connect(lambda info: received.append(info))
-
- with patch.object(platform, "system", return_value="Windows"):
- checker.check_for_updates()
-
- assert received == []
-
- def test_skips_network_call_on_linux(self, settings: Settings):
- received = []
- checker = self._make_checker(settings, VERSION_INFO)
- checker.update_available.connect(lambda info: received.append(info))
-
- with patch.object(platform, "system", return_value="Linux"):
- checker.check_for_updates()
-
- assert received == []
-
- def test_stores_last_check_date_after_reply(self, settings: Settings):
- checker = self._make_checker(settings, {**VERSION_INFO, "version": VERSION})
-
- with patch.object(platform, "system", return_value="Windows"):
- checker.check_for_updates()
-
- stored = settings.value(Settings.Key.LAST_UPDATE_CHECK, "")
- assert stored != ""
- datetime.fromisoformat(stored) # should not raise
-
- def test_stores_available_version_when_update_found(self, settings: Settings):
- checker = self._make_checker(settings, VERSION_INFO)
-
- with patch.object(platform, "system", return_value="Windows"), \
- patch("buzz.update_checker.VERSION", "1.0.0"):
- checker.check_for_updates()
-
- assert settings.value(Settings.Key.UPDATE_AVAILABLE_VERSION, "") == "99.0.0"
-
- def test_clears_available_version_when_up_to_date(self, settings: Settings):
- settings.set_value(Settings.Key.UPDATE_AVAILABLE_VERSION, "99.0.0")
- checker = self._make_checker(settings, {**VERSION_INFO, "version": VERSION})
-
- with patch.object(platform, "system", return_value="Windows"):
- checker.check_for_updates()
-
- assert settings.value(Settings.Key.UPDATE_AVAILABLE_VERSION, "") == ""
diff --git a/tests/widgets/__init__.py b/tests/widgets/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/tests/widgets/advanced_settings_dialog_test.py b/tests/widgets/advanced_settings_dialog_test.py
deleted file mode 100644
index 0809b550..00000000
--- a/tests/widgets/advanced_settings_dialog_test.py
+++ /dev/null
@@ -1,153 +0,0 @@
-import pytest
-from pytestqt.qtbot import QtBot
-
-from buzz.transcriber.transcriber import TranscriptionOptions
-from buzz.widgets.transcriber.advanced_settings_dialog import AdvancedSettingsDialog
-
-
-class TestAdvancedSettingsDialogSilenceThreshold:
- def test_silence_threshold_spinbox_hidden_by_default(self, qtbot: QtBot):
- """Silence threshold UI is not shown when show_recording_settings=False."""
- options = TranscriptionOptions()
- dialog = AdvancedSettingsDialog(transcription_options=options)
- qtbot.add_widget(dialog)
- assert not hasattr(dialog, "silence_threshold_spin_box")
-
- def test_silence_threshold_spinbox_shown_when_recording_settings(self, qtbot: QtBot):
- """Silence threshold spinbox is present when show_recording_settings=True."""
- options = TranscriptionOptions()
- dialog = AdvancedSettingsDialog(
- transcription_options=options, show_recording_settings=True
- )
- qtbot.add_widget(dialog)
- assert hasattr(dialog, "silence_threshold_spin_box")
- assert dialog.silence_threshold_spin_box is not None
-
- def test_silence_threshold_spinbox_initial_value(self, qtbot: QtBot):
- """Spinbox reflects the current silence_threshold from options."""
- options = TranscriptionOptions(silence_threshold=0.0075)
- dialog = AdvancedSettingsDialog(
- transcription_options=options, show_recording_settings=True
- )
- qtbot.add_widget(dialog)
- assert dialog.silence_threshold_spin_box.value() == pytest.approx(0.0075)
-
- def test_silence_threshold_change_updates_options(self, qtbot: QtBot):
- """Changing spinbox value updates transcription_options.silence_threshold."""
- options = TranscriptionOptions(silence_threshold=0.0025)
- dialog = AdvancedSettingsDialog(
- transcription_options=options, show_recording_settings=True
- )
- qtbot.add_widget(dialog)
- dialog.silence_threshold_spin_box.setValue(0.005)
- assert dialog.transcription_options.silence_threshold == pytest.approx(0.005)
-
- def test_silence_threshold_change_emits_signal(self, qtbot: QtBot):
- """Changing the spinbox emits transcription_options_changed."""
- options = TranscriptionOptions(silence_threshold=0.0025)
- dialog = AdvancedSettingsDialog(
- transcription_options=options, show_recording_settings=True
- )
- qtbot.add_widget(dialog)
-
- emitted = []
- dialog.transcription_options_changed.connect(lambda o: emitted.append(o))
-
- dialog.silence_threshold_spin_box.setValue(0.005)
-
- assert len(emitted) == 1
- assert emitted[0].silence_threshold == pytest.approx(0.005)
-
-
-class TestAdvancedSettingsDialogLineSeparator:
- def test_line_separator_shown_when_recording_settings(self, qtbot: QtBot):
- options = TranscriptionOptions()
- dialog = AdvancedSettingsDialog(
- transcription_options=options, show_recording_settings=True
- )
- qtbot.add_widget(dialog)
- assert hasattr(dialog, "line_separator_line_edit")
- assert dialog.line_separator_line_edit is not None
-
- def test_line_separator_hidden_by_default(self, qtbot: QtBot):
- options = TranscriptionOptions()
- dialog = AdvancedSettingsDialog(transcription_options=options)
- qtbot.add_widget(dialog)
- assert not hasattr(dialog, "line_separator_line_edit")
-
- def test_line_separator_initial_value_displayed_as_escape(self, qtbot: QtBot):
- options = TranscriptionOptions(line_separator="\n\n")
- dialog = AdvancedSettingsDialog(
- transcription_options=options, show_recording_settings=True
- )
- qtbot.add_widget(dialog)
- assert dialog.line_separator_line_edit.text() == r"\n\n"
-
- def test_line_separator_change_updates_options(self, qtbot: QtBot):
- options = TranscriptionOptions(line_separator="\n\n")
- dialog = AdvancedSettingsDialog(
- transcription_options=options, show_recording_settings=True
- )
- qtbot.add_widget(dialog)
- dialog.line_separator_line_edit.setText(r"\n")
- assert dialog.transcription_options.line_separator == "\n"
-
- def test_line_separator_change_emits_signal(self, qtbot: QtBot):
- options = TranscriptionOptions(line_separator="\n\n")
- dialog = AdvancedSettingsDialog(
- transcription_options=options, show_recording_settings=True
- )
- qtbot.add_widget(dialog)
- emitted = []
- dialog.transcription_options_changed.connect(lambda o: emitted.append(o))
- dialog.line_separator_line_edit.setText(r"\n")
- assert len(emitted) == 1
- assert emitted[0].line_separator == "\n"
-
- def test_line_separator_invalid_escape_does_not_crash(self, qtbot: QtBot):
- options = TranscriptionOptions(line_separator="\n\n")
- dialog = AdvancedSettingsDialog(
- transcription_options=options, show_recording_settings=True
- )
- qtbot.add_widget(dialog)
- dialog.line_separator_line_edit.setText("\\")
- # Options unchanged — previous valid value kept
- assert dialog.transcription_options.line_separator == "\n\n"
-
- def test_line_separator_tab_character(self, qtbot: QtBot):
- options = TranscriptionOptions()
- dialog = AdvancedSettingsDialog(
- transcription_options=options, show_recording_settings=True
- )
- qtbot.add_widget(dialog)
- dialog.line_separator_line_edit.setText(r"\t")
- assert dialog.transcription_options.line_separator == "\t"
-
- def test_line_separator_plain_text(self, qtbot: QtBot):
- options = TranscriptionOptions()
- dialog = AdvancedSettingsDialog(
- transcription_options=options, show_recording_settings=True
- )
- qtbot.add_widget(dialog)
- dialog.line_separator_line_edit.setText(" | ")
- assert dialog.transcription_options.line_separator == " | "
-
-
-class TestTranscriptionOptionsLineSeparator:
- def test_default_line_separator(self):
- options = TranscriptionOptions()
- assert options.line_separator == "\n\n"
-
- def test_custom_line_separator(self):
- options = TranscriptionOptions(line_separator="\n")
- assert options.line_separator == "\n"
-
-
-class TestTranscriptionOptionsSilenceThreshold:
- def test_default_silence_threshold(self):
- options = TranscriptionOptions()
- assert options.silence_threshold == pytest.approx(0.0025)
-
- def test_custom_silence_threshold(self):
- options = TranscriptionOptions(silence_threshold=0.01)
- assert options.silence_threshold == pytest.approx(0.01)
diff --git a/tests/widgets/audio_meter_widget_test.py b/tests/widgets/audio_meter_widget_test.py
deleted file mode 100644
index d91e5d70..00000000
--- a/tests/widgets/audio_meter_widget_test.py
+++ /dev/null
@@ -1,56 +0,0 @@
-import pytest
-from pytestqt.qtbot import QtBot
-
-from buzz.widgets.audio_meter_widget import AudioMeterWidget
-
-
-class TestAudioMeterWidget:
- def test_initial_amplitude_is_zero(self, qtbot: QtBot):
- widget = AudioMeterWidget()
- qtbot.add_widget(widget)
- assert widget.current_amplitude == 0.0
-
- def test_initial_average_amplitude_is_zero(self, qtbot: QtBot):
- widget = AudioMeterWidget()
- qtbot.add_widget(widget)
- assert widget.average_amplitude == 0.0
-
- def test_update_amplitude(self, qtbot: QtBot):
- widget = AudioMeterWidget()
- qtbot.add_widget(widget)
- widget.update_amplitude(0.5)
- assert widget.current_amplitude == pytest.approx(0.5)
-
- def test_update_amplitude_smoothing(self, qtbot: QtBot):
- """Lower amplitude should decay via smoothing factor, not drop instantly."""
- widget = AudioMeterWidget()
- qtbot.add_widget(widget)
- widget.update_amplitude(1.0)
- widget.update_amplitude(0.0)
- # current_amplitude should be smoothed: max(0.0, 1.0 * SMOOTHING_FACTOR)
- assert widget.current_amplitude == pytest.approx(1.0 * widget.SMOOTHING_FACTOR)
-
- def test_update_average_amplitude(self, qtbot: QtBot):
- widget = AudioMeterWidget()
- qtbot.add_widget(widget)
- widget.update_average_amplitude(0.0123)
- assert widget.average_amplitude == pytest.approx(0.0123)
-
- def test_reset_amplitude_clears_current(self, qtbot: QtBot):
- widget = AudioMeterWidget()
- qtbot.add_widget(widget)
- widget.update_amplitude(0.8)
- widget.reset_amplitude()
- assert widget.current_amplitude == 0.0
-
- def test_reset_amplitude_clears_average(self, qtbot: QtBot):
- widget = AudioMeterWidget()
- qtbot.add_widget(widget)
- widget.update_average_amplitude(0.05)
- widget.reset_amplitude()
- assert widget.average_amplitude == 0.0
-
- def test_fixed_height(self, qtbot: QtBot):
- widget = AudioMeterWidget()
- qtbot.add_widget(widget)
- assert widget.height() == 56
diff --git a/tests/widgets/audio_player_test.py b/tests/widgets/audio_player_test.py
deleted file mode 100644
index a3375248..00000000
--- a/tests/widgets/audio_player_test.py
+++ /dev/null
@@ -1,157 +0,0 @@
-import os
-import pytest
-
-from PyQt6.QtCore import QTime
-from PyQt6.QtMultimedia import QMediaPlayer
-from PyQt6.QtWidgets import QHBoxLayout
-from pytestqt.qtbot import QtBot
-
-from buzz.widgets.audio_player import AudioPlayer
-from tests.audio import test_audio_path
-from buzz.settings.settings import Settings
-
-
-def assert_approximately_equal(actual, expected, tolerance=0.001):
- """Helper function to compare values with tolerance for floating-point precision"""
- assert abs(actual - expected) < tolerance, f"Value {actual} is not approximately equal to {expected}"
-
-
-class TestAudioPlayer:
- def test_should_load_audio(self, qtbot: QtBot):
- widget = AudioPlayer(test_audio_path)
- qtbot.add_widget(widget)
-
- actual = os.path.normpath(widget.media_player.source().toLocalFile())
- expected = os.path.normpath(test_audio_path)
- assert actual == expected
-
- def test_should_update_time_label(self, qtbot: QtBot):
- widget = AudioPlayer(test_audio_path)
- qtbot.add_widget(widget)
-
- widget.on_duration_changed(2000)
- widget.on_position_changed(1000)
-
- position_time = QTime(0, 0).addMSecs(1000).toString()
- duration_time = QTime(0, 0).addMSecs(2000).toString()
-
- assert widget.time_label.text() == f"{position_time} / {duration_time}"
-
- def test_should_toggle_play_button_icon(self, qtbot: QtBot):
- widget = AudioPlayer(test_audio_path)
- qtbot.add_widget(widget)
-
- widget.on_playback_state_changed(QMediaPlayer.PlaybackState.PlayingState)
- assert widget.play_button.icon().themeName() == widget.pause_icon.themeName()
-
- widget.on_playback_state_changed(QMediaPlayer.PlaybackState.PausedState)
- assert widget.play_button.icon().themeName() == widget.play_icon.themeName()
-
- widget.on_playback_state_changed(QMediaPlayer.PlaybackState.StoppedState)
- assert widget.play_button.icon().themeName() == widget.play_icon.themeName()
-
- def test_should_have_basic_audio_controls(self, qtbot: QtBot):
- widget = AudioPlayer(test_audio_path)
- qtbot.add_widget(widget)
-
- # Speed controls were moved to transcription viewer - just verify basic audio player functionality
- assert widget.play_button is not None
- assert widget.scrubber is not None
- assert widget.time_label is not None
-
- # Verify the widget loads audio correctly
- assert widget.media_player is not None
- assert os.path.normpath(widget.media_player.source().toLocalFile()) == os.path.normpath(test_audio_path)
-
- def test_should_change_playback_rate_directly(self, qtbot: QtBot):
- widget = AudioPlayer(test_audio_path)
- qtbot.add_widget(widget)
-
- # Speed controls moved to transcription viewer - test basic playback rate functionality
- initial_rate = widget.media_player.playbackRate()
- widget.media_player.setPlaybackRate(1.5)
- assert_approximately_equal(widget.media_player.playbackRate(), 1.5)
-
- def test_should_handle_custom_playback_rates(self, qtbot: QtBot):
- widget = AudioPlayer(test_audio_path)
- qtbot.add_widget(widget)
-
- # Speed controls moved to transcription viewer - test basic playback rate functionality
- widget.media_player.setPlaybackRate(1.7)
- assert_approximately_equal(widget.media_player.playbackRate(), 1.7)
-
- def test_should_handle_various_playback_rates(self, qtbot: QtBot):
- widget = AudioPlayer(test_audio_path)
- qtbot.add_widget(widget)
-
- # Speed controls moved to transcription viewer - test basic playback rate functionality
- # Test that the media player can handle various playback rates
- widget.media_player.setPlaybackRate(0.5)
- assert_approximately_equal(widget.media_player.playbackRate(), 0.5)
-
- widget.media_player.setPlaybackRate(2.0)
- assert_approximately_equal(widget.media_player.playbackRate(), 2.0)
-
- def test_should_use_single_row_layout(self, qtbot: QtBot):
- widget = AudioPlayer(test_audio_path)
- qtbot.add_widget(widget)
-
- # Verify the layout structure
- layout = widget.layout()
- assert isinstance(layout, QHBoxLayout)
- # Speed controls moved to transcription viewer - simplified layout
- assert layout.count() == 3 # play_button, scrubber, time_label
-
- def test_should_persist_playback_rate_setting(self, qtbot: QtBot):
- widget = AudioPlayer(test_audio_path)
- qtbot.add_widget(widget)
-
- # Speed controls moved to transcription viewer - test that settings are loaded
- # The widget should load the saved playback rate from settings
- assert widget.settings is not None
- saved_rate = widget.settings.value(Settings.Key.AUDIO_PLAYBACK_RATE, 1.0, float)
- assert isinstance(saved_rate, float)
- assert 0.1 <= saved_rate <= 5.0
-
- def test_should_handle_range_looping(self, qtbot: QtBot):
- widget = AudioPlayer(test_audio_path)
- qtbot.add_widget(widget)
-
- # Test range setting and looping functionality
- widget.set_range((1000, 3000)) # 1-3 seconds
- assert widget.range_ms == (1000, 3000)
-
- # Clear range
- widget.clear_range()
- assert widget.range_ms is None
-
- def test_should_handle_invalid_media(self, qtbot: QtBot):
- widget = AudioPlayer(test_audio_path)
- qtbot.add_widget(widget)
-
- widget.set_invalid_media(True)
-
- # Speed controls moved to transcription viewer - just verify invalid media handling
- assert widget.invalid_media is True
- assert widget.play_button.isEnabled() is False
- assert widget.scrubber.isEnabled() is False
- assert widget.time_label.isEnabled() is False
-
- def test_should_stop_playback(self, qtbot: QtBot):
- widget = AudioPlayer(test_audio_path)
- qtbot.add_widget(widget)
-
- # Test stop functionality
- widget.stop()
- assert widget.media_player.playbackState() == QMediaPlayer.PlaybackState.StoppedState
-
- def test_should_handle_media_status_changes(self, qtbot: QtBot):
- widget = AudioPlayer(test_audio_path)
- qtbot.add_widget(widget)
-
- # Test media status handling
- widget.on_media_status_changed(QMediaPlayer.MediaStatus.LoadedMedia)
- assert widget.invalid_media is False
-
- widget.on_media_status_changed(QMediaPlayer.MediaStatus.InvalidMedia)
- assert widget.invalid_media is True
diff --git a/tests/widgets/conftest.py b/tests/widgets/conftest.py
deleted file mode 100644
index 67b1ec05..00000000
--- a/tests/widgets/conftest.py
+++ /dev/null
@@ -1,25 +0,0 @@
-import gc
-import logging
-import pytest
-from unittest.mock import patch
-from buzz.settings.settings import Settings
-
-
-@pytest.fixture(autouse=True)
-def mock_get_password():
- with patch("buzz.widgets.recording_transcriber_widget.get_password", return_value=None):
- yield
-
-
-@pytest.fixture(autouse=True)
-def force_gc_between_tests():
- yield
- gc.collect()
-
-
-@pytest.fixture(scope="package")
-def reset_settings():
- settings = Settings()
- settings.clear()
- settings.sync()
- logging.debug("Reset settings")
diff --git a/tests/widgets/export_transcription_menu_test.py b/tests/widgets/export_transcription_menu_test.py
deleted file mode 100644
index 30a735be..00000000
--- a/tests/widgets/export_transcription_menu_test.py
+++ /dev/null
@@ -1,73 +0,0 @@
-import pathlib
-import uuid
-
-import pytest
-from PyQt6.QtCore import QObject, pyqtSignal
-from pytestqt.qtbot import QtBot
-
-from buzz.db.entity.transcription import Transcription
-from buzz.db.entity.transcription_segment import TranscriptionSegment
-from buzz.model_loader import ModelType, WhisperModelSize
-from buzz.transcriber.transcriber import Task
-from buzz.widgets.transcription_viewer.export_transcription_menu import (
- ExportTranscriptionMenu,
-)
-from tests.audio import test_audio_path
-
-
-class TranslationSignal(QObject):
- translation = pyqtSignal(str, int)
-
-
-class TestExportTranscriptionMenu:
- @pytest.fixture()
- def transcription(
- self, transcription_dao, transcription_segment_dao
- ) -> Transcription:
- id = uuid.uuid4()
- transcription_dao.insert(
- Transcription(
- id=str(id),
- status="completed",
- file=test_audio_path,
- task=Task.TRANSCRIBE.value,
- model_type=ModelType.WHISPER.value,
- whisper_model_size=WhisperModelSize.TINY.value,
- )
- )
- transcription_segment_dao.insert(TranscriptionSegment(40, 299, "Bien", "", str(id)))
- transcription_segment_dao.insert(
- TranscriptionSegment(299, 329, "venue dans", "", str(id))
- )
-
- return transcription_dao.find_by_id(str(id))
-
- def test_should_export_segments(
- self,
- tmp_path: pathlib.Path,
- qtbot: QtBot,
- transcription,
- transcription_service,
- shortcuts,
- mocker,
- ):
- output_file_path = tmp_path / "whisper.txt"
- mocker.patch(
- "PyQt6.QtWidgets.QFileDialog.getSaveFileName",
- return_value=(str(output_file_path), ""),
- )
-
- translation_signal = TranslationSignal()
-
- widget = ExportTranscriptionMenu(
- transcription,
- transcription_service,
- False,
- translation_signal.translation
- )
- qtbot.add_widget(widget)
-
- widget.actions()[0].trigger()
-
- with open(output_file_path, encoding="utf-8") as output_file:
- assert "Bien venue dans" in output_file.read()
diff --git a/tests/widgets/file_transcriber_widget_test.py b/tests/widgets/file_transcriber_widget_test.py
deleted file mode 100644
index ef66edd2..00000000
--- a/tests/widgets/file_transcriber_widget_test.py
+++ /dev/null
@@ -1,37 +0,0 @@
-from unittest.mock import Mock
-
-from PyQt6.QtCore import Qt
-from pytestqt.qtbot import QtBot
-
-from buzz.widgets.transcriber.file_transcriber_widget import FileTranscriberWidget
-from tests.audio import test_audio_path
-
-
-class TestFileTranscriberWidget:
- def test_should_set_window_title(self, qtbot: QtBot):
- widget = FileTranscriberWidget(
- file_paths=[test_audio_path],
- )
- qtbot.add_widget(widget)
- assert widget.windowTitle() == "whisper-french.mp3"
-
- def test_should_emit_triggered_event(self, qtbot: QtBot):
- widget = FileTranscriberWidget(
- file_paths=[test_audio_path],
- )
- qtbot.add_widget(widget)
-
- mock_triggered = Mock()
- widget.triggered.connect(mock_triggered)
-
- with qtbot.wait_signal(widget.triggered, timeout=30 * 1000):
- qtbot.mouseClick(widget.run_button, Qt.MouseButton.LeftButton)
-
- (
- transcription_options,
- file_transcription_options,
- model_path,
- ) = mock_triggered.call_args[0][0]
- assert transcription_options.language is None
- assert file_transcription_options.file_paths == [test_audio_path]
- assert len(model_path) > 0
diff --git a/tests/widgets/hugging_face_search_line_edit_test.py b/tests/widgets/hugging_face_search_line_edit_test.py
deleted file mode 100644
index d9a5b312..00000000
--- a/tests/widgets/hugging_face_search_line_edit_test.py
+++ /dev/null
@@ -1,177 +0,0 @@
-import json
-from unittest.mock import MagicMock, patch
-
-import pytest
-from PyQt6.QtCore import Qt, QEvent, QPoint
-from PyQt6.QtGui import QKeyEvent
-from PyQt6.QtNetwork import QNetworkReply, QNetworkAccessManager
-from PyQt6.QtWidgets import QListWidgetItem
-from pytestqt.qtbot import QtBot
-
-from buzz.widgets.transcriber.hugging_face_search_line_edit import HuggingFaceSearchLineEdit
-
-
-@pytest.fixture
-def widget(qtbot: QtBot):
- mock_manager = MagicMock(spec=QNetworkAccessManager)
- mock_manager.finished = MagicMock()
- mock_manager.finished.connect = MagicMock()
- w = HuggingFaceSearchLineEdit(network_access_manager=mock_manager)
- qtbot.add_widget(w)
- # Prevent popup.show() from triggering a Wayland fatal protocol error
- # in headless/CI environments where popup windows lack a transient parent.
- w.popup.show = MagicMock()
- return w
-
-
-class TestHuggingFaceSearchLineEdit:
- def test_initial_state(self, widget):
- assert widget.text() == ""
- assert widget.placeholderText() != ""
-
- def test_default_value_set(self, qtbot: QtBot):
- mock_manager = MagicMock(spec=QNetworkAccessManager)
- mock_manager.finished = MagicMock()
- mock_manager.finished.connect = MagicMock()
- w = HuggingFaceSearchLineEdit(default_value="openai/whisper-tiny", network_access_manager=mock_manager)
- qtbot.add_widget(w)
- assert w.text() == "openai/whisper-tiny"
-
- def test_on_text_edited_emits_model_selected(self, widget, qtbot: QtBot):
- spy = MagicMock()
- widget.model_selected.connect(spy)
- widget.on_text_edited("some/model")
- spy.assert_called_once_with("some/model")
-
- def test_fetch_models_skips_short_text(self, widget):
- widget.setText("ab")
- result = widget.fetch_models()
- assert result is None
-
- def test_fetch_models_makes_request_for_long_text(self, widget):
- widget.setText("whisper-tiny")
- mock_reply = MagicMock()
- widget.network_manager.get = MagicMock(return_value=mock_reply)
- result = widget.fetch_models()
- widget.network_manager.get.assert_called_once()
- assert result == mock_reply
-
- def test_fetch_models_url_contains_search_text(self, widget):
- widget.setText("whisper")
- widget.network_manager.get = MagicMock(return_value=MagicMock())
- widget.fetch_models()
- call_args = widget.network_manager.get.call_args[0][0]
- assert "whisper" in call_args.url().toString()
-
- def test_on_request_response_network_error_does_not_populate_popup(self, widget):
- mock_reply = MagicMock(spec=QNetworkReply)
- mock_reply.error.return_value = QNetworkReply.NetworkError.ConnectionRefusedError
- widget.on_request_response(mock_reply)
- assert widget.popup.count() == 0
-
- def test_on_request_response_populates_popup(self, widget):
- mock_reply = MagicMock(spec=QNetworkReply)
- mock_reply.error.return_value = QNetworkReply.NetworkError.NoError
- models = [{"id": "openai/whisper-tiny"}, {"id": "openai/whisper-base"}]
- mock_reply.readAll.return_value.data.return_value = json.dumps(models).encode()
- widget.on_request_response(mock_reply)
- assert widget.popup.count() == 2
- assert widget.popup.item(0).text() == "openai/whisper-tiny"
- assert widget.popup.item(1).text() == "openai/whisper-base"
-
- def test_on_request_response_empty_models_does_not_show_popup(self, widget):
- mock_reply = MagicMock(spec=QNetworkReply)
- mock_reply.error.return_value = QNetworkReply.NetworkError.NoError
- mock_reply.readAll.return_value.data.return_value = json.dumps([]).encode()
- widget.on_request_response(mock_reply)
- assert widget.popup.count() == 0
- widget.popup.show.assert_not_called()
-
- def test_on_request_response_item_has_user_role_data(self, widget):
- mock_reply = MagicMock(spec=QNetworkReply)
- mock_reply.error.return_value = QNetworkReply.NetworkError.NoError
- models = [{"id": "facebook/mms-1b-all"}]
- mock_reply.readAll.return_value.data.return_value = json.dumps(models).encode()
- widget.on_request_response(mock_reply)
- item = widget.popup.item(0)
- assert item.data(Qt.ItemDataRole.UserRole) == "facebook/mms-1b-all"
-
- def test_on_select_item_emits_model_selected(self, widget, qtbot: QtBot):
- item = QListWidgetItem("openai/whisper-tiny")
- item.setData(Qt.ItemDataRole.UserRole, "openai/whisper-tiny")
- widget.popup.addItem(item)
- widget.popup.setCurrentItem(item)
-
- spy = MagicMock()
- widget.model_selected.connect(spy)
- widget.on_select_item()
-
- spy.assert_called_with("openai/whisper-tiny")
- assert widget.text() == "openai/whisper-tiny"
-
- def test_on_select_item_hides_popup(self, widget):
- item = QListWidgetItem("openai/whisper-tiny")
- item.setData(Qt.ItemDataRole.UserRole, "openai/whisper-tiny")
- widget.popup.addItem(item)
- widget.popup.setCurrentItem(item)
-
- with patch.object(widget.popup, 'hide') as mock_hide:
- widget.on_select_item()
- mock_hide.assert_called_once()
-
- def test_on_popup_selected_stops_timer(self, widget):
- widget.timer.start()
- assert widget.timer.isActive()
- widget.on_popup_selected()
- assert not widget.timer.isActive()
-
- def test_event_filter_ignores_non_popup_target(self, widget):
- other = MagicMock()
- event = MagicMock()
- assert widget.eventFilter(other, event) is False
-
- def test_event_filter_mouse_press_hides_popup(self, widget):
- event = MagicMock()
- event.type.return_value = QEvent.Type.MouseButtonPress
- with patch.object(widget.popup, 'hide') as mock_hide:
- result = widget.eventFilter(widget.popup, event)
- assert result is True
- mock_hide.assert_called_once()
-
- def test_event_filter_escape_hides_popup(self, widget, qtbot: QtBot):
- event = QKeyEvent(QEvent.Type.KeyPress, Qt.Key.Key_Escape, Qt.KeyboardModifier.NoModifier)
- with patch.object(widget.popup, 'hide') as mock_hide:
- result = widget.eventFilter(widget.popup, event)
- assert result is True
- mock_hide.assert_called_once()
-
- def test_event_filter_enter_selects_item(self, widget, qtbot: QtBot):
- item = QListWidgetItem("openai/whisper-tiny")
- item.setData(Qt.ItemDataRole.UserRole, "openai/whisper-tiny")
- widget.popup.addItem(item)
- widget.popup.setCurrentItem(item)
-
- spy = MagicMock()
- widget.model_selected.connect(spy)
-
- event = QKeyEvent(QEvent.Type.KeyPress, Qt.Key.Key_Return, Qt.KeyboardModifier.NoModifier)
- result = widget.eventFilter(widget.popup, event)
- assert result is True
- spy.assert_called_with("openai/whisper-tiny")
-
- def test_event_filter_enter_no_item_returns_true(self, widget, qtbot: QtBot):
- event = QKeyEvent(QEvent.Type.KeyPress, Qt.Key.Key_Return, Qt.KeyboardModifier.NoModifier)
- result = widget.eventFilter(widget.popup, event)
- assert result is True
-
- def test_event_filter_navigation_keys_return_false(self, widget):
- for key in [Qt.Key.Key_Up, Qt.Key.Key_Down, Qt.Key.Key_Home,
- Qt.Key.Key_End, Qt.Key.Key_PageUp, Qt.Key.Key_PageDown]:
- event = QKeyEvent(QEvent.Type.KeyPress, key, Qt.KeyboardModifier.NoModifier)
- assert widget.eventFilter(widget.popup, event) is False
-
- def test_event_filter_other_key_hides_popup(self, widget):
- event = QKeyEvent(QEvent.Type.KeyPress, Qt.Key.Key_A, Qt.KeyboardModifier.NoModifier)
- with patch.object(widget.popup, 'hide') as mock_hide:
- widget.eventFilter(widget.popup, event)
- mock_hide.assert_called_once()
diff --git a/tests/widgets/import_url_dialog_test.py b/tests/widgets/import_url_dialog_test.py
deleted file mode 100644
index 804c457e..00000000
--- a/tests/widgets/import_url_dialog_test.py
+++ /dev/null
@@ -1,23 +0,0 @@
-from unittest.mock import patch
-
-from buzz.locale import _
-from buzz.widgets.import_url_dialog import ImportURLDialog
-
-
-class TestImportURLDialog:
- def test_should_show_error_with_invalid_url(self, qtbot):
- dialog = ImportURLDialog()
- dialog.line_edit.setText("bad-url")
-
- with patch("PyQt6.QtWidgets.QMessageBox.critical") as mock_critical:
- dialog.button_box.button(dialog.button_box.StandardButton.Ok).click()
- mock_critical.assert_called_with(
- dialog, _("Invalid URL"), _("The URL you entered is invalid.")
- )
-
- def test_should_return_url_with_valid_url(self, qtbot):
- dialog = ImportURLDialog()
- dialog.line_edit.setText("https://example.com")
-
- dialog.button_box.button(dialog.button_box.StandardButton.Ok).click()
- assert dialog.url == "https://example.com"
diff --git a/tests/widgets/main_window_test.py b/tests/widgets/main_window_test.py
deleted file mode 100644
index baccd2c8..00000000
--- a/tests/widgets/main_window_test.py
+++ /dev/null
@@ -1,405 +0,0 @@
-import logging
-import os
-import tempfile
-from typing import List
-from unittest.mock import patch, Mock
-
-import pytest
-from PyQt6.QtCore import QSize, Qt
-from PyQt6.QtGui import QKeyEvent, QAction
-from PyQt6.QtWidgets import (
- QMessageBox,
- QPushButton,
- QToolBar,
- QMenuBar,
- QTableView,
-)
-from pytestqt.qtbot import QtBot
-
-from buzz.locale import _
-from buzz.db.entity.transcription import Transcription
-from buzz.db.service.transcription_service import TranscriptionService
-from buzz.widgets.main_window import MainWindow
-from buzz.widgets.transcriber.file_transcriber_widget import FileTranscriberWidget
-
-mock_transcriptions: List[Transcription] = [
- Transcription(status="completed"),
- Transcription(status="canceled"),
- Transcription(status="failed", error_message=_("Error")),
-]
-
-
-def get_test_asset(filename: str):
- return os.path.join(os.path.dirname(__file__), "../../testdata/", filename)
-
-
-class TestMainWindow:
- def test_should_set_window_title_and_icon(self, qtbot, transcription_service):
- window = MainWindow(transcription_service)
- qtbot.add_widget(window)
- assert window.windowTitle() == "Buzz"
- assert window.windowIcon().pixmap(QSize(64, 64)).isNull() is False
- window.close()
-
- def test_should_run_file_transcription_task(
- self, qtbot: QtBot, transcription_service
- ):
- window = MainWindow(transcription_service)
-
- self._import_file_and_start_transcription(window)
-
- open_transcript_action = self._get_toolbar_action(window, _("Open Transcript"))
- assert open_transcript_action.isEnabled() is False
-
- table_widget = self._get_tasks_table(window)
- qtbot.wait_until(
- self._get_assert_task_status_callback(table_widget, 0, "completed"),
- timeout=2 * 60 * 1000,
- )
-
- table_widget.setCurrentIndex(table_widget.model().index(0, 0))
- assert open_transcript_action.isEnabled()
- window.close()
-
- @staticmethod
- def _get_tasks_table(window: MainWindow) -> QTableView:
- return window.findChild(QTableView)
-
- def test_should_run_url_import_file_transcription_task(
- self, qtbot: QtBot, db, transcription_service
- ):
- window = MainWindow(transcription_service)
- menu: QMenuBar = window.menuBar()
- file_action = menu.actions()[0]
- import_url_action: QAction = file_action.menu().actions()[1]
-
- with patch(
- "buzz.widgets.import_url_dialog.ImportURLDialog.prompt"
- ) as prompt_mock:
- prompt_mock.return_value = "https://github.com/chidiwilliams/buzz/raw/main/testdata/whisper-french.mp3"
- import_url_action.trigger()
-
- file_transcriber_widget: FileTranscriberWidget = window.findChild(
- FileTranscriberWidget
- )
- run_button: QPushButton = file_transcriber_widget.findChild(QPushButton)
- run_button.click()
-
- table_widget = self._get_tasks_table(window)
- qtbot.wait_until(
- self._get_assert_task_status_callback(table_widget, 0, "completed"),
- timeout=2 * 60 * 1000,
- )
-
- window.close()
-
- @pytest.mark.timeout(300)
- def test_should_run_and_cancel_transcription_task(
- self, qtbot, db, transcription_service
- ):
- window = MainWindow(transcription_service)
- qtbot.add_widget(window)
-
- self._import_file_and_start_transcription(window, long_audio=True)
-
- table_widget = self._get_tasks_table(window)
-
- 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")
-
- 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)
-
- 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
- assert window.toolbar.open_transcript_action.isEnabled() is False
-
- window.close()
-
- @pytest.mark.parametrize("transcription_dao", [mock_transcriptions], indirect=True)
- def test_should_load_tasks_from_cache(
- self, qtbot, transcription_dao, transcription_segment_dao, monkeypatch
- ):
- # Mock the queue worker to prevent it from processing tasks
- mock_queue_worker = Mock()
- mock_queue_worker.task_started = Mock()
- mock_queue_worker.task_progress = Mock()
- mock_queue_worker.task_download_progress = Mock()
- mock_queue_worker.task_error = Mock()
- mock_queue_worker.task_completed = Mock()
- mock_queue_worker.completed = Mock()
- mock_queue_worker.cancel_task = Mock()
- mock_queue_worker.add_task = Mock()
- mock_queue_worker.stop = Mock()
-
- monkeypatch.setattr("buzz.widgets.main_window.FileTranscriberQueueWorker", Mock(return_value=mock_queue_worker))
-
- window = MainWindow(
- TranscriptionService(transcription_dao, transcription_segment_dao)
- )
- qtbot.add_widget(window)
-
- table_widget = self._get_tasks_table(window)
- assert table_widget.model().rowCount() == 3
-
- # Get all statuses and verify they match expected values
- statuses = [self._get_status(table_widget, i) for i in range(3)]
- expected_statuses = {"completed", "canceled", "failed"}
- assert set(statuses) == expected_statuses, f"Expected {expected_statuses}, got {statuses}"
-
- # Test that completed transcriptions enable the open action, others don't
- for i in range(3):
- table_widget.selectRow(i)
- status = self._get_status(table_widget, i)
- if status == "completed":
- assert window.toolbar.open_transcript_action.isEnabled()
- else:
- assert window.toolbar.open_transcript_action.isEnabled() is False
- window.close()
-
- @pytest.mark.parametrize("transcription_dao", [mock_transcriptions], indirect=True)
- def test_should_clear_history_with_rows_selected(
- self, qtbot, transcription_dao, transcription_segment_dao
- ):
- window = MainWindow(
- TranscriptionService(transcription_dao, transcription_segment_dao)
- )
- qtbot.add_widget(window)
-
- table_widget = self._get_tasks_table(window)
- table_widget.selectAll()
-
- with patch("PyQt6.QtWidgets.QMessageBox.exec") as question_message_box_mock:
- question_message_box_mock.return_value = QMessageBox.StandardButton.Yes
- window.toolbar.clear_history_action.trigger()
-
- assert table_widget.model().rowCount() == 0
- window.close()
-
- @pytest.mark.parametrize("transcription_dao", [mock_transcriptions], indirect=True)
- def test_should_have_clear_history_action_disabled_with_no_rows_selected(
- self, qtbot, transcription_dao, transcription_segment_dao
- ):
- window = MainWindow(
- TranscriptionService(transcription_dao, transcription_segment_dao)
- )
- qtbot.add_widget(window)
-
- assert window.toolbar.clear_history_action.isEnabled() is False
- window.close()
-
- @pytest.mark.parametrize("transcription_dao", [mock_transcriptions], indirect=True)
- def test_should_open_transcription_viewer_when_menu_action_is_clicked(
- self, qtbot, transcription_dao, transcription_segment_dao
- ):
- window = MainWindow(
- TranscriptionService(transcription_dao, transcription_segment_dao)
- )
- qtbot.add_widget(window)
-
- table_widget = self._get_tasks_table(window)
-
- # Find and select the completed transcription row
- completed_row = None
- for i in range(table_widget.model().rowCount()):
- if self._get_status(table_widget, i) == "completed":
- completed_row = i
- break
-
- assert completed_row is not None, "No completed transcription found"
- table_widget.selectRow(completed_row)
-
- window.toolbar.open_transcript_action.trigger()
-
- assert window.transcription_viewer_widget is not None
-
- window.close()
-
- @pytest.mark.parametrize("transcription_dao", [mock_transcriptions], indirect=True)
- def test_should_open_transcription_viewer_when_return_clicked(
- self, qtbot, transcription_dao, transcription_segment_dao
- ):
- window = MainWindow(
- TranscriptionService(transcription_dao, transcription_segment_dao)
- )
- qtbot.add_widget(window)
-
- table_widget = self._get_tasks_table(window)
-
- # Find and select the completed transcription row
- completed_row = None
- for i in range(table_widget.model().rowCount()):
- if self._get_status(table_widget, i) == "completed":
- completed_row = i
- break
-
- assert completed_row is not None, "No completed transcription found"
- table_widget.selectRow(completed_row)
-
- table_widget.keyPressEvent(
- QKeyEvent(
- QKeyEvent.Type.KeyPress,
- Qt.Key.Key_Return,
- Qt.KeyboardModifier.NoModifier,
- "\r",
- )
- )
-
- assert window.transcription_viewer_widget is not None
-
- window.close()
-
- @pytest.mark.parametrize("transcription_dao", [mock_transcriptions], indirect=True)
- def test_should_have_open_transcript_action_disabled_with_no_rows_selected(
- self, qtbot, transcription_dao, transcription_segment_dao
- ):
- window = MainWindow(
- TranscriptionService(transcription_dao, transcription_segment_dao)
- )
- qtbot.add_widget(window)
-
- assert window.toolbar.open_transcript_action.isEnabled() is False
- window.close()
-
- def test_import_folder_opens_file_transcriber_with_supported_files(
- self, qtbot, transcription_service
- ):
- window = MainWindow(transcription_service)
- qtbot.add_widget(window)
-
- with tempfile.TemporaryDirectory() as folder:
- # Create supported and unsupported files
- supported = ["audio.mp3", "video.mp4", "clip.wav"]
- unsupported = ["document.txt", "image.png"]
- subdir = os.path.join(folder, "sub")
- os.makedirs(subdir)
- nested = "nested.flac"
-
- for name in supported + unsupported:
- open(os.path.join(folder, name), "w").close()
- open(os.path.join(subdir, nested), "w").close()
-
- with patch("PyQt6.QtWidgets.QFileDialog.getExistingDirectory") as mock_dir, \
- patch.object(window, "open_file_transcriber_widget") as mock_open:
- mock_dir.return_value = folder
- window.on_import_folder_action_triggered()
-
- collected = mock_open.call_args[0][0]
- collected_names = {os.path.basename(p) for p in collected}
- assert collected_names == {"audio.mp3", "video.mp4", "clip.wav", "nested.flac"}
-
- window.close()
-
- def test_import_folder_does_nothing_when_cancelled(
- self, qtbot, transcription_service
- ):
- window = MainWindow(transcription_service)
- qtbot.add_widget(window)
-
- with patch("PyQt6.QtWidgets.QFileDialog.getExistingDirectory") as mock_dir, \
- patch.object(window, "open_file_transcriber_widget") as mock_open:
- mock_dir.return_value = ""
- window.on_import_folder_action_triggered()
-
- mock_open.assert_not_called()
- window.close()
-
- def test_import_folder_does_nothing_when_no_supported_files(
- self, qtbot, transcription_service
- ):
- window = MainWindow(transcription_service)
- qtbot.add_widget(window)
-
- with tempfile.TemporaryDirectory() as folder:
- open(os.path.join(folder, "readme.txt"), "w").close()
- open(os.path.join(folder, "image.jpg"), "w").close()
-
- with patch("PyQt6.QtWidgets.QFileDialog.getExistingDirectory") as mock_dir, \
- patch.object(window, "open_file_transcriber_widget") as mock_open:
- mock_dir.return_value = folder
- window.on_import_folder_action_triggered()
-
- mock_open.assert_not_called()
- window.close()
-
- @staticmethod
- def _import_file_and_start_transcription(
- window: MainWindow, long_audio: bool = False
- ):
- with patch(
- "PyQt6.QtWidgets.QFileDialog.getOpenFileNames"
- ) as open_file_names_mock:
- open_file_names_mock.return_value = (
- [
- get_test_asset(
- "audio-long.mp3" if long_audio else "whisper-french.mp3"
- )
- ],
- "",
- )
- new_transcription_action = TestMainWindow._get_toolbar_action(
- window, _("New File Transcription")
- )
- new_transcription_action.trigger()
-
- file_transcriber_widget: FileTranscriberWidget = window.findChild(
- FileTranscriberWidget
- )
- run_button: QPushButton = file_transcriber_widget.findChild(QPushButton)
- run_button.click()
-
- @staticmethod
- def _get_assert_task_status_callback(
- table_widget: QTableView,
- row_index: int,
- expected_status: str,
- ):
- def assert_task_status():
- assert table_widget.model().rowCount() > 0
- assert expected_status in TestMainWindow._get_status(
- table_widget, row_index
- )
-
- return assert_task_status
-
- @staticmethod
- def _get_status(table_widget: QTableView, row_index: int):
- return table_widget.model().index(row_index, 9).data()
-
- @staticmethod
- def _get_toolbar_action(window: MainWindow, text: str):
- toolbar: QToolBar = window.findChild(QToolBar)
- return [action for action in toolbar.actions() if action.text() == text][0]
diff --git a/tests/widgets/menu_bar_test.py b/tests/widgets/menu_bar_test.py
deleted file mode 100644
index 6ee555a5..00000000
--- a/tests/widgets/menu_bar_test.py
+++ /dev/null
@@ -1,35 +0,0 @@
-from unittest.mock import patch, Mock
-
-from PyQt6.QtCore import QSettings
-
-from buzz.widgets.menu_bar import MenuBar
-from buzz.widgets.preferences_dialog.models.preferences import Preferences
-from buzz.widgets.preferences_dialog.preferences_dialog import PreferencesDialog
-
-
-class TestMenuBar:
- def test_import_folder_action_emits_signal(self, qtbot, shortcuts):
- menu_bar = MenuBar(
- shortcuts=shortcuts, preferences=Preferences.load(QSettings())
- )
- qtbot.add_widget(menu_bar)
-
- signal_mock = Mock()
- menu_bar.import_folder_action_triggered.connect(signal_mock)
- menu_bar.import_folder_action.trigger()
-
- signal_mock.assert_called_once()
-
- def test_open_preferences_dialog(self, qtbot, shortcuts):
- menu_bar = MenuBar(
- shortcuts=shortcuts, preferences=Preferences.load(QSettings())
- )
- qtbot.add_widget(menu_bar)
-
- preferences_dialog = menu_bar.findChild(PreferencesDialog)
- assert preferences_dialog is None
-
- menu_bar.preferences_action.trigger()
-
- preferences_dialog = menu_bar.findChild(PreferencesDialog)
- assert isinstance(preferences_dialog, PreferencesDialog)
diff --git a/tests/widgets/model_download_progress_dialog.py b/tests/widgets/model_download_progress_dialog.py
deleted file mode 100644
index e9738ddc..00000000
--- a/tests/widgets/model_download_progress_dialog.py
+++ /dev/null
@@ -1,29 +0,0 @@
-from PyQt6.QtCore import Qt
-
-from buzz.locale import _
-from buzz.model_loader import ModelType
-from buzz.widgets.model_download_progress_dialog import ModelDownloadProgressDialog
-
-
-class TestModelDownloadProgressDialog:
- def test_should_show_dialog(self, qtbot):
- dialog = ModelDownloadProgressDialog(model_type=ModelType.WHISPER, parent=None)
- qtbot.add_widget(dialog)
- assert dialog.labelText() == f"{_('Downloading model')} (0%)"
-
- def test_should_update_label_on_progress(self, qtbot):
- dialog = ModelDownloadProgressDialog(model_type=ModelType.WHISPER, parent=None)
- qtbot.add_widget(dialog)
- dialog.set_value(0.0)
-
- dialog.set_value(0.01)
- assert dialog.labelText().startswith(f"{_('Downloading model')} (1%")
-
- dialog.set_value(0.1)
- assert dialog.labelText().startswith(f"{_('Downloading model')} (10%")
-
- # Other windows should not be processing while models are being downloaded
- def test_should_be_an_application_modal(self, qtbot):
- dialog = ModelDownloadProgressDialog(model_type=ModelType.WHISPER, parent=None)
- qtbot.add_widget(dialog)
- assert dialog.windowModality() == Qt.WindowModality.WindowModal
diff --git a/tests/widgets/model_type_combo_box_test.py b/tests/widgets/model_type_combo_box_test.py
deleted file mode 100644
index 39babb63..00000000
--- a/tests/widgets/model_type_combo_box_test.py
+++ /dev/null
@@ -1,35 +0,0 @@
-import platform
-
-import pytest
-
-from buzz.widgets.model_type_combo_box import ModelTypeComboBox
-
-
-class TestModelTypeComboBox:
- @pytest.mark.parametrize(
- "model_types",
- [
- pytest.param(
- [
- "Whisper",
- "Whisper.cpp",
- "Hugging Face",
- "Faster Whisper",
- "OpenAI Whisper API",
- # Faster Whisper is not available on macOS x86_64
- ] if not (platform.system() == "Darwin" and platform.machine() == "x86_64") else [
- "Whisper",
- "Whisper.cpp",
- "Hugging Face",
- "OpenAI Whisper API",
- ],
- ),
- ],
- )
- def test_should_display_items(self, qtbot, model_types):
- widget = ModelTypeComboBox()
- qtbot.add_widget(widget)
-
- assert widget.count() == len(model_types)
- for index, model_type in enumerate(model_types):
- assert widget.itemText(index) == model_type
diff --git a/tests/widgets/openai_api_key_line_edit_test.py b/tests/widgets/openai_api_key_line_edit_test.py
deleted file mode 100644
index 6383763b..00000000
--- a/tests/widgets/openai_api_key_line_edit_test.py
+++ /dev/null
@@ -1,24 +0,0 @@
-from buzz.widgets.openai_api_key_line_edit import OpenAIAPIKeyLineEdit
-
-
-class TestOpenAIAPIKeyLineEdit:
- def test_should_emit_key_changed(self, qtbot):
- line_edit = OpenAIAPIKeyLineEdit(key="")
- qtbot.add_widget(line_edit)
-
- with qtbot.wait_signal(line_edit.key_changed):
- line_edit.setText("abcdefg")
-
- def test_should_toggle_visibility(self, qtbot):
- line_edit = OpenAIAPIKeyLineEdit(key="")
- qtbot.add_widget(line_edit)
-
- assert line_edit.echoMode() == OpenAIAPIKeyLineEdit.EchoMode.Password
-
- toggle_action = line_edit.actions()[0]
-
- toggle_action.trigger()
- assert line_edit.echoMode() == OpenAIAPIKeyLineEdit.EchoMode.Normal
-
- toggle_action.trigger()
- assert line_edit.echoMode() == OpenAIAPIKeyLineEdit.EchoMode.Password
diff --git a/tests/widgets/preferences_dialog/__init__.py b/tests/widgets/preferences_dialog/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/tests/widgets/preferences_dialog/folder_watch_preferences_widget_test.py b/tests/widgets/preferences_dialog/folder_watch_preferences_widget_test.py
deleted file mode 100644
index 1d68a062..00000000
--- a/tests/widgets/preferences_dialog/folder_watch_preferences_widget_test.py
+++ /dev/null
@@ -1,100 +0,0 @@
-from unittest.mock import Mock
-
-from PyQt6.QtWidgets import QCheckBox, QLineEdit
-
-from buzz.model_loader import TranscriptionModel
-from buzz.transcriber.transcriber import Task
-from buzz.widgets.preferences_dialog.folder_watch_preferences_widget import (
- FolderWatchPreferencesWidget,
-)
-from buzz.widgets.preferences_dialog.models.file_transcription_preferences import (
- FileTranscriptionPreferences,
-)
-from buzz.widgets.preferences_dialog.models.folder_watch_preferences import (
- FolderWatchPreferences,
-)
-
-
-class TestFolderWatchPreferencesWidget:
- def test_edit_folder_watch_preferences(self, qtbot):
- widget = FolderWatchPreferencesWidget(
- config=FolderWatchPreferences(
- enabled=False,
- input_directory="",
- output_directory="",
- file_transcription_options=FileTranscriptionPreferences(
- language=None,
- task=Task.TRANSCRIBE,
- model=TranscriptionModel.default(),
- word_level_timings=False,
- extract_speech=False,
- initial_prompt="",
- enable_llm_translation=False,
- llm_model="",
- llm_prompt="",
- output_formats=set(),
- ),
- ),
- )
- mock_config_changed = Mock()
- widget.config_changed.connect(mock_config_changed)
- qtbot.add_widget(widget)
-
- checkbox = widget.findChild(QCheckBox, "EnableFolderWatchCheckbox")
- input_folder_line_edit = widget.findChild(QLineEdit, "InputFolderLineEdit")
- output_folder_line_edit = widget.findChild(QLineEdit, "OutputFolderLineEdit")
-
- assert not checkbox.isChecked()
- assert input_folder_line_edit.text() == ""
- assert output_folder_line_edit.text() == ""
- assert not input_folder_line_edit.isEnabled()
- assert not output_folder_line_edit.isEnabled()
-
- checkbox.setChecked(True)
- assert input_folder_line_edit.isEnabled()
- assert output_folder_line_edit.isEnabled()
- input_folder_line_edit.setText("test/input/folder")
- output_folder_line_edit.setText("test/output/folder")
-
- last_config_changed_call = mock_config_changed.call_args_list[-1]
- assert last_config_changed_call[0][0].enabled
- assert last_config_changed_call[0][0].input_directory == "test/input/folder"
- assert last_config_changed_call[0][0].output_directory == "test/output/folder"
-
- def test_delete_processed_files_checkbox(self, qtbot):
- widget = FolderWatchPreferencesWidget(
- config=FolderWatchPreferences(
- enabled=False,
- input_directory="",
- output_directory="",
- file_transcription_options=FileTranscriptionPreferences(
- language=None,
- task=Task.TRANSCRIBE,
- model=TranscriptionModel.default(),
- word_level_timings=False,
- extract_speech=False,
- initial_prompt="",
- enable_llm_translation=False,
- llm_model="",
- llm_prompt="",
- output_formats=set(),
- ),
- ),
- )
- mock_config_changed = Mock()
- widget.config_changed.connect(mock_config_changed)
- qtbot.add_widget(widget)
-
- delete_checkbox = widget.findChild(QCheckBox, "DeleteProcessedFilesCheckbox")
- assert delete_checkbox is not None
- assert not delete_checkbox.isChecked()
-
- delete_checkbox.setChecked(True)
-
- last_config = mock_config_changed.call_args_list[-1][0][0]
- assert last_config.delete_processed_files is True
-
- delete_checkbox.setChecked(False)
-
- last_config = mock_config_changed.call_args_list[-1][0][0]
- assert last_config.delete_processed_files is False
diff --git a/tests/widgets/preferences_dialog/general_preferences_widget_test.py b/tests/widgets/preferences_dialog/general_preferences_widget_test.py
deleted file mode 100644
index d3eb5f1a..00000000
--- a/tests/widgets/preferences_dialog/general_preferences_widget_test.py
+++ /dev/null
@@ -1,153 +0,0 @@
-from PyQt6.QtCore import Qt
-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, ValidateOpenAIApiKeyJob
-)
-
-
-class TestGeneralPreferencesWidget:
- def test_should_disable_test_button_if_no_api_key(self, qtbot, mocker):
- mocker.patch(
- "buzz.widgets.preferences_dialog.general_preferences_widget.get_password",
- return_value="",
- )
-
- widget = GeneralPreferencesWidget()
- qtbot.add_widget(widget)
-
- test_button = widget.findChild(QPushButton)
- assert isinstance(test_button, QPushButton)
-
- assert test_button.text() == _("Test")
- assert not test_button.isEnabled()
-
- line_edit = widget.findChild(QLineEdit)
- assert isinstance(line_edit, QLineEdit)
- line_edit.setText("123")
-
- assert test_button.isEnabled()
-
- def test_should_test_openai_api_key(self, qtbot, mocker):
- mocker.patch(
- "buzz.widgets.preferences_dialog.general_preferences_widget.get_password",
- return_value="wrong-api-key",
- )
-
- widget = GeneralPreferencesWidget()
- qtbot.add_widget(widget)
-
- test_button = widget.findChild(QPushButton)
- assert isinstance(test_button, QPushButton)
-
- test_button.click()
-
- message_box_warning_mock = mocker.Mock()
- QMessageBox.warning = message_box_warning_mock
-
- def mock_called():
- message_box_warning_mock.assert_called()
- assert message_box_warning_mock.call_args[0][1] == _("OpenAI API Key Test")
- assert (
- message_box_warning_mock.call_args[0][2]
- == "Incorrect API key provided: wrong-ap*-key. You can find your "
- "API key at https://platform.openai.com/account/api-keys."
- )
-
- qtbot.waitUntil(mock_called)
-
- def test_recording_export_preferences(self, qtbot, mocker):
- mocker.patch(
- "PyQt6.QtWidgets.QFileDialog.getExistingDirectory",
- return_value="/path/to/export/folder",
- )
-
- widget = GeneralPreferencesWidget()
- qtbot.add_widget(widget)
-
- browse_button = widget.findChild(QPushButton, "RecordingExportFolderBrowseButton")
- checkbox = widget.findChild(QCheckBox, "EnableRecordingExportCheckbox")
-
- browse_button_enabled = browse_button.isEnabled()
-
- qtbot.mouseClick(widget.export_enabled_checkbox, Qt.MouseButton.LeftButton)
- checkbox.setChecked(not browse_button_enabled)
-
- assert browse_button.isEnabled() != browse_button_enabled
-
- qtbot.mouseClick(widget.recording_export_folder_browse_button, Qt.MouseButton.LeftButton)
-
- assert widget.recording_export_folder_line_edit.text() == "/path/to/export/folder"
-
- assert widget.settings.value(
- key=widget.settings.Key.RECORDING_TRANSCRIBER_EXPORT_ENABLED,
- default_value=False) != browse_button_enabled
- assert widget.settings.value(
- key=widget.settings.Key.RECORDING_TRANSCRIBER_EXPORT_FOLDER,
- default_value='/home/user/documents') == '/path/to/export/folder'
-
- def test_openai_base_url_preferences(self, qtbot, mocker):
- widget = GeneralPreferencesWidget()
- qtbot.add_widget(widget)
-
- settings = Settings()
-
- openai_base_url = settings.value(
- key=Settings.Key.CUSTOM_OPENAI_BASE_URL, default_value=""
- )
-
- assert openai_base_url == ""
- assert widget.custom_openai_base_url_line_edit.text() == ""
-
- widget.custom_openai_base_url_line_edit.setText("http://localhost:11434/v1")
-
- updated_openai_base_url = settings.value(
- key=Settings.Key.CUSTOM_OPENAI_BASE_URL, default_value=""
- )
-
- assert updated_openai_base_url == "http://localhost:11434/v1"
-
-
-class TestTestOpenAIApiKeyJob:
- # No error = success
- def test_run_success(self, mocker):
- mock_client = mocker.Mock()
- mock_client.models.list.return_value = None
- 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 = ValidateOpenAIApiKeyJob(api_key="test_key")
- mock_success = mocker.Mock()
- mock_failed = mocker.Mock()
- job.signals.success.connect(mock_success)
- job.signals.failed.connect(mock_failed)
-
- job.run()
-
- mock_success.assert_called_once()
- mock_failed.assert_not_called()
- mock_client.models.list.assert_called_once()
-
- # Has error = failure
- def test_run_authentication_error(self, mocker):
- from openai import AuthenticationError
- mock_client = mocker.Mock()
- mock_client.models.list.side_effect = AuthenticationError(
- message="Incorrect API key provided", response=mocker.Mock(), body={"message": "Incorrect API key provided"}
- )
- 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 = ValidateOpenAIApiKeyJob(api_key="wrong_key")
- mock_success = mocker.Mock()
- mock_failed = mocker.Mock()
- job.signals.success.connect(mock_success)
- job.signals.failed.connect(mock_failed)
-
- job.run()
-
- mock_success.assert_not_called()
- mock_failed.assert_called_once_with("Incorrect API key provided")
- mock_client.models.list.assert_called_once()
\ No newline at end of file
diff --git a/tests/widgets/preferences_dialog/models_preferences_widget_test.py b/tests/widgets/preferences_dialog/models_preferences_widget_test.py
deleted file mode 100644
index 3da12b3c..00000000
--- a/tests/widgets/preferences_dialog/models_preferences_widget_test.py
+++ /dev/null
@@ -1,113 +0,0 @@
-import os
-
-import pytest
-from PyQt6.QtCore import Qt
-from PyQt6.QtWidgets import QComboBox, QPushButton
-from pytestqt.qtbot import QtBot
-
-from buzz.locale import _
-from buzz.model_loader import (
- TranscriptionModel,
- ModelType,
-)
-from buzz.widgets.preferences_dialog.models_preferences_widget import (
- ModelsPreferencesWidget,
-)
-from tests.model_loader import get_model_path
-
-
-class TestModelsPreferencesWidget:
- @pytest.fixture(scope="class")
- def clear_model_cache(self):
- for model_type in ModelType:
- if model_type.is_available():
- path = TranscriptionModel(model_type=model_type).get_local_model_path()
- if path and os.path.isfile(path):
- os.remove(path)
-
- def test_should_show_model_list(self, qtbot):
- widget = ModelsPreferencesWidget()
- qtbot.add_widget(widget)
-
- first_item = widget.model_list_widget.topLevelItem(0)
- assert first_item.text(0) == _("Downloaded")
-
- second_item = widget.model_list_widget.topLevelItem(1)
- assert second_item.text(0) == _("Available for Download")
-
- def test_should_change_model_type(self, qtbot):
- widget = ModelsPreferencesWidget()
- qtbot.add_widget(widget)
-
- combo_box = widget.findChild(QComboBox)
- assert isinstance(combo_box, QComboBox)
- combo_box.setCurrentText("Faster Whisper")
-
- first_item = widget.model_list_widget.topLevelItem(0)
- assert first_item.text(0) == _("Downloaded")
-
- second_item = widget.model_list_widget.topLevelItem(1)
- assert second_item.text(0) == _("Available for Download")
-
- def test_should_download_model(self, qtbot: QtBot, clear_model_cache):
- # make progress dialog non-modal to unblock qtbot.wait_until
- widget = ModelsPreferencesWidget(
- progress_dialog_modality=Qt.WindowModality.NonModal
- )
- qtbot.add_widget(widget)
-
- assert widget.model.get_local_model_path() is None
-
- available_item = widget.model_list_widget.topLevelItem(1)
- assert available_item.text(0) == _("Available for Download")
-
- tiny_item = available_item.child(0)
- assert tiny_item.text(0) == "Tiny"
- tiny_item.setSelected(True)
-
- download_button = widget.findChild(QPushButton, "DownloadButton")
- assert isinstance(download_button, QPushButton)
-
- assert download_button.text() == _("Download")
- download_button.click()
-
- def downloaded_model():
- assert not download_button.isVisible()
-
- _downloaded_item = widget.model_list_widget.topLevelItem(0)
- assert _downloaded_item.childCount() > 0
- assert _downloaded_item.child(0).text(0) == "Tiny"
-
- _available_item = widget.model_list_widget.topLevelItem(1)
- assert (
- _available_item.childCount() == 0
- or _available_item.child(0).text(0) != "Tiny"
- )
-
- assert os.path.isfile(widget.model.get_local_model_path())
-
- qtbot.wait_until(callback=downloaded_model, timeout=60_000)
-
- @pytest.fixture(scope="class")
- def default_model_path(self) -> str:
- return get_model_path(transcription_model=(TranscriptionModel.default()))
-
- def test_should_show_downloaded_model(self, qtbot, default_model_path):
- widget = ModelsPreferencesWidget()
- widget.show()
- qtbot.add_widget(widget)
-
- available_item = widget.model_list_widget.topLevelItem(0)
- assert available_item.text(0) == _("Downloaded")
-
- tiny_item = available_item.child(0)
- assert tiny_item.text(0) == "Tiny"
- tiny_item.setSelected(True)
-
- delete_button = widget.findChild(QPushButton, "DeleteButton")
- assert delete_button.isVisible()
-
- show_file_location_button = widget.findChild(
- QPushButton, "ShowFileLocationButton"
- )
- assert show_file_location_button.isVisible()
diff --git a/tests/widgets/preferences_dialog/preferences_dialog_test.py b/tests/widgets/preferences_dialog/preferences_dialog_test.py
deleted file mode 100644
index b61a3b85..00000000
--- a/tests/widgets/preferences_dialog/preferences_dialog_test.py
+++ /dev/null
@@ -1,68 +0,0 @@
-import os
-
-from PyQt6.QtCore import QSettings
-from PyQt6.QtWidgets import QTabWidget
-from pytestqt.qtbot import QtBot
-
-from buzz.locale import _
-from buzz.widgets.preferences_dialog.models.preferences import Preferences
-from buzz.widgets.preferences_dialog.preferences_dialog import PreferencesDialog
-
-
-class TestPreferencesDialog:
- locale_file_path = os.path.abspath(
- os.path.join(os.path.dirname(__file__), "../../../buzz/locale/lv_LV/LC_MESSAGES/buzz.mo")
- )
-
- def test_create(self, qtbot: QtBot, shortcuts):
- dialog = PreferencesDialog(
- shortcuts=shortcuts, preferences=Preferences.load(QSettings())
- )
- qtbot.add_widget(dialog)
-
- assert dialog.windowTitle() == _("Preferences")
-
- tab_widget = dialog.findChild(QTabWidget)
- assert isinstance(tab_widget, QTabWidget)
- assert tab_widget.count() == 4
- assert tab_widget.tabText(0) == _("General")
- assert tab_widget.tabText(1) == _("Models")
- assert tab_widget.tabText(2) == _("Shortcuts")
- assert tab_widget.tabText(3) == _("Folder Watch")
-
- def test_create_localized(self, qtbot: QtBot, shortcuts, mocker):
- mocker.patch(
- "PyQt6.QtCore.QLocale.name",
- return_value='lv_LV',
- )
-
- # Reload the module after the patch
- from importlib import reload
- import buzz.locale
- import buzz.widgets.preferences_dialog.models.preferences
- import buzz.widgets.preferences_dialog.preferences_dialog
-
- reload(buzz.locale)
- reload(buzz.widgets.preferences_dialog.models.preferences)
- reload(buzz.widgets.preferences_dialog.preferences_dialog)
-
- from buzz.locale import _
- from buzz.widgets.preferences_dialog.models.preferences import Preferences
- from buzz.widgets.preferences_dialog.preferences_dialog import PreferencesDialog
-
- dialog = PreferencesDialog(
- shortcuts=shortcuts, preferences=Preferences.load(QSettings())
- )
- qtbot.add_widget(dialog)
-
- assert os.path.isfile(self.locale_file_path), "File .mo file does not exist"
- assert _("Preferences") == "Iestatījumi"
- assert dialog.windowTitle() == "Iestatījumi"
-
- tab_widget = dialog.findChild(QTabWidget)
- assert isinstance(tab_widget, QTabWidget)
- assert tab_widget.count() == 4
- assert tab_widget.tabText(0) == "Vispārīgi"
- assert tab_widget.tabText(1) == "Modeļi"
- assert tab_widget.tabText(2) == "Īsinājumi"
- assert tab_widget.tabText(3) == "Mapes vērošana"
diff --git a/tests/widgets/presentation_window_test.py b/tests/widgets/presentation_window_test.py
deleted file mode 100644
index 2e224272..00000000
--- a/tests/widgets/presentation_window_test.py
+++ /dev/null
@@ -1,324 +0,0 @@
-import os
-import pytest
-import tempfile
-
-from unittest.mock import patch, MagicMock
-from pytestqt.qtbot import QtBot
-from PyQt6.QtCore import Qt
-from PyQt6.QtGui import QKeyEvent
-
-from buzz.widgets.presentation_window import PresentationWindow
-from buzz.settings.settings import Settings
-from buzz.locale import _
-
-class TestPresentationWindow:
- def test_should_set_window_title(self, qtbot: QtBot):
- """Test that the window title is set correctly"""
- window = PresentationWindow()
- qtbot.add_widget(window)
-
- assert _("Live Transcript Presentation") in window.windowTitle()
- window.close()
-
- def test_should_have_window_flag(self, qtbot: QtBot):
- """Test that window has the Window flag set"""
- window = PresentationWindow()
- qtbot.add_widget(window)
-
- assert window.windowFlags() & Qt.WindowType.Window
- window.close()
-
- def test_should_have_transcript_display(self, qtbot: QtBot):
- """Test that the transcript display is created"""
- window = PresentationWindow()
- qtbot.add_widget(window)
-
- assert window.transcript_display is not None
- assert window.transcript_display.isReadOnly()
- window.close()
-
- def test_should_have_translation_display_hidden(self, qtbot: QtBot):
- """Test that the translation display is created but hidden initially"""
- window = PresentationWindow()
- qtbot.add_widget(window)
-
- assert window.translation_display is not None
- assert window.translation_display.isReadOnly()
- assert not window.translation_display.isVisible()
- window.close()
-
- def test_should_have_default_size(self, qtbot: QtBot):
- """Test that the window has default size"""
- window = PresentationWindow()
- qtbot.add_widget(window)
-
- assert window.width() == 800
- assert window.height() == 600
- window.close()
-
-
-class TestPresentationWindowUpdateTranscripts:
- def test_update_transcripts_with_text(self, qtbot: QtBot):
- """Test updating transcripts with text"""
- window = PresentationWindow()
- qtbot.add_widget(window)
-
- window.update_transcripts("Hello world")
-
- assert window._current_transcript == "Hello world"
- assert "Hello world" in window.transcript_display.toHtml()
- window.close()
-
- def test_update_transcripts_with_empty_text(self, qtbot: QtBot):
- """Test that empty text does not update the display"""
- window = PresentationWindow()
- qtbot.add_widget(window)
-
- window.update_transcripts("")
-
- assert window._current_transcript == ""
- window.close()
-
- def test_update_transcripts_escapes_html(self, qtbot: QtBot):
- """Test that special HTML characters are escaped"""
- window = PresentationWindow()
- qtbot.add_widget(window)
-
- window.update_transcripts("")
-
- html = window.transcript_display.toHtml()
- assert "