diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..509ffa9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,45 @@ +# Contributing + +## Быстрый старт для разработчика + +```bash +git clone https://github.com/dementev-dev/local-transcriber +cd local-transcriber +uv sync +``` + +## Запуск + +```bash +uv run transcribe meeting.mp4 # CLI +uv run pytest # тесты +uv run pytest -v # подробный вывод тестов +``` + +## Структура проекта + +``` +src/local_transcriber/ +├── cli.py # Точка входа CLI (typer) +├── config.py # Загрузка .transcriber.toml, device-aware дефолты +├── formatter.py # Форматирование результата в markdown +├── transcriber.py # Обёртка над faster-whisper (загрузка модели, транскрипция) +└── utils.py # Утилиты: валидация файлов, детект устройства, глобы +tests/ +├── test_cli.py # Тесты CLI (typer runner + моки) +└── ... +``` + +## Соглашения + +- Тесты: `uv run pytest` должен проходить перед PR +- Стиль: стандартный Python (ruff-совместимый) +- Коммиты: [Conventional Commits](https://www.conventionalcommits.org/) +- Язык кода: английский (имена, комментарии); UI-строки для пользователя — русский + +## Как сделать PR + +1. Форкните репозиторий +2. Создайте ветку: `git checkout -b feat/my-feature` +3. Убедитесь, что тесты проходят: `uv run pytest` +4. Откройте Pull Request с описанием изменений diff --git a/README.md b/README.md index c7d4494..63e975c 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,54 @@ # local-transcriber -Локальный CLI для транскрипции аудио и видео через [faster-whisper](https://github.com/SYSTRAN/faster-whisper). -Принимает файл, распознаёт речь на машине (без внешних API) и создаёт markdown с таймкодами. -Результат удобен для последующей обработки ИИ — суммаризация, action items и т.д. -Работает с GPU (NVIDIA CUDA, быстро) и CPU (медленнее). +Локальная транскрипция аудио и видео в markdown — без облака, без API-ключей. ```bash transcribe meeting.mp4 # → meeting-transcript.md ``` -## Требования - -- Python ≥ 3.10 -- [uv](https://docs.astral.sh/uv/) — менеджер пакетов -- ffmpeg в PATH -- (Опционально) NVIDIA GPU + установленный драйвер (проверка: `nvidia-smi`) +- **Полностью локально** — данные не покидают машину +- **Авто-GPU** — автоматически использует NVIDIA CUDA, если доступен +- **Батч-режим** — обработка нескольких файлов за один вызов +- **Markdown с таймкодами** — удобен для суммаризации ИИ +- **Аудио и видео** — mp3, wav, mp4, mkv и [другие форматы](#поддерживаемые-форматы) ## Установка -```bash -git clone -cd local-transcriber -uv tool install . -``` - -После этого команда `transcribe` доступна глобально в PATH. - -Обновление после `git pull`: +**1. Установить [uv](https://docs.astral.sh/uv/getting-started/installation/)** (если ещё нет): ```bash -uv tool install --force . +curl -LsSf https://astral.sh/uv/install.sh | sh # Linux / macOS +``` +```powershell +powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" # Windows ``` -Модели скачиваются автоматически при первом запуске (~1.5 GB для medium, ~3 GB для large-v3), -нужен доступ в интернет (Hugging Face Hub). +**2. Установить transcriber:** -> **Если `transcribe: command not found`** — убедитесь, что директория -> инструментов uv добавлена в PATH. Выполните `uv tool dir --bin` -> чтобы узнать путь, и добавьте его в PATH вашего shell. +```bash +uv tool install git+https://github.com/dementev-dev/local-transcriber +``` + +**3. Готово:** + +```bash +transcribe meeting.mp4 +``` +Модели скачиваются автоматически при первом запуске (~1.5 GB для medium), нужен доступ в интернет. + +
+transcribe: command not found + +Выполните `uv tool update-shell` — это добавит нужный путь в PATH автоматически. + +
+ +**Обновление:** + +```bash +uv tool install --force git+https://github.com/dementev-dev/local-transcriber +``` ## Использование @@ -73,34 +83,9 @@ transcribe *.mp4 --force - Файлы с существующим транскриптом (`*-transcript.md`) автоматически пропускаются - `--force` / `-f` — перезаписать существующие транскрипты -- В конце выводится итоговая статистика: обработано, пропущено, ошибок - При ошибке в одном файле остальные продолжают обрабатываться - `--output` несовместим с несколькими файлами -### Конфигурационный файл - -Дефолтные параметры можно задать в `.transcriber.toml`: - -```toml -language = "ru" -model = "large-v3" -compute_type = "float16" -``` - -Порядок поиска: -1. `.transcriber.toml` в текущей директории (проектный конфиг) -2. `~/.config/transcriber/config.toml` (глобальный конфиг пользователя) - -Приоритет: **CLI-аргумент > конфиг > device-aware дефолт > встроенный дефолт**. - -Дефолты зависят от устройства (если не заданы явно): - -| Параметр | GPU (CUDA) | CPU | -|----------|-----------|-----| -| model | medium | medium | -| compute_type | float16 | float32 | -| language | ru | ru | - ### Опции CLI | Опция | Сокращение | По умолчанию | Описание | @@ -113,7 +98,76 @@ compute_type = "float16" | `--force` | `-f` | — | Перезаписать существующие транскрипты | | `--verbose` | `-v` | — | Подробный вывод | -## Модели +## Платформы + +| | Linux / WSL2 | macOS | Windows | +|---|---|---|---| +| CPU | ✅ | ✅ | ✅ | +| GPU (NVIDIA) | ✅ авто | — | ✅ (нужен CUDA toolkit) | + +
+Linux / WSL2 + +- GPU работает из коробки — cuBLAS ставится автоматически как зависимость +- Нужен только драйвер NVIDIA (проверка: `nvidia-smi`) +- На ARM (aarch64) cuBLAS через pip недоступен — нужен системный CUDA toolkit + +
+ +
+macOS + +- Работает на CPU (Intel и Apple Silicon) +- GPU (CUDA) недоступен — NVIDIA не поддерживает macOS + +
+ +
+Windows + +- CPU работает из коробки +- Для GPU нужен CUDA toolkit: + ``` + winget install -e --id Nvidia.CUDA + ``` + > `winget install` требует запуска от имени администратора (elevated terminal). + > `uv tool install` работает без админа (ставит в пользовательскую директорию). +- После установки CUDA перезапустите терминал + +
+ +## Конфигурация + +Дефолтные параметры можно задать в `.transcriber.toml`: + +```toml +model = "large-v3" +language = "en" +``` + +Порядок поиска: +1. `.transcriber.toml` в текущей директории +2. `~/.config/transcriber/config.toml` + +Приоритет: **CLI-аргумент > конфиг > device-aware дефолт > встроенный дефолт**. + +Дефолты зависят от устройства: + +| Параметр | GPU (CUDA) | CPU | +|----------|-----------|-----| +| model | medium | medium | +| compute_type | float16 | float32 | +| language | ru | ru | + +## Модели и GPU + +Рекомендации: +- **По умолчанию:** `medium` — хороший баланс скорости и качества +- **Макс. качество:** `large-v3` + `--compute-type float16` (GPU) +- **Быстрый тест:** `tiny` — для проверки пайплайна + +
+Таблица моделей | Модель | Размер на диске | VRAM (int8) | Скорость (GPU) | Качество | |--------|----------------|-------------|----------------|----------| @@ -123,7 +177,10 @@ compute_type = "float16" | `medium` | ~1.5 GB | ~2.5 GB | ★★ | ★★★★ | | `large-v3` | ~3 GB | ~2.5 GB | ★ | ★★★★★ | -### Типы квантизации (`--compute-type`) +
+ +
+Типы квантизации (--compute-type) | Тип | Устройство | VRAM/RAM | Качество | Когда использовать | |-----|-----------|----------|----------|--------------------| @@ -133,77 +190,14 @@ compute_type = "float16" | `float32` | CPU | Среднее | Отлично | **По умолчанию для CPU** | **Важно:** `int8` на длинных записях может давать галлюцинации (повтор фраз, потеря контента). -`float16` и `float32` значительно стабильнее на записях >20 минут с техническими терминами. +`float16` и `float32` значительно стабильнее на записях >20 минут. -## Установка ffmpeg +
-- **Linux / WSL2**: `sudo apt install ffmpeg` -- **macOS**: `brew install ffmpeg` -- **Windows**: `winget install ffmpeg` +Подробнее: бенчмарки, совместимость GPU, результаты тестирования — [docs/gpu.md](docs/gpu.md). -## GPU и CUDA - -Для работы с GPU необходим установленный NVIDIA драйвер -(проверка: `nvidia-smi` в терминале). Драйвер предоставляет `libcuda.so.1`, -без которого CUDA не работает — его нельзя поставить через pip. - -### По платформам - -- **Linux / WSL2 (x86_64)**: библиотека cuBLAS ставится автоматически - (зависимость `nvidia-cublas-cu12` подтягивается при установке). - Дополнительных шагов не требуется. - На ARM (aarch64) cuBLAS через pip недоступен — нужен системный CUDA toolkit. -- **Windows**: нужен системный CUDA toolkit - (`choco install cuda` или `winget install -e --id Nvidia.CUDA`) - -### Режимы `--device` - -- `auto` (по умолчанию) — выберет GPU если `nvidia-smi` доступен, иначе CPU -- `cuda` — строго GPU, ошибка если недоступен (без silent fallback) -- `cpu` — строго CPU - -### Совместимость GPU - -| GPU | VRAM | medium float16 | large-v3 float16 | Рекомендация | -|-----|------|---------------|-----------------|--------------| -| RTX 3060 | 6 GB | ✅ | ✅ | medium float16 (дефолт) | -| RTX 4050 | 6 GB | ✅ | ✅ | medium float16 | -| Quadro M3000M | 4 GB | ✅ | ⚠️ tight | medium float16 или int8 | - -### Ожидаемая скорость - -Замеры на RTX 3060 Laptop (6 GB) и Intel CPU (WSL2): - -| Конфигурация | 16 мин файл | 42 мин файл | Отн. скорость | -|-------------|-------------|-------------|---------------| -| GPU + medium float16 | ~35с | ~133с | ~19x реалтайм | -| GPU + large-v3 float16 | ~90с | ~350с | ~7x реалтайм | -| CPU + medium float32 | 613с (10 мин) | ~26 мин* | ~1.5x реалтайм | -| CPU + large-v3 int8 | 839с (14 мин) | ~37 мин* | ~1:1 реалтайм | - -*Оценка на основе пропорции. - -### Результаты тестирования качества - -Тесты проведены на реальных записях рабочих созвонов (русский язык, технические термины: -SQL, PostgreSQL, Greenplum, Airflow, ClickHouse, Docker, CDR, GTP, MAP). - -| Конфигурация | Качество (длинная запись, 42 мин) | Проблемы | -|---|---|---| -| large-v3 int8 GPU | Плохо | Галлюцинации (фразы ×25), потеря контента | -| large-v3 float16 GPU | Отлично | — | -| medium float16 GPU | Хорошо | Редкие мелкие ляпы в терминах | -| medium float32 CPU | Хорошо | Сопоставимо с large-v3 int8, без галлюцинаций | -| large-v3 int8 CPU | Хорошо | Без галлюцинаций (на коротких файлах) | - -**Ключевые выводы:** - -1. **Указание языка (`--language ru`) критично** — auto-detect может ошибиться и выдать мусор -2. **float16/float32 стабильнее int8** — особенно на записях >20 минут -3. **medium + float16 на GPU — лучший баланс** скорости и качества для повседневного использования -4. **large-v3 + float16 на GPU** — для максимального качества важных записей - -## Формат выходного файла +
+Формат вывода ```markdown # Транскрипт: meeting.mp4 @@ -217,20 +211,29 @@ SQL, PostgreSQL, Greenplum, Airflow, ClickHouse, Docker, CDR, GTP, MAP). --- [00:00:00.00 - 00:00:15.40] Добрый день, коллеги. Сегодня мы обсудим результаты -квартала. Первый вопрос — по метрикам продукта. Как вы видите на слайде, MAU вырос -на двадцать три процента по сравнению с предыдущим кварталом. +квартала. Первый вопрос — по метрикам продукта. [00:00:18.10 - 00:00:25.73] Теперь перейдём к финансовым показателям. - -... ``` Близкие по времени сегменты автоматически объединяются в абзацы (пауза > 2 сек или длительность > 60 сек разделяет абзацы). Таймкоды: `MM:SS.ss`, для записей длиннее 1 часа — `HH:MM:SS.ss`. +
+ ## Поддерживаемые форматы - **Аудио**: mp3, wav, flac, ogg, m4a, wma, aac - **Видео**: mp4, mkv, avi, mov, webm, ts -Формат определяется по расширению, декодирование выполняет ffmpeg. +## Для разработчиков + +```bash +git clone https://github.com/dementev-dev/local-transcriber +cd local-transcriber +uv sync +uv run transcribe meeting.mp4 # запуск CLI +uv run pytest # тесты +``` + +Подробнее — [CONTRIBUTING.md](CONTRIBUTING.md). diff --git a/docs/gpu.md b/docs/gpu.md new file mode 100644 index 0000000..c5a3018 --- /dev/null +++ b/docs/gpu.md @@ -0,0 +1,90 @@ +# GPU и CUDA + +## Режимы `--device` + +- `auto` (по умолчанию) — выберет GPU если `nvidia-smi` доступен, иначе CPU +- `cuda` — строго GPU, ошибка если недоступен (без silent fallback) +- `cpu` — строго CPU + +## Настройка по платформам + +### Linux / WSL2 (x86_64) + +Библиотека cuBLAS ставится автоматически (зависимость `nvidia-cublas-cu12` подтягивается при установке). +Дополнительных шагов не требуется. + +> На ARM (aarch64) cuBLAS через pip недоступен — нужен системный CUDA toolkit. + +### Windows + +Нужен системный CUDA toolkit: + +```bash +choco install cuda +# или +winget install -e --id Nvidia.CUDA # требует запуска от имени администратора +``` + +После установки перезапустите терминал. + +## Совместимость GPU + +| GPU | VRAM | medium float16 | large-v3 float16 | Рекомендация | +|-----|------|---------------|-----------------|--------------| +| RTX 3060 | 6 GB | ✅ | ✅ | medium float16 (дефолт) | +| RTX 4050 | 6 GB | ✅ | ✅ | medium float16 | +| Quadro M3000M | 4 GB | ✅ | ⚠️ tight | medium float16 или int8 | + +## Ожидаемая скорость + +Замеры на RTX 3060 Laptop (6 GB) и Intel CPU (WSL2): + +| Конфигурация | 16 мин файл | 42 мин файл | Отн. скорость | +|-------------|-------------|-------------|---------------| +| GPU + medium float16 | ~35с | ~133с | ~19x реалтайм | +| GPU + large-v3 float16 | ~90с | ~350с | ~7x реалтайм | +| CPU + medium float32 | 613с (10 мин) | ~26 мин* | ~1.5x реалтайм | +| CPU + large-v3 int8 | 839с (14 мин) | ~37 мин* | ~1:1 реалтайм | + +*Оценка на основе пропорции. + +## Результаты тестирования качества + +Тесты проведены на реальных записях рабочих созвонов (русский язык, технические термины: +SQL, PostgreSQL, Greenplum, Airflow, ClickHouse, Docker, CDR, GTP, MAP). + +| Конфигурация | Качество (длинная запись, 42 мин) | Проблемы | +|---|---|---| +| large-v3 int8 GPU | Плохо | Галлюцинации (фразы ×25), потеря контента | +| large-v3 float16 GPU | Отлично | — | +| medium float16 GPU | Хорошо | Редкие мелкие ляпы в терминах | +| medium float32 CPU | Хорошо | Сопоставимо с large-v3 int8, без галлюцинаций | +| large-v3 int8 CPU | Хорошо | Без галлюцинаций (на коротких файлах) | + +### Ключевые выводы + +1. **Указание языка (`--language ru`) критично** — auto-detect может ошибиться и выдать мусор +2. **float16/float32 стабильнее int8** — особенно на записях >20 минут +3. **medium + float16 на GPU — лучший баланс** скорости и качества для повседневного использования +4. **large-v3 + float16 на GPU** — для максимального качества важных записей + +## Troubleshooting + +### CUDA не обнаружен + +Убедитесь, что `nvidia-smi` возвращает информацию о GPU. Драйвер NVIDIA предоставляет `libcuda.so.1`, +без которого CUDA не работает — его нельзя поставить через pip. + +### Windows: ошибка при загрузке модели на GPU + +GPU на Windows требует CUDA toolkit (включает cuBLAS). Установите: +```bash +choco install cuda +# или +winget install -e --id Nvidia.CUDA +``` +После установки перезапустите терминал. + +### ARM (aarch64) + +cuBLAS через pip недоступен на ARM — нужен системный CUDA toolkit. diff --git a/src/local_transcriber/cli.py b/src/local_transcriber/cli.py index 479d74e..010c3e9 100644 --- a/src/local_transcriber/cli.py +++ b/src/local_transcriber/cli.py @@ -18,7 +18,6 @@ from .transcriber import ( ) from .utils import ( build_output_path, - check_ffmpeg, detect_device, expand_globs, get_gpu_name, @@ -111,7 +110,6 @@ def _run_single( ) -> None: start = time.monotonic() - check_ffmpeg() validated_file = validate_input_file(file) requested_device = defaults["device"] resolved_device = detect_device(requested_device) @@ -198,8 +196,6 @@ def _run_batch( verbose: bool, force: bool, ) -> None: - check_ffmpeg() - # Phase 1: Prescan to_process: list[Path] = [] skipped = 0 diff --git a/src/local_transcriber/utils.py b/src/local_transcriber/utils.py index 91abfc9..07085fb 100644 --- a/src/local_transcriber/utils.py +++ b/src/local_transcriber/utils.py @@ -1,7 +1,6 @@ import glob import shutil import subprocess -import sys import warnings from pathlib import Path @@ -11,18 +10,6 @@ SUPPORTED_EXTENSIONS = { } -def check_ffmpeg() -> None: - try: - subprocess.run(["ffmpeg", "-version"], capture_output=True, check=False) - except FileNotFoundError: - sys.exit( - "ffmpeg не найден в PATH. Установите ffmpeg:\n" - " Linux: apt install ffmpeg\n" - " Windows: winget install ffmpeg\n" - " macOS: brew install ffmpeg" - ) - - def detect_device(requested: str = "auto") -> str: if requested != "auto": return requested diff --git a/tests/test_cli.py b/tests/test_cli.py index 0f15324..2cef38b 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -40,7 +40,6 @@ def _single_patches(result=None, tmp_file=None, actual_device="cpu"): tfr = TranscribeFileResult(result=result, model=model, actual_device=actual_device) return [ patch("local_transcriber.cli.load_config", return_value={}), - patch("local_transcriber.cli.check_ffmpeg"), patch("local_transcriber.cli.validate_input_file", return_value=tmp_file), patch("local_transcriber.cli.detect_device", return_value=actual_device), patch("local_transcriber.cli.ensure_model_available", return_value="/models/medium"), @@ -55,7 +54,7 @@ def test_cli_happy_path_exit_code_zero(tmp_path): audio.write_bytes(b"fake") patches = _single_patches(tmp_file=audio) - with patches[0], patches[1], patches[2], patches[3], patches[4], patches[5], patches[6], patches[7]: + with patches[0], patches[1], patches[2], patches[3], patches[4], patches[5], patches[6]: out = runner.invoke(app, [str(audio)]) assert out.exit_code == 0 @@ -71,7 +70,6 @@ def test_cli_default_options_passed_to_transcribe(tmp_path): with ( patch("local_transcriber.cli.load_config", return_value={}), - patch("local_transcriber.cli.check_ffmpeg"), patch("local_transcriber.cli.validate_input_file", return_value=audio), patch("local_transcriber.cli.detect_device", return_value="cpu"), patch("local_transcriber.cli.ensure_model_available", return_value="/models/medium"), @@ -98,7 +96,6 @@ def test_cli_custom_options(tmp_path): with ( patch("local_transcriber.cli.load_config", return_value={}), - patch("local_transcriber.cli.check_ffmpeg"), patch("local_transcriber.cli.validate_input_file", return_value=audio), patch("local_transcriber.cli.detect_device", return_value="cuda"), patch("local_transcriber.cli.ensure_model_available", return_value="/models/small"), @@ -131,7 +128,6 @@ def test_cli_verbose_passes_on_segment_callback(tmp_path): with ( patch("local_transcriber.cli.load_config", return_value={}), - patch("local_transcriber.cli.check_ffmpeg"), patch("local_transcriber.cli.validate_input_file", return_value=audio), patch("local_transcriber.cli.detect_device", return_value="cpu"), patch("local_transcriber.cli.ensure_model_available", return_value="/models/medium"), @@ -152,7 +148,7 @@ def test_cli_empty_speech_warning(tmp_path): result = _make_result(segments=[]) patches = _single_patches(result=result, tmp_file=audio) - with patches[0], patches[1], patches[2], patches[3], patches[4], patches[5], patches[6], patches[7]: + with patches[0], patches[1], patches[2], patches[3], patches[4], patches[5], patches[6]: out = runner.invoke(app, [str(audio)]) assert out.exit_code == 0 @@ -170,7 +166,6 @@ def test_cli_default_output_path(tmp_path): with ( patch("local_transcriber.cli.load_config", return_value={}), - patch("local_transcriber.cli.check_ffmpeg"), patch("local_transcriber.cli.validate_input_file", return_value=audio), patch("local_transcriber.cli.detect_device", return_value="cpu"), patch("local_transcriber.cli.ensure_model_available", return_value="/models/medium"), @@ -196,7 +191,6 @@ def test_cli_custom_output_path(tmp_path): with ( patch("local_transcriber.cli.load_config", return_value={}), - patch("local_transcriber.cli.check_ffmpeg"), patch("local_transcriber.cli.validate_input_file", return_value=audio), patch("local_transcriber.cli.detect_device", return_value="cpu"), patch("local_transcriber.cli.ensure_model_available", return_value="/models/medium"), @@ -210,19 +204,6 @@ def test_cli_custom_output_path(tmp_path): assert written_path == out_file -def test_cli_error_exit_code_one(tmp_path): - audio = tmp_path / "test.mp3" - audio.write_bytes(b"fake") - - with ( - patch("local_transcriber.cli.load_config", return_value={}), - patch("local_transcriber.cli.check_ffmpeg", side_effect=SystemExit(1)), - ): - out = runner.invoke(app, [str(audio)]) - - assert out.exit_code == 1 - - def test_cli_passes_status_callback_to_transcribe(tmp_path): audio = tmp_path / "test.mp3" audio.write_bytes(b"fake") @@ -233,7 +214,6 @@ def test_cli_passes_status_callback_to_transcribe(tmp_path): with ( patch("local_transcriber.cli.load_config", return_value={}), - patch("local_transcriber.cli.check_ffmpeg"), patch("local_transcriber.cli.validate_input_file", return_value=audio), patch("local_transcriber.cli.detect_device", return_value="cpu"), patch("local_transcriber.cli.ensure_model_available", return_value="/models/medium"), @@ -258,7 +238,6 @@ def test_cli_resolves_model_before_transcribe(tmp_path): with ( patch("local_transcriber.cli.load_config", return_value={}), - patch("local_transcriber.cli.check_ffmpeg"), patch("local_transcriber.cli.validate_input_file", return_value=audio), patch("local_transcriber.cli.detect_device", return_value="cpu"), patch("local_transcriber.cli.ensure_model_available", return_value="/models/large-v3") as mock_ensure, @@ -281,7 +260,6 @@ def test_cli_windows_cuda_diagnostic(tmp_path): with ( patch("local_transcriber.cli.load_config", return_value={}), - patch("local_transcriber.cli.check_ffmpeg"), patch("local_transcriber.cli.validate_input_file", return_value=audio), patch("local_transcriber.cli.detect_device", return_value="cuda"), patch("local_transcriber.cli.ensure_model_available", return_value="/models/medium"), @@ -305,7 +283,6 @@ def test_cli_linux_cuda_error_no_windows_hint(tmp_path): with ( patch("local_transcriber.cli.load_config", return_value={}), - patch("local_transcriber.cli.check_ffmpeg"), patch("local_transcriber.cli.validate_input_file", return_value=audio), patch("local_transcriber.cli.detect_device", return_value="cuda"), patch("local_transcriber.cli.ensure_model_available", return_value="/models/medium"), @@ -330,7 +307,6 @@ def test_cli_device_fallback_warning(tmp_path): with ( patch("local_transcriber.cli.load_config", return_value={}), - patch("local_transcriber.cli.check_ffmpeg"), patch("local_transcriber.cli.validate_input_file", return_value=audio), patch("local_transcriber.cli.detect_device", return_value="cuda"), patch("local_transcriber.cli.ensure_model_available", return_value="/models/medium"), @@ -354,7 +330,6 @@ def test_cli_strict_device_passed_to_transcribe(tmp_path): with ( patch("local_transcriber.cli.load_config", return_value={}), - patch("local_transcriber.cli.check_ffmpeg"), patch("local_transcriber.cli.validate_input_file", return_value=audio), patch("local_transcriber.cli.detect_device", return_value="cuda"), patch("local_transcriber.cli.ensure_model_available", return_value="/models/medium"), @@ -374,7 +349,6 @@ def test_cli_strict_device_passed_to_transcribe(tmp_path): with ( patch("local_transcriber.cli.load_config", return_value={}), - patch("local_transcriber.cli.check_ffmpeg"), patch("local_transcriber.cli.validate_input_file", return_value=audio), patch("local_transcriber.cli.detect_device", return_value="cpu"), patch("local_transcriber.cli.ensure_model_available", return_value="/models/medium"), @@ -395,7 +369,6 @@ def test_cli_keyboard_interrupt(tmp_path): with ( patch("local_transcriber.cli.load_config", return_value={}), - patch("local_transcriber.cli.check_ffmpeg"), patch("local_transcriber.cli.validate_input_file", return_value=audio), patch("local_transcriber.cli.detect_device", return_value="cpu"), patch("local_transcriber.cli.ensure_model_available", return_value="/models/medium"), @@ -429,7 +402,6 @@ def test_cli_unexpected_error_verbose_traceback(tmp_path): with ( patch("local_transcriber.cli.load_config", return_value={}), - patch("local_transcriber.cli.check_ffmpeg"), patch("local_transcriber.cli.validate_input_file", return_value=audio), patch("local_transcriber.cli.detect_device", return_value="cpu"), patch("local_transcriber.cli.ensure_model_available", return_value="/models/medium"), @@ -451,7 +423,6 @@ def test_cli_unexpected_error_no_verbose_hint(tmp_path): with ( patch("local_transcriber.cli.load_config", return_value={}), - patch("local_transcriber.cli.check_ffmpeg"), patch("local_transcriber.cli.validate_input_file", return_value=audio), patch("local_transcriber.cli.detect_device", return_value="cpu"), patch("local_transcriber.cli.ensure_model_available", return_value="/models/medium"), @@ -481,7 +452,6 @@ def test_cli_batch_two_files(tmp_path): with ( patch("local_transcriber.cli.load_config", return_value={}), - patch("local_transcriber.cli.check_ffmpeg"), patch("local_transcriber.cli.validate_input_file", side_effect=lambda p: p), patch("local_transcriber.cli.detect_device", return_value="cpu"), patch("local_transcriber.cli.ensure_model_available", return_value="/models/medium"), @@ -509,7 +479,6 @@ def test_cli_batch_skips_existing(tmp_path): with ( patch("local_transcriber.cli.load_config", return_value={}), - patch("local_transcriber.cli.check_ffmpeg"), patch("local_transcriber.cli.validate_input_file", side_effect=lambda p: p), patch("local_transcriber.cli.detect_device", return_value="cpu"), patch("local_transcriber.cli.ensure_model_available", return_value="/models/medium"), @@ -537,7 +506,6 @@ def test_cli_batch_all_skipped_no_model_load(tmp_path): with ( patch("local_transcriber.cli.load_config", return_value={}), - patch("local_transcriber.cli.check_ffmpeg"), patch("local_transcriber.cli.validate_input_file", side_effect=lambda p: p), patch("local_transcriber.cli.load_model", mock_load_model), ): @@ -560,7 +528,6 @@ def test_cli_batch_force_overwrites(tmp_path): with ( patch("local_transcriber.cli.load_config", return_value={}), - patch("local_transcriber.cli.check_ffmpeg"), patch("local_transcriber.cli.validate_input_file", side_effect=lambda p: p), patch("local_transcriber.cli.detect_device", return_value="cpu"), patch("local_transcriber.cli.ensure_model_available", return_value="/models/medium"), @@ -595,7 +562,6 @@ def test_cli_batch_per_file_error(tmp_path): with ( patch("local_transcriber.cli.load_config", return_value={}), - patch("local_transcriber.cli.check_ffmpeg"), patch("local_transcriber.cli.validate_input_file", side_effect=lambda p: p), patch("local_transcriber.cli.detect_device", return_value="cpu"), patch("local_transcriber.cli.ensure_model_available", return_value="/models/medium"), @@ -627,7 +593,6 @@ def test_cli_batch_invalid_in_prescan(tmp_path): with ( patch("local_transcriber.cli.load_config", return_value={}), - patch("local_transcriber.cli.check_ffmpeg"), patch("local_transcriber.cli.validate_input_file", side_effect=validate_side_effect), patch("local_transcriber.cli.detect_device", return_value="cpu"), patch("local_transcriber.cli.ensure_model_available", return_value="/models/medium"), @@ -674,7 +639,6 @@ def test_cli_config_applied(tmp_path): with ( patch("local_transcriber.cli.load_config", return_value={"model": "tiny"}), - patch("local_transcriber.cli.check_ffmpeg"), patch("local_transcriber.cli.validate_input_file", return_value=audio), patch("local_transcriber.cli.detect_device", return_value="cpu"), patch("local_transcriber.cli.ensure_model_available", return_value="/models/tiny") as mock_ensure, @@ -696,7 +660,6 @@ def test_cli_cli_overrides_config(tmp_path): with ( patch("local_transcriber.cli.load_config", return_value={"model": "tiny"}), - patch("local_transcriber.cli.check_ffmpeg"), patch("local_transcriber.cli.validate_input_file", return_value=audio), patch("local_transcriber.cli.detect_device", return_value="cpu"), patch("local_transcriber.cli.ensure_model_available", return_value="/models/small") as mock_ensure, @@ -722,7 +685,6 @@ def test_cli_batch_fallback_warning(tmp_path): with ( patch("local_transcriber.cli.load_config", return_value={}), - patch("local_transcriber.cli.check_ffmpeg"), patch("local_transcriber.cli.validate_input_file", side_effect=lambda p: p), patch("local_transcriber.cli.detect_device", return_value="cuda"), patch("local_transcriber.cli.ensure_model_available", return_value="/models/medium"), @@ -750,7 +712,6 @@ def test_cli_batch_empty_speech_warning(tmp_path): with ( patch("local_transcriber.cli.load_config", return_value={}), - patch("local_transcriber.cli.check_ffmpeg"), patch("local_transcriber.cli.validate_input_file", side_effect=lambda p: p), patch("local_transcriber.cli.detect_device", return_value="cpu"), patch("local_transcriber.cli.ensure_model_available", return_value="/models/medium"), @@ -781,7 +742,6 @@ def test_cli_batch_midstream_fallback_warning(tmp_path): with ( patch("local_transcriber.cli.load_config", return_value={}), - patch("local_transcriber.cli.check_ffmpeg"), patch("local_transcriber.cli.validate_input_file", side_effect=lambda p: p), patch("local_transcriber.cli.detect_device", return_value="cuda"), patch("local_transcriber.cli.ensure_model_available", return_value="/models/medium"), @@ -808,7 +768,6 @@ def test_cli_batch_model_loaded_once(tmp_path): with ( patch("local_transcriber.cli.load_config", return_value={}), - patch("local_transcriber.cli.check_ffmpeg"), patch("local_transcriber.cli.validate_input_file", side_effect=lambda p: p), patch("local_transcriber.cli.detect_device", return_value="cpu"), patch("local_transcriber.cli.ensure_model_available", return_value="/models/medium"),