docs(readme): редизайн README и удаление зависимости от ffmpeg

- Зачем:
  - README перегружен деталями GPU/бенчмарков, нет Quick Start, macOS/Windows — второй класс.
  - системный ffmpeg не нужен — PyAV (зависимость faster-whisper) включает FFmpeg в wheels.
- Что:
  - переписан README: Quick Start в первых строках, установка без клонирования через uv tool install, паритет платформ, progressive disclosure через <details>.
  - удалена функция check_ffmpeg() из utils.py, убраны импорт и вызовы из cli.py.
  - убраны моки check_ffmpeg и тест ffmpeg-ошибки из test_cli.py.
  - GPU-контент вынесен в docs/gpu.md, добавлен CONTRIBUTING.md.
- Проверка:
  - uv run pytest — 97 passed, 1 skipped.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-18 23:07:39 +03:00
co-authored by Claude Opus 4.6
parent b7e6ab634a
commit 87f7dc1723
6 changed files with 265 additions and 185 deletions
+2 -43
View File
@@ -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"),