From 557190e8a9b639999deb82b6db7448e50df5536b Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Tue, 17 Mar 2026 21:21:24 +0300 Subject: [PATCH] =?UTF-8?q?docs(project):=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=8B=20PRD=20=D0=B8=20=D0=BF=D0=BB=D0=B0?= =?UTF-8?q?=D0=BD=20=D1=80=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - зафиксированы требования к проекту и план разработки для выравнивания команды. - Что: - добавлен docs/PRD.md с требованиями к продукту. - добавлен docs/plan.md с планом реализации. - Проверка: - открыть docs/PRD.md и docs/plan.md и убедиться в корректности содержимого. --- docs/PRD.md | 201 ++++++++++++++++++++++++++++++++++++++ docs/plan.md | 268 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 469 insertions(+) create mode 100644 docs/PRD.md create mode 100644 docs/plan.md diff --git a/docs/PRD.md b/docs/PRD.md new file mode 100644 index 0000000..dc60191 --- /dev/null +++ b/docs/PRD.md @@ -0,0 +1,201 @@ +# PRD: local-transcriber — Локальный CLI для транскрипции аудио/видео + +## 1. Обзор продукта + +**Название**: `local-transcriber` +**Тип**: CLI-утилита (Python, управление зависимостями через uv) +**Назначение**: Принимает аудио- или видеофайл, выполняет распознавание речи локально (без внешних API), и создаёт рядом с исходным файлом markdown-файл с транскриптом и таймкодами. + +**Пример использования**: +```bash +transcribe meeting-2026-03-17.mp4 +# → создаёт meeting-2026-03-17-transcript.md +``` + +## 2. Целевые пользователи и сценарии + +- Разработчик/инженер, которому нужен текст из записи встречи, лекции, подкаста +- Дальнейшая обработка транскрипта ИИ (суммаризация, извлечение action items и т.д.) — вне скоупа, но учитывается в формате вывода +- Машины с GPU (NVIDIA, CUDA) и без GPU (CPU-only fallback) +- Windows (native / WSL2) и Linux + +## 3. Функциональные требования + +### 3.1. Основной flow + +1. Пользователь вызывает CLI, передаёт путь к файлу (или glob-маску, post-MVP) +2. Проверка: ffmpeg доступен в PATH +3. Файл передаётся в faster-whisper (он сам обрабатывает и аудио, и видео через libav/ffmpeg — отдельное извлечение аудиодорожки не нужно) +4. Результат форматируется в markdown с таймкодами +5. Файл `<имя>-transcript.md` сохраняется рядом с исходным (кодировка: UTF-8) + +**Поведение при перезаписи**: +- Явный вызов по файлу → транскрипт перезаписывается, даже если уже существует +- Батч-режим (glob-маска, post-MVP) → пропускать файлы, для которых транскрипт уже существует; `--force` для принудительной перезаписи + +**Пустая речь**: если модель не распознала ни одного сегмента — файл транскрипта всё равно создаётся (с метаданными в шапке), выводится предупреждение: `⚠ Речь не обнаружена в файле <имя>` + +### 3.2. CLI-интерфейс + +``` +transcribe <путь_к_файлу> [опции] + +Опции: + --model, -m Модель Whisper (tiny|base|small|medium|large-v3) + По умолчанию: large-v3 + --language, -l Язык (ru|en|auto) + По умолчанию: auto (автодетект) + --output, -o Путь к выходному файлу + По умолчанию: -transcript.md + --device, -d Устройство (auto|cpu|cuda) + По умолчанию: auto (CUDA если доступен, иначе CPU) + --compute-type Тип вычислений (float16|int8|int8_float16|float32) + По умолчанию: int8 (универсален для GPU 4-8 GB и CPU) + --verbose, -v Подробный вывод (прогресс сегментов) +``` + +### 3.3. Формат выходного файла + +Файл `*-transcript.md`: + +```markdown +# Транскрипт: meeting-2026-03-17.mp4 + +- **Дата транскрипции**: 2026-03-17 14:30:05 +- **Модель**: large-v3 +- **Язык**: ru (detected) / ru (forced) +- **Длительность**: 01:23:45 +- **Устройство**: CUDA (NVIDIA GeForce RTX 3060) + +--- + +[00:00.00 - 00:04.82] Добрый день, коллеги. Сегодня мы обсудим результаты квартала. + +[00:04.82 - 00:09.15] Первый вопрос — по метрикам продукта. + +[00:09.15 - 00:15.40] Как вы видите на слайде, MAU вырос на двадцать три процента +по сравнению с предыдущим кварталом. + +... +``` + +**Правила форматирования**: +- Таймкоды в формате `[MM:SS.ss - MM:SS.ss]` (минуты:секунды.сотые) +- Для записей длиннее 1 часа — `[HH:MM:SS.ss - HH:MM:SS.ss]` +- Каждый сегмент — отдельный абзац +- Метаданные в шапке файла +- Пустая строка между сегментами для читаемости + +### 3.4. Поддерживаемые форматы + +**Аудио**: mp3, wav, flac, ogg, m4a, wma, aac +**Видео**: mp4, mkv, avi, mov, webm, ts + +Определение типа — по расширению. Фактическое декодирование выполняет ffmpeg внутри faster-whisper; если формат не поддерживается, ошибка будет от ffmpeg. + +## 4. Нефункциональные требования + +### 4.1. Производительность + +| Конфигурация | Ожидаемая скорость (real-time factor) | +|---------------------------|---------------------------------------| +| RTX 3060 + large-v3 | ~10-15x (1 час аудио ≈ 4-6 мин) | +| RTX 4050 + large-v3 | ~12-18x (1 час аудио ≈ 3-5 мин) | +| Quadro M3000M + large-v3 | ~3-5x (1 час аудио ≈ 12-20 мин) | +| CPU (modern) + large-v3 | ~0.5-1x (1 час аудио ≈ 60-120 мин) | +| CPU + small | ~3-5x (1 час аудио ≈ 12-20 мин) | + +### 4.1.1. Совместимость GPU + +| GPU | VRAM | large-v3 int8 (~2.5 GB) | large-v3 float16 (~4.5 GB) | Рекомендация | +|-------------------|-------|--------------------------|----------------------------|-------------------------| +| RTX 3060 | 6 GB | ✅ | ✅ (впритык) | int8 — безопасный выбор | +| RTX 4050 | 6 GB | ✅ | ✅ (впритык) | int8 — безопасный выбор | +| Quadro M3000M | 4 GB | ✅ | ⚠️ может OOM | int8 обязательно | +| Без GPU | — | CPU int8 | — | int8 на CPU | + +Дефолт `int8` выбран как универсальный: работает на всех GPU от 4 GB и на CPU, при минимальной потере качества относительно float16. + +### 4.2. Требования к окружению + +- Python ≥ 3.10 +- ffmpeg в PATH (используется faster-whisper внутри для декодирования любых медиаформатов) +- Для GPU: CUDA toolkit (cuBLAS, cuDNN) — ставится автоматически через CTranslate2 +- Дисковое пространство для моделей: ~3 GB (large-v3) +- Выходные файлы: UTF-8 (явная кодировка при записи) + +### 4.3. Кроссплатформенность + +- Linux: нативный запуск +- Windows: нативный Python или WSL2 +- macOS: не приоритет, но faster-whisper поддерживает CPU-режим + +## 5. Технический стек + +| Компонент | Технология | +|---------------------|-------------------------------------------------| +| Язык | Python 3.10+ | +| Управление проектом | uv (pyproject.toml) | +| Распознавание речи | faster-whisper (CTranslate2 backend) | +| Медиа-декодирование | ffmpeg (системная зависимость, используется faster-whisper внутри) | +| CLI-фреймворк | typer | +| Прогресс | rich (progress bar + статус) | + +### 5.1. Почему faster-whisper + +- В 4× быстрее оригинального OpenAI Whisper при том же качестве +- Меньше потребление VRAM (large-v3 влезает в 6 GB с float16/int8) +- Нативный Python API, без Docker +- Поддержка CPU fallback из коробки +- Активное сообщество, регулярные обновления +- Автоматическая загрузка моделей из Hugging Face Hub + +### 5.2. Структура проекта + +``` +local-transcriber/ +├── pyproject.toml +├── README.md +├── src/ +│ └── local_transcriber/ +│ ├── __init__.py +│ ├── cli.py # CLI entry point (typer) +│ ├── transcriber.py # Обёртка над faster-whisper +│ ├── formatter.py # Форматирование в markdown +│ └── utils.py # Проверки (ffmpeg), определение device и т.д. +└── tests/ + └── ... +``` + +## 6. Риски и ограничения + +| Риск | Влияние | Митигация | +|------|---------|-----------| +| Качество распознавания русского текста | Среднее | large-v3 хорошо справляется с ru; при проблемах — попробовать `--language ru` вместо auto | +| Нет разделения по спикерам | Низкое | Осознанно выведено за скоуп MVP; добавление diarization (pyannote.audio) — возможное расширение | +| ffmpeg отсутствует в системе | Высокое | Проверка при старте + понятное сообщение об ошибке с инструкцией по установке | +| Первый запуск: долгая загрузка модели | Низкое | Прогресс-бар при скачивании; модели кешируются в `~/.cache/huggingface/` | +| CUDA несовместимость на Windows | Среднее | Автоматический fallback на CPU + предупреждение; в README — инструкция по CUDA | +| Большие файлы (>2 часов) | Низкое | faster-whisper работает потоково, не грузит всё в память | +| OOM на GPU с 4 GB VRAM | Среднее | Дефолт int8 (~2.5 GB); при OOM — fallback на CPU с предупреждением | +| Файл без речи (тишина, музыка, шум) | Низкое | Создаётся транскрипт с пустым телом + предупреждение в stderr | + +## 7. Вне скоупа MVP + +- Разделение по спикерам (speaker diarization) +- Веб-интерфейс / GUI +- Пакетная обработка нескольких файлов (батч) — см. post-MVP +- Стриминг с микрофона (real-time) +- Интеграция с LLM для пост-обработки транскрипта +- Перевод (translation mode) +- Вывод в форматах SRT / VTT / JSON +- Аудиофильтрация / шумоподавление (faster-whisper сам нормализует; ручной препроцессинг может ухудшить результат) + +## 8. Возможные расширения (post-MVP) + +1. **Speaker diarization** — pyannote.audio, требует отдельной модели + GPU +2. **Батч-режим** — `transcribe ./recordings/*.mp4`; по умолчанию пропускает файлы, для которых транскрипт уже есть; `--force` для перезаписи +3. **Экспорт в SRT/VTT** — для субтитров +4. **Watch-режим** — мониторинг директории, автотранскрипция новых файлов +5. **Интеграция с LLM** — `--summarize` для генерации саммари поверх транскрипта +6. **Конфигурационный файл** — `.transcriber.toml` для дефолтов проекта diff --git a/docs/plan.md b/docs/plan.md new file mode 100644 index 0000000..09ee429 --- /dev/null +++ b/docs/plan.md @@ -0,0 +1,268 @@ +# plan.md — План реализации local-transcriber + +> Источник требований: docs/PRD.md +> Каждый шаг — атомарный коммит. После выполнения шага — отметить `[x]`. +> Агент: реализуй шаги последовательно. Читай ТОЛЬКО свой текущий шаг + секции PRD, на которые он ссылается. Не реализуй функциональность из других шагов. Не рефактори код предыдущих шагов без явной необходимости. + +--- + +## Шаг 1: Scaffold проекта (без ML-зависимостей) + +- [ ] Создать структуру: + ``` + local-transcriber/ + ├── pyproject.toml + ├── .gitignore + ├── src/ + │ └── local_transcriber/ + │ ├── __init__.py + │ ├── cli.py + │ ├── transcriber.py + │ ├── formatter.py + │ └── utils.py + └── tests/ + ├── __init__.py + └── test_formatter.py # заглушка + ``` +- [ ] `pyproject.toml`: + - `name = "local-transcriber"`, `python = ">=3.10"` + - `[project.scripts]`: `transcribe = "local_transcriber.cli:app"` + - dependencies: `typer`, `rich` (НЕ faster-whisper — он добавляется в шаге 3) + - dev-dependencies: `pytest` +- [ ] `.gitignore`: `__pycache__/`, `.venv/`, `*.egg-info/`, `.mypy_cache/`, `dist/`, `*.pyc` +- [ ] В каждом модуле — заглушки с сигнатурами и `raise NotImplementedError`: + + **utils.py**: + ```python + from pathlib import Path + + def check_ffmpeg() -> None: ... + def detect_device(requested: str = "auto") -> tuple[str, str]: ... + def validate_input_file(path: Path) -> Path: ... + def build_output_path(input_path: Path, output: Path | None = None) -> Path: ... + ``` + + **transcriber.py**: + ```python + from dataclasses import dataclass + from pathlib import Path + + @dataclass + class Segment: + start: float # секунды + end: float # секунды + text: str + + @dataclass + class TranscribeResult: + segments: list[Segment] + language: str + language_probability: float + duration: float # секунды + + def transcribe( + file_path: Path, + model_name: str = "large-v3", + device: str = "auto", + compute_type: str = "int8", + language: str | None = None, + ) -> TranscribeResult: ... + ``` + + **formatter.py**: + ```python + from pathlib import Path + from .transcriber import TranscribeResult + + def format_timestamp(seconds: float, use_hours: bool = False) -> str: ... + + def format_transcript( + result: TranscribeResult, + source_filename: str, + model_name: str, + device_info: str, + language_mode: str, + ) -> str: ... + + def write_transcript(content: str, output_path: Path) -> None: ... + ``` + + **cli.py**: + ```python + import typer + app = typer.Typer() + + @app.command() + def main(file: Path) -> None: + typer.echo("TODO: not implemented") + + if __name__ == "__main__": + app() + ``` + +- [ ] `uv sync` → `uv run transcribe --help` работает + +**Критерий готовности**: `uv run transcribe --help` показывает аргументы. `uv run pytest` проходит (тесты пустые, но pytest находит test_formatter.py). Все модули импортируются без ошибок. + +**Коммит**: `git add -A && git commit -m "step 1: scaffold with stubs and interfaces"` + +--- + +## Шаг 2: utils.py — проверки окружения + +> PRD-ссылки: 3.1 (flow), 3.2 (опции --device), 3.4 (форматы), 4.2 (ffmpeg) + +- [ ] `check_ffmpeg()`: + - `subprocess.run(["ffmpeg", "-version"], capture_output=True)` + - При `FileNotFoundError` → `SystemExit` с сообщением и инструкцией: `apt install ffmpeg` / `winget install ffmpeg` / `brew install ffmpeg` +- [ ] `detect_device(requested: str = "auto") -> tuple[str, str]`: + - Если `requested != "auto"` → вернуть `(requested, "int8")` + - Иначе: проверить CUDA через `shutil.which("nvidia-smi")` как быстрый хинт + - Если nvidia-smi найден → `("cuda", "int8")` + - Иначе → `("cpu", "int8")` + - **Не импортировать** ctranslate2 или torch здесь — faster-whisper ещё не в зависимостях + - Точная проверка CUDA будет при загрузке модели (шаг 3), здесь — best effort +- [ ] `validate_input_file(path: Path) -> Path`: + - Проверить: существует, является файлом (не директорией), размер > 0 + - Расширение из допустимых (PRD 3.4) → если нет, **warning** (не ошибка), продолжить + - Вернуть `path.resolve()` +- [ ] `build_output_path(input_path: Path, output: Path | None = None) -> Path`: + - Если `output` задан → вернуть его + - Иначе → `input_path.with_stem(input_path.stem + "-transcript").with_suffix(".md")` + +**Критерий готовности**: `uv run python -c "from local_transcriber.utils import check_ffmpeg, detect_device; check_ffmpeg(); print(detect_device())"` — работает на машине агента (CPU fallback). + +**Коммит**: `git add -A && git commit -m "step 2: utils — ffmpeg check, device detection, path helpers"` + +--- + +## Шаг 3: transcriber.py — обёртка над faster-whisper + +> PRD-ссылки: 5.1 (faster-whisper), 4.1.1 (GPU совместимость), 6 (риски OOM) + +- [ ] `uv add faster-whisper` — добавить в зависимости +- [ ] Реализовать `transcribe()`: + - Создать `WhisperModel(model_name, device=device, compute_type=compute_type)` + - При ошибке загрузки на CUDA (OOM, CUDA error) → **поймать**, вывести warning, **повторить с device="cpu"** + - `model.transcribe(str(file_path), language=language if language != "auto" else None)` + - faster-whisper возвращает `(segment_generator, info)` — итерировать generator, собрать в `list[Segment]` + - Заполнить `TranscribeResult` из info (language, duration и т.д.) +- [ ] Обработка ошибок: + - `RuntimeError` с "CUDA" / "out of memory" → fallback на CPU + warning + - Ошибка ffmpeg (невалидный медиафайл) → пробросить с понятным текстом + +**Критерий готовности**: на машине агента — `transcribe()` работает с `device="cpu"`, `model="tiny"` (быстро скачивается). Полноценная проверка с large-v3 и GPU — на локальной машине разработчика. + +**Коммит**: `git add -A && git commit -m "step 3: transcriber — faster-whisper wrapper with CUDA fallback"` + +--- + +## Шаг 4: formatter.py + тесты + +> PRD-ссылки: 3.3 (формат выходного файла) + +- [ ] `format_timestamp(seconds: float, use_hours: bool = False) -> str`: + - `False` → `"01:23.45"` (MM:SS.ss) + - `True` → `"01:23:45.67"` (HH:MM:SS.ss) + - Сотые — всегда 2 знака после точки +- [ ] `format_transcript(...)`: + - Шапка по шаблону PRD 3.3 (заголовок, метаданные, разделитель) + - Автоматически `use_hours=True` если `result.duration > 3600` + - Сегменты: `[MM:SS.ss - MM:SS.ss] текст\n\n` + - Если `len(result.segments) == 0` → после разделителя: `\n*Речь не обнаружена.*\n` +- [ ] `write_transcript(content: str, output_path: Path)`: + - `open(output_path, "w", encoding="utf-8")` +- [ ] Тесты в `tests/test_formatter.py`: + - `test_format_timestamp_minutes` — обычный таймкод + - `test_format_timestamp_hours` — формат с часами + - `test_format_transcript_basic` — проверить шапку + пару сегментов + - `test_format_transcript_empty` — 0 сегментов → содержит "Речь не обнаружена" + - `test_format_transcript_long` — duration > 3600 → таймкоды с часами + +**Критерий готовности**: `uv run pytest tests/test_formatter.py -v` — все тесты зелёные. + +**Коммит**: `git add -A && git commit -m "step 4: formatter with markdown output and tests"` + +--- + +## Шаг 5: cli.py — связка всех модулей, happy path + +> PRD-ссылки: 3.1 (flow), 3.2 (CLI-интерфейс) + +- [ ] Typer command с аргументами и опциями по PRD 3.2: + - `file: Path` — позиционный аргумент + - `--model` / `-m` → default `"large-v3"` + - `--language` / `-l` → default `"auto"` + - `--output` / `-o` → optional Path + - `--device` / `-d` → default `"auto"` + - `--compute-type` → default `"int8"` + - `--verbose` / `-v` → flag, default False +- [ ] Happy path flow: + 1. `check_ffmpeg()` + 2. `validate_input_file(file)` + 3. `detect_device()` если `--device auto`, иначе использовать переданные device + compute_type + 4. rich Console → stderr: информация о запуске (модель, устройство, файл) + 5. rich Spinner/Status во время транскрипции + 6. `transcribe(...)` + 7. Если 0 сегментов → `console.print("⚠ Речь не обнаружена в файле ...", style="yellow")` + 8. `format_transcript(...)` → `write_transcript(...)` + 9. `console.print("✓ Транскрипт сохранён: <путь>", style="green")` + 10. Статистика: кол-во сегментов, время работы (замерить через `time.monotonic()`) +- [ ] Exit codes: 0 — успех (включая пустую речь), 1 — ошибка + +**Критерий готовности**: `uv run transcribe test.mp3` — создаёт корректный .md файл (проверить на локальной машине с реальным файлом). + +**Коммит**: `git add -A && git commit -m "step 5: CLI happy path — end-to-end transcription"` + +--- + +## Шаг 6: Error handling и UX polish + +> PRD-ссылки: 6 (риски) + +- [ ] Graceful Ctrl+C: перехват `KeyboardInterrupt` в cli.py → `console.print("Прервано пользователем", style="yellow")` + `raise SystemExit(130)` +- [ ] Красивые ошибки: обернуть main в try/except, для пользовательских ошибок (файл не найден, ffmpeg, OOM) — вывод через rich без traceback; для неожиданных — traceback только с `--verbose` +- [ ] `--verbose` режим: при транскрипции выводить каждый сегмент в stderr по мере получения из generator (до сборки в список) +- [ ] Проверка: неподдерживаемое расширение → warning, но попытка продолжить + +**Критерий готовности**: ручной прогон edge cases — несуществующий файл, .txt файл, Ctrl+C во время работы. + +**Коммит**: `git add -A && git commit -m "step 6: error handling, verbose mode, graceful shutdown"` + +--- + +## Шаг 7: README.md + +> PRD-ссылки: 4.2 (требования), 4.1.1 (GPU таблица), 4.3 (кроссплатформенность) + +- [ ] Описание: что делает, зачем +- [ ] Требования: Python ≥ 3.10, ffmpeg, (опционально) NVIDIA GPU + CUDA +- [ ] Установка: + ```bash + git clone + cd local-transcriber + uv sync + ``` +- [ ] Использование: 3-4 примера команд (простой, с языком, с моделью, CPU) +- [ ] Установка ffmpeg: Linux (`apt`), Windows (`winget`/`scoop`), WSL2, macOS (`brew`) +- [ ] GPU и CUDA: краткое пояснение, ссылка на NVIDIA docs, что CTranslate2 ставит нужное +- [ ] Таблица моделей: имя, размер на диске, VRAM (int8), относительная скорость, качество +- [ ] Пример выходного файла (сокращённый) + +**Критерий готовности**: коллега может по README установить и запустить на Windows/WSL2 без вопросов. + +**Коммит**: `git add -A && git commit -m "step 7: README with install, usage, GPU guide"` + +--- + +## Инструкция для агента + +Команда на каждый шаг: +``` +Прочитай docs/PRD.md (только секции, указанные в текущем шаге) и plan.md (только текущий шаг). +Реализуй шаг N. +После реализации — отметь все чекбоксы шага как [x] в plan.md. +Не трогай код и чекбоксы из других шагов. +``` + +После каждого шага — `git add -A && git commit -m "<сообщение из шага>"`. \ No newline at end of file