feat(utils): реализованы проверки окружения (шаг 2)
- Зачем:
- необходимы утилиты для проверки ffmpeg, определения устройства и
валидации входного файла перед запуском транскрипции.
- Что:
- check_ffmpeg() завершает процесс с понятным сообщением, если ffmpeg не в PATH.
- detect_device() возвращает "cuda" при наличии nvidia-smi, иначе "cpu".
- get_gpu_name() получает имя GPU через nvidia-smi или возвращает None.
- validate_input_file() проверяет существование, тип и размер файла;
неизвестное расширение — warning, не ошибка.
- build_output_path() формирует путь <stem>-transcript.md рядом с исходником.
- Проверка:
- uv run pytest tests/test_utils.py -v — 9 passed.
- uv run python -c "from local_transcriber.utils import check_ffmpeg, detect_device; check_ffmpeg(); print(detect_device())"
This commit is contained in:
+6
-6
@@ -119,10 +119,10 @@
|
|||||||
|
|
||||||
> PRD-ссылки: 3.1 (flow), 3.2 (опции --device), 3.4 (форматы), 4.2 (ffmpeg)
|
> PRD-ссылки: 3.1 (flow), 3.2 (опции --device), 3.4 (форматы), 4.2 (ffmpeg)
|
||||||
|
|
||||||
- [ ] `check_ffmpeg()`:
|
- [x] `check_ffmpeg()`:
|
||||||
- `subprocess.run(["ffmpeg", "-version"], capture_output=True)`
|
- `subprocess.run(["ffmpeg", "-version"], capture_output=True)`
|
||||||
- При `FileNotFoundError` → `SystemExit` с сообщением и инструкцией: `apt install ffmpeg` / `winget install ffmpeg` / `brew install ffmpeg`
|
- При `FileNotFoundError` → `SystemExit` с сообщением и инструкцией: `apt install ffmpeg` / `winget install ffmpeg` / `brew install ffmpeg`
|
||||||
- [ ] `detect_device(requested: str = "auto") -> str`:
|
- [x] `detect_device(requested: str = "auto") -> str`:
|
||||||
- Если `requested != "auto"` → вернуть `requested`
|
- Если `requested != "auto"` → вернуть `requested`
|
||||||
- Иначе: проверить CUDA через `shutil.which("nvidia-smi")` как быстрый хинт
|
- Иначе: проверить CUDA через `shutil.which("nvidia-smi")` как быстрый хинт
|
||||||
- Если nvidia-smi найден → `"cuda"`
|
- Если nvidia-smi найден → `"cuda"`
|
||||||
@@ -130,19 +130,19 @@
|
|||||||
- **Не импортировать** ctranslate2 или torch здесь — faster-whisper ещё не в зависимостях
|
- **Не импортировать** ctranslate2 или torch здесь — faster-whisper ещё не в зависимостях
|
||||||
- Точная проверка CUDA будет при загрузке модели (шаг 3), здесь — best effort
|
- Точная проверка CUDA будет при загрузке модели (шаг 3), здесь — best effort
|
||||||
- `--compute-type` остаётся независимым параметром CLI, не связан с detect_device
|
- `--compute-type` остаётся независимым параметром CLI, не связан с detect_device
|
||||||
- [ ] `get_gpu_name() -> str | None`:
|
- [x] `get_gpu_name() -> str | None`:
|
||||||
- `subprocess.run(["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"], capture_output=True)`
|
- `subprocess.run(["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"], capture_output=True)`
|
||||||
- Вернуть первую строку stdout (strip) или `None` если nvidia-smi недоступен / ошибка
|
- Вернуть первую строку stdout (strip) или `None` если nvidia-smi недоступен / ошибка
|
||||||
- Используется для формирования `device_info` в шапке markdown: `"CUDA (NVIDIA GeForce RTX 3060)"` или `"CPU"`
|
- Используется для формирования `device_info` в шапке markdown: `"CUDA (NVIDIA GeForce RTX 3060)"` или `"CPU"`
|
||||||
- [ ] `validate_input_file(path: Path) -> Path`:
|
- [x] `validate_input_file(path: Path) -> Path`:
|
||||||
- Проверить: существует, является файлом (не директорией), размер > 0
|
- Проверить: существует, является файлом (не директорией), размер > 0
|
||||||
- Расширение из допустимых (PRD 3.4) → если нет, **warning** (не ошибка), продолжить
|
- Расширение из допустимых (PRD 3.4) → если нет, **warning** (не ошибка), продолжить
|
||||||
- Вернуть `path.resolve()`
|
- Вернуть `path.resolve()`
|
||||||
- [ ] `build_output_path(input_path: Path, output: Path | None = None) -> Path`:
|
- [x] `build_output_path(input_path: Path, output: Path | None = None) -> Path`:
|
||||||
- Если `output` задан → вернуть его
|
- Если `output` задан → вернуть его
|
||||||
- Иначе → `input_path.with_stem(input_path.stem + "-transcript").with_suffix(".md")`
|
- Иначе → `input_path.with_stem(input_path.stem + "-transcript").with_suffix(".md")`
|
||||||
|
|
||||||
- [ ] Тесты в `tests/test_utils.py`:
|
- [x] Тесты в `tests/test_utils.py`:
|
||||||
- `test_validate_input_file_not_found` — несуществующий файл → ошибка
|
- `test_validate_input_file_not_found` — несуществующий файл → ошибка
|
||||||
- `test_validate_input_file_empty` — пустой файл → ошибка
|
- `test_validate_input_file_empty` — пустой файл → ошибка
|
||||||
- `test_validate_input_file_unknown_ext` — `.txt` → warning, но не ошибка
|
- `test_validate_input_file_unknown_ext` — `.txt` → warning, но не ошибка
|
||||||
|
|||||||
@@ -1,21 +1,68 @@
|
|||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import warnings
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
SUPPORTED_EXTENSIONS = {
|
||||||
|
".mp3", ".wav", ".flac", ".ogg", ".m4a", ".wma", ".aac",
|
||||||
|
".mp4", ".mkv", ".avi", ".mov", ".webm", ".ts",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def check_ffmpeg() -> None:
|
def check_ffmpeg() -> None:
|
||||||
raise NotImplementedError
|
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:
|
def detect_device(requested: str = "auto") -> str:
|
||||||
raise NotImplementedError
|
if requested != "auto":
|
||||||
|
return requested
|
||||||
|
if shutil.which("nvidia-smi") is not None:
|
||||||
|
return "cuda"
|
||||||
|
return "cpu"
|
||||||
|
|
||||||
|
|
||||||
def get_gpu_name() -> str | None:
|
def get_gpu_name() -> str | None:
|
||||||
raise NotImplementedError
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
name = result.stdout.strip().splitlines()[0].strip()
|
||||||
|
return name if name else None
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def validate_input_file(path: Path) -> Path:
|
def validate_input_file(path: Path) -> Path:
|
||||||
raise NotImplementedError
|
if not path.exists():
|
||||||
|
raise FileNotFoundError(f"Файл не найден: {path}")
|
||||||
|
if not path.is_file():
|
||||||
|
raise ValueError(f"Путь не является файлом: {path}")
|
||||||
|
if path.stat().st_size == 0:
|
||||||
|
raise ValueError(f"Файл пустой: {path}")
|
||||||
|
if path.suffix.lower() not in SUPPORTED_EXTENSIONS:
|
||||||
|
warnings.warn(
|
||||||
|
f"Расширение '{path.suffix}' не входит в список поддерживаемых. "
|
||||||
|
"Попытка продолжить.",
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
return path.resolve()
|
||||||
|
|
||||||
|
|
||||||
def build_output_path(input_path: Path, output: Path | None = None) -> Path:
|
def build_output_path(input_path: Path, output: Path | None = None) -> Path:
|
||||||
raise NotImplementedError
|
if output is not None:
|
||||||
|
return output
|
||||||
|
return input_path.with_stem(input_path.stem + "-transcript").with_suffix(".md")
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from local_transcriber.utils import (
|
||||||
|
build_output_path,
|
||||||
|
detect_device,
|
||||||
|
get_gpu_name,
|
||||||
|
validate_input_file,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_input_file_not_found(tmp_path):
|
||||||
|
missing = tmp_path / "no_such_file.mp3"
|
||||||
|
with pytest.raises(FileNotFoundError):
|
||||||
|
validate_input_file(missing)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_input_file_empty(tmp_path):
|
||||||
|
empty = tmp_path / "empty.mp3"
|
||||||
|
empty.touch()
|
||||||
|
with pytest.raises(ValueError, match="пустой"):
|
||||||
|
validate_input_file(empty)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_input_file_unknown_ext(tmp_path):
|
||||||
|
f = tmp_path / "notes.txt"
|
||||||
|
f.write_text("hello")
|
||||||
|
with pytest.warns(UserWarning, match="не входит в список"):
|
||||||
|
result = validate_input_file(f)
|
||||||
|
assert result == f.resolve()
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_input_file_ok(tmp_path):
|
||||||
|
f = tmp_path / "audio.mp3"
|
||||||
|
f.write_bytes(b"\x00" * 16)
|
||||||
|
result = validate_input_file(f)
|
||||||
|
assert result == f.resolve()
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_output_path_default():
|
||||||
|
inp = Path("/some/dir/meeting-2026-03-17.mp4")
|
||||||
|
result = build_output_path(inp)
|
||||||
|
assert result == Path("/some/dir/meeting-2026-03-17-transcript.md")
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_output_path_custom():
|
||||||
|
inp = Path("/some/dir/audio.mp3")
|
||||||
|
custom = Path("/out/result.md")
|
||||||
|
result = build_output_path(inp, output=custom)
|
||||||
|
assert result == custom
|
||||||
|
|
||||||
|
|
||||||
|
def test_detect_device_explicit():
|
||||||
|
assert detect_device("cpu") == "cpu"
|
||||||
|
assert detect_device("cuda") == "cuda"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_gpu_name_no_nvidia_smi():
|
||||||
|
with patch("shutil.which", return_value=None):
|
||||||
|
with patch("subprocess.run", side_effect=FileNotFoundError):
|
||||||
|
result = get_gpu_name()
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_gpu_name_success():
|
||||||
|
mock_result = subprocess.CompletedProcess(
|
||||||
|
args=[], returncode=0, stdout="NVIDIA GeForce RTX 3060\n", stderr=""
|
||||||
|
)
|
||||||
|
with patch("subprocess.run", return_value=mock_result):
|
||||||
|
result = get_gpu_name()
|
||||||
|
assert result == "NVIDIA GeForce RTX 3060"
|
||||||
|
|||||||
Reference in New Issue
Block a user