docs: добавлен ADR-001 (CUDA bootstrap), обновлены PRD и plan
- Зачем:
- зафиксировать архитектурное решение по GPU runtime и rejected alternatives,
чтобы не переизобретать отклонённые подходы в будущем.
- Что:
- создан docs/adr/001-cuda-bootstrap.md (контекст, решение, tradeoffs, альтернативы).
- PRD 4.2: исправлено описание CUDA-зависимостей (cuDNN не нужен, cuBLAS из pip).
- plan.md: добавлен выполненный шаг 5.1 со ссылкой на ADR.
- удалены docs/plan-gpu-runtime.md и отчёты ревью (review-stages-*).
- Проверка:
- cat docs/adr/001-cuda-bootstrap.md.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -120,7 +120,7 @@ transcribe <путь_к_файлу> [опции]
|
|||||||
|
|
||||||
- Python ≥ 3.10
|
- Python ≥ 3.10
|
||||||
- ffmpeg в PATH (используется faster-whisper внутри для декодирования любых медиаформатов)
|
- ffmpeg в PATH (используется faster-whisper внутри для декодирования любых медиаформатов)
|
||||||
- Для GPU: CUDA toolkit (cuBLAS, cuDNN) — ставится автоматически через CTranslate2
|
- Для GPU: Linux/WSL2 — cuBLAS из nvidia-cublas-cu12 (ставится автоматически через `uv sync`); Windows — системный CUDA toolkit (см. ADR-001)
|
||||||
- Дисковое пространство для моделей: ~3 GB (large-v3)
|
- Дисковое пространство для моделей: ~3 GB (large-v3)
|
||||||
- Выходные файлы: UTF-8 (явная кодировка при записи)
|
- Выходные файлы: UTF-8 (явная кодировка при записи)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# ADR-001: Preload libcublas из pip-пакета для GPU на Linux/WSL2
|
||||||
|
|
||||||
|
**Статус**: Принято
|
||||||
|
**Дата**: 2026-03-18
|
||||||
|
|
||||||
|
## Контекст
|
||||||
|
|
||||||
|
ctranslate2 (backend faster-whisper) в runtime делает `dlopen("libcublas.so.12")`,
|
||||||
|
но не бандлит эту библиотеку в свой wheel — ожидает её в системе.
|
||||||
|
Без установленного CUDA toolkit `uv run transcribe --device cuda` падает с ошибкой.
|
||||||
|
|
||||||
|
Ключевые факты:
|
||||||
|
- `libcuda.so.1` приходит от NVIDIA driver (всегда есть, если GPU есть)
|
||||||
|
- `libcublas.so.12` отсутствует в wheel ctranslate2 на обеих платформах
|
||||||
|
- **cuDNN не нужен** — в `libctranslate2.so` ноль символов cudnn (проверено `nm -D` и `strings`)
|
||||||
|
- На Windows ctranslate2 делает `os.add_dll_directory` в своём `__init__.py`,
|
||||||
|
но только для DLL внутри пакета; `cublas64_12.dll` тоже не бандлится
|
||||||
|
|
||||||
|
## Решение
|
||||||
|
|
||||||
|
### nvidia-cublas-cu12 как pip-зависимость
|
||||||
|
|
||||||
|
В `pyproject.toml`:
|
||||||
|
```
|
||||||
|
"nvidia-cublas-cu12>=12.4; sys_platform == 'linux' and platform_machine == 'x86_64'"
|
||||||
|
```
|
||||||
|
|
||||||
|
Нижняя граница `>=12.4` — ctranslate2 собран с CUDA 12.4. Пакет доступен только
|
||||||
|
для Linux x86_64 (на Windows и macOS не устанавливается по platform marker).
|
||||||
|
|
||||||
|
### ctypes.CDLL preload вместо LD_LIBRARY_PATH
|
||||||
|
|
||||||
|
`_cuda_bootstrap.py` загружает `libcublas.so.12` по полному пути через
|
||||||
|
`ctypes.CDLL(path, mode=RTLD_GLOBAL)` **до** первого `import ctranslate2`.
|
||||||
|
|
||||||
|
Почему не `os.environ["LD_LIBRARY_PATH"]`: на Linux/glibc динамический линкер (`ld.so`)
|
||||||
|
кеширует пути поиска при старте процесса и **не перечитывает** `LD_LIBRARY_PATH`
|
||||||
|
из environ в рамках уже запущенного процесса.
|
||||||
|
|
||||||
|
Почему `RTLD_GLOBAL`: без этого флага символы cublas не видны другим `.so`,
|
||||||
|
загруженным позже (в т.ч. `libctranslate2.so`).
|
||||||
|
|
||||||
|
Динамический линкер кеширует загруженные библиотеки по soname — когда ctranslate2
|
||||||
|
потом вызовет `dlopen("libcublas.so.12")`, линкер вернёт уже загруженный handle.
|
||||||
|
|
||||||
|
### strict_device для явного --device
|
||||||
|
|
||||||
|
`--device cuda` / `--device cpu` → `strict_device=True` → CUDA-ошибка = raise, без fallback.
|
||||||
|
`--device auto` → `strict_device=False` → текущее поведение с fallback на CPU.
|
||||||
|
|
||||||
|
Мотивация: silent fallback для часового файла = 60 минут вместо 5.
|
||||||
|
|
||||||
|
## Последствия и tradeoffs
|
||||||
|
|
||||||
|
**~400 MB на CPU-only Linux x86_64**: nvidia-cublas-cu12 ставится на все Linux x86_64,
|
||||||
|
включая машины без GPU. Bootstrap при этом preload'ит libcublas (overhead ~1 ms),
|
||||||
|
но она не используется, т.к. ctranslate2 не получит запрос на CUDA device.
|
||||||
|
|
||||||
|
Альтернатива — `[project.optional-dependencies]` + `uv sync --extra cuda`,
|
||||||
|
но тогда теряется zero-config UX. Для v1 оставляем как обязательную зависимость.
|
||||||
|
|
||||||
|
**Windows GPU**: nvidia-cublas-cu12 недоступен как pip-пакет для Windows.
|
||||||
|
Единственный путь — системный CUDA toolkit (`choco install cuda` / `winget install -e --id Nvidia.CUDA`).
|
||||||
|
CLI выводит эту подсказку при CUDA-ошибке на `sys.platform == "win32"`.
|
||||||
|
|
||||||
|
**Namespace package**: `nvidia.cublas` — namespace package (`__file__` is `None`),
|
||||||
|
для определения директории используется `__path__[0]`, а не `__file__`.
|
||||||
|
|
||||||
|
## Отклонённые альтернативы
|
||||||
|
|
||||||
|
| Альтернатива | Почему отклонена |
|
||||||
|
|---|---|
|
||||||
|
| `nvidia-cudnn-cu12` в зависимостях | ctranslate2 не использует cuDNN — ноль символов, проверено через `nm -D` |
|
||||||
|
| Self-reexec с `LD_LIBRARY_PATH` | ctypes.CDLL решает задачу без перезапуска процесса; self-reexec создаёт проблемы с сигналами, tty, fd |
|
||||||
|
| `os.environ["LD_LIBRARY_PATH"] += ...` | glibc кеширует пути при старте, не перечитывает environ |
|
||||||
|
| `DeviceResolution` dataclass + `resolve_device()` | Один `bool strict_device` решает ту же задачу проще |
|
||||||
|
| ctranslate2 preflight (`get_supported_compute_types`) | Латентность; try/catch при загрузке модели не хуже |
|
||||||
|
| Bootstrap console_scripts entrypoint | Не нужен — достаточно вызова в начале `transcriber.py` |
|
||||||
@@ -241,6 +241,20 @@
|
|||||||
**Критерий готовности**: `uv run transcribe test.mp3` — создаёт корректный .md файл (проверить на локальной машине с реальным файлом).
|
**Критерий готовности**: `uv run transcribe test.mp3` — создаёт корректный .md файл (проверить на локальной машине с реальным файлом).
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Шаг 5.1: GPU runtime — прозрачная работа CUDA на Linux/WSL2
|
||||||
|
|
||||||
|
> Детали решения и обоснование: [ADR-001](adr/001-cuda-bootstrap.md)
|
||||||
|
|
||||||
|
- [x] `nvidia-cublas-cu12>=12.4` в dependencies (Linux x86_64)
|
||||||
|
- [x] `_cuda_bootstrap.py` — preload libcublas через `ctypes.CDLL(RTLD_GLOBAL)` до импорта ctranslate2
|
||||||
|
- [x] `strict_device` в transcriber — `--device cuda/cpu` без silent fallback
|
||||||
|
- [x] CLI: диагностика requested vs resolved device, Windows CUDA-подсказка
|
||||||
|
- [x] Тесты: bootstrap (unit + интеграционный), strict_device, Windows-диагностика
|
||||||
|
|
||||||
|
**Критерий готовности**: `uv run pytest -v` — 52 passed, 1 skipped. На GPU-машине без системного CUDA toolkit: `uv run transcribe test.mp3 --device cuda` работает.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Шаг 6: Error handling и UX polish
|
## Шаг 6: Error handling и UX polish
|
||||||
|
|||||||
@@ -1,141 +0,0 @@
|
|||||||
# Review: Stages 1-3
|
|
||||||
|
|
||||||
## Executive Summary
|
|
||||||
|
|
||||||
| Severity | Count |
|
|
||||||
|----------|-------|
|
|
||||||
| CRITICAL | 0 |
|
|
||||||
| HIGH | 0 |
|
|
||||||
| MEDIUM | 2 |
|
|
||||||
| LOW | 1 |
|
|
||||||
|
|
||||||
**Overall Risk:** MEDIUM
|
|
||||||
**Recommendation:** CONDITIONAL
|
|
||||||
|
|
||||||
**Key Metrics:**
|
|
||||||
- Files analyzed: 10/10
|
|
||||||
- Lines changed: +427 / -0
|
|
||||||
- Test coverage gaps: 3 behaviors
|
|
||||||
- High blast radius changes: 0
|
|
||||||
- Security regressions detected: 0
|
|
||||||
|
|
||||||
## What Changed
|
|
||||||
|
|
||||||
**Commit Range:** `e0fcf43..WORKTREE`
|
|
||||||
**Commits:** `ade3b23`, `510d6cc`, plus uncommitted stage 3 changes
|
|
||||||
|
|
||||||
| File | +Lines | -Lines | Risk | Notes |
|
|
||||||
|------|--------|--------|------|-------|
|
|
||||||
| pyproject.toml | 28 | 0 | LOW | Project scaffold and dependency wiring |
|
|
||||||
| src/local_transcriber/cli.py | 14 | 0 | LOW | Step 1 placeholder CLI |
|
|
||||||
| src/local_transcriber/formatter.py | 23 | 0 | LOW | Step 1 placeholder formatter |
|
|
||||||
| src/local_transcriber/transcriber.py | 99 | 0 | MEDIUM | Core transcription flow and fallback logic |
|
|
||||||
| src/local_transcriber/utils.py | 68 | 0 | MEDIUM | Environment checks and input validation |
|
|
||||||
| tests/test_transcriber.py | 121 | 0 | LOW | Mock tests for step 3 |
|
|
||||||
| tests/test_utils.py | 74 | 0 | LOW | Unit tests for step 2 |
|
|
||||||
|
|
||||||
## Findings
|
|
||||||
|
|
||||||
### MEDIUM: CUDA fallback does not trigger when `model.transcribe()` fails before returning a generator
|
|
||||||
|
|
||||||
**File:** `src/local_transcriber/transcriber.py:51`
|
|
||||||
**Test Coverage:** PARTIAL
|
|
||||||
|
|
||||||
`transcribe()` wraps exceptions from `model.transcribe(...)` into `RuntimeError`, but the CPU fallback exists only in the later generator-iteration branch. If `faster-whisper` raises a CUDA or OOM error during `model.transcribe(...)` itself, the function exits instead of retrying on CPU.
|
|
||||||
|
|
||||||
This is a direct mismatch with the step 3 requirement to fall back on CUDA/OOM failures.
|
|
||||||
|
|
||||||
**Reproduction:**
|
|
||||||
- Mock `WhisperModel(...).transcribe` to raise `RuntimeError("CUDA kernel launch failed")`
|
|
||||||
- Current result: `RuntimeError("Ошибка при транскрипции файла ...")`
|
|
||||||
- Expected result: warning + retry on CPU
|
|
||||||
|
|
||||||
**Recommendation:**
|
|
||||||
- Apply the same `_is_cuda_error()` fallback path around `model.transcribe(...)`, not only around iteration of the returned generator
|
|
||||||
- Add a test for CUDA failure raised directly by `model.transcribe(...)`
|
|
||||||
|
|
||||||
### MEDIUM: `get_gpu_name()` crashes on empty successful `nvidia-smi` output
|
|
||||||
|
|
||||||
**File:** `src/local_transcriber/utils.py:42`
|
|
||||||
**Test Coverage:** NO
|
|
||||||
|
|
||||||
When `subprocess.run(...)` returns `returncode == 0` with empty `stdout`, `splitlines()[0]` raises `IndexError`. The plan explicitly allows `get_gpu_name()` to return `None`, so this path should degrade gracefully instead of crashing.
|
|
||||||
|
|
||||||
This will surface later in CLI/device formatting and turn a non-critical metadata lookup into a hard failure.
|
|
||||||
|
|
||||||
**Reproduction:**
|
|
||||||
- Mock `subprocess.run` to return `CompletedProcess(..., returncode=0, stdout="")`
|
|
||||||
- Current result: `IndexError: list index out of range`
|
|
||||||
- Expected result: `None`
|
|
||||||
|
|
||||||
**Recommendation:**
|
|
||||||
- Check `stdout.strip()` before indexing the first line
|
|
||||||
- Add a unit test for empty `stdout`
|
|
||||||
|
|
||||||
### LOW: `on_segment` emits duplicate segments after mid-stream CUDA fallback
|
|
||||||
|
|
||||||
**File:** `src/local_transcriber/transcriber.py:63`
|
|
||||||
**Test Coverage:** NO
|
|
||||||
|
|
||||||
If the GPU generator yields one or more segments and then fails with a CUDA error, `on_segment` is called for the partial GPU output and then called again for the full CPU retry. The returned `segments` list is reset correctly, but callback side effects are not.
|
|
||||||
|
|
||||||
For the planned `--verbose` flow this means duplicated stderr output such as:
|
|
||||||
|
|
||||||
```text
|
|
||||||
first
|
|
||||||
first
|
|
||||||
second
|
|
||||||
```
|
|
||||||
|
|
||||||
while the final transcript contains only `first`, `second`.
|
|
||||||
|
|
||||||
**Recommendation:**
|
|
||||||
- Buffer callback output until the segment stream completes successfully, or
|
|
||||||
- suppress callback invocation during the first failed attempt and only emit after a successful run
|
|
||||||
|
|
||||||
## Test Coverage Analysis
|
|
||||||
|
|
||||||
**Observed coverage:** targeted unit tests exist for step 2 and step 3 happy paths, but not for several failure paths.
|
|
||||||
|
|
||||||
**Untested Changes:**
|
|
||||||
|
|
||||||
| Function | Risk | Gap |
|
|
||||||
|----------|------|-----|
|
|
||||||
| `transcribe()` | MEDIUM | No test for CUDA failure raised by `model.transcribe(...)` |
|
|
||||||
| `transcribe()` | LOW | No test for duplicate callback behavior after generator fallback |
|
|
||||||
| `get_gpu_name()` | MEDIUM | No test for empty successful stdout |
|
|
||||||
|
|
||||||
## Blast Radius Analysis
|
|
||||||
|
|
||||||
The current blast radius is low because stage 1-3 code is only consumed by placeholder CLI wiring and tests. The highest-impact function is `transcribe()`, which will become user-facing once stage 5 is implemented.
|
|
||||||
|
|
||||||
| Function | Current Callers | Risk | Priority |
|
|
||||||
|----------|-----------------|------|----------|
|
|
||||||
| `transcribe()` | tests only | MEDIUM | P1 |
|
|
||||||
| `get_gpu_name()` | not yet wired into CLI | MEDIUM | P1 |
|
|
||||||
| `validate_input_file()` | tests only | LOW | P2 |
|
|
||||||
|
|
||||||
## Historical Context
|
|
||||||
|
|
||||||
- `ade3b23` introduced the scaffold and placeholder module layout for step 1
|
|
||||||
- `510d6cc` added environment validation and output-path logic for step 2
|
|
||||||
- Stage 3 is currently in the working tree and introduces the first non-trivial runtime behavior, including fallback and callback logic
|
|
||||||
|
|
||||||
The defects above are all newly introduced in the step 3 working tree implementation, not legacy behavior.
|
|
||||||
|
|
||||||
## Recommendations
|
|
||||||
|
|
||||||
### Immediate
|
|
||||||
|
|
||||||
- [ ] Fix CUDA fallback for errors raised by `model.transcribe(...)`
|
|
||||||
- [ ] Make `get_gpu_name()` return `None` on empty stdout instead of raising
|
|
||||||
- [ ] Add regression tests for both cases
|
|
||||||
|
|
||||||
### Before Stage 5
|
|
||||||
|
|
||||||
- [ ] Decide how `on_segment` should behave across retries and make the behavior explicit
|
|
||||||
- [ ] Add one test that covers generator failure after partial output
|
|
||||||
|
|
||||||
### Residual Risk
|
|
||||||
|
|
||||||
- CLI acceptance for `uv run transcribe --help` was not reproducible in this environment because `uv run transcribe` hit a local `snap-confine` execution issue unrelated to the project code
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
# Review: Stages 4-5 (Rerun)
|
|
||||||
|
|
||||||
## Executive Summary
|
|
||||||
|
|
||||||
| Severity | Count |
|
|
||||||
|----------|-------|
|
|
||||||
| CRITICAL | 0 |
|
|
||||||
| HIGH | 0 |
|
|
||||||
| MEDIUM | 0 |
|
|
||||||
| LOW | 0 |
|
|
||||||
|
|
||||||
**Overall Risk:** LOW
|
|
||||||
**Recommendation:** APPROVE
|
|
||||||
|
|
||||||
## What Was Rechecked
|
|
||||||
|
|
||||||
- `src/local_transcriber/formatter.py`
|
|
||||||
- `src/local_transcriber/cli.py`
|
|
||||||
- `tests/test_formatter.py`
|
|
||||||
- `tests/test_cli.py`
|
|
||||||
|
|
||||||
## Result
|
|
||||||
|
|
||||||
No new findings.
|
|
||||||
|
|
||||||
Previously reported issues for stages 4-5 are addressed:
|
|
||||||
|
|
||||||
- centisecond carry in `format_timestamp()` is fixed
|
|
||||||
- transcript formatting no longer depends on leading whitespace in `seg.text`
|
|
||||||
- CLI now has automated tests for happy path, options, empty speech warning, output path handling, and error exit code
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
|
|
||||||
- `uv run pytest` -> 31 passed
|
|
||||||
- `.venv/bin/transcribe --help` -> works
|
|
||||||
- spot checks:
|
|
||||||
- `format_timestamp(0.995)` -> `00:01.00`
|
|
||||||
- `format_timestamp(59.995)` -> `01:00.00`
|
|
||||||
- `format_timestamp(3599.995, use_hours=True)` -> `01:00:00.00`
|
|
||||||
|
|
||||||
## Residual Risk
|
|
||||||
|
|
||||||
- Step 6 error-handling polish is still not implemented, so user-facing error formatting remains intentionally incomplete at this stage
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
# Review: Stages 4-5
|
|
||||||
|
|
||||||
## Executive Summary
|
|
||||||
|
|
||||||
| Severity | Count |
|
|
||||||
|----------|-------|
|
|
||||||
| CRITICAL | 0 |
|
|
||||||
| HIGH | 0 |
|
|
||||||
| MEDIUM | 2 |
|
|
||||||
| LOW | 1 |
|
|
||||||
|
|
||||||
**Overall Risk:** MEDIUM
|
|
||||||
**Recommendation:** CONDITIONAL
|
|
||||||
|
|
||||||
**Key Metrics:**
|
|
||||||
- Files analyzed: 4
|
|
||||||
- Verified commands: `pytest`, CLI help, mocked CLI happy path
|
|
||||||
- Test coverage gaps: 1 user-facing module (`cli.py`)
|
|
||||||
- High blast radius changes: 0
|
|
||||||
- Security regressions detected: 0
|
|
||||||
|
|
||||||
## What Changed
|
|
||||||
|
|
||||||
**Commit Range:** `dfc5f46..WORKTREE`
|
|
||||||
**Commits:** `0d1a734`, plus uncommitted step 5 changes
|
|
||||||
|
|
||||||
| File | Risk | Notes |
|
|
||||||
|------|------|-------|
|
|
||||||
| `src/local_transcriber/formatter.py` | MEDIUM | Output contract and markdown formatting |
|
|
||||||
| `tests/test_formatter.py` | LOW | Unit coverage for formatter |
|
|
||||||
| `src/local_transcriber/cli.py` | MEDIUM | Main user-facing flow and file writing |
|
|
||||||
| `docs/plan.md` | LOW | Checkbox updates for step 5 |
|
|
||||||
|
|
||||||
## Findings
|
|
||||||
|
|
||||||
### MEDIUM: `format_timestamp()` can emit invalid centiseconds like `.100`
|
|
||||||
|
|
||||||
**File:** `src/local_transcriber/formatter.py:7`
|
|
||||||
**Test Coverage:** NO
|
|
||||||
|
|
||||||
The implementation rounds centiseconds independently from the integral seconds:
|
|
||||||
|
|
||||||
```python
|
|
||||||
total_seconds = int(seconds)
|
|
||||||
centiseconds = int(round((seconds - total_seconds) * 100))
|
|
||||||
```
|
|
||||||
|
|
||||||
For values such as `0.995`, this produces `00:00.100` instead of carrying into the next second.
|
|
||||||
|
|
||||||
**Reproduction:**
|
|
||||||
- `format_timestamp(0.995)` returns `00:00.100`
|
|
||||||
|
|
||||||
**Impact:**
|
|
||||||
- Breaks the PRD timestamp format contract (`SS.ss` must always have exactly two fractional digits)
|
|
||||||
- Can produce malformed transcript timestamps on real segment boundaries
|
|
||||||
|
|
||||||
**Recommendation:**
|
|
||||||
- Round the full timestamp first and then split into components, or normalize `centiseconds == 100` by incrementing seconds
|
|
||||||
- Add a regression test for `0.995`
|
|
||||||
|
|
||||||
### MEDIUM: Transcript formatting depends on segment text already containing a leading space
|
|
||||||
|
|
||||||
**File:** `src/local_transcriber/formatter.py:62`
|
|
||||||
**Test Coverage:** PARTIAL
|
|
||||||
|
|
||||||
Segment lines are written as:
|
|
||||||
|
|
||||||
```python
|
|
||||||
f"[{start} - {end}]{seg.text}"
|
|
||||||
```
|
|
||||||
|
|
||||||
This only matches the PRD format if `seg.text` already starts with a space. The current tests mask that dependency by building fixtures with leading spaces.
|
|
||||||
|
|
||||||
**Reproduction:**
|
|
||||||
- A mocked CLI run with a segment text of `"Hello"` writes:
|
|
||||||
- `[00:00.00 - 00:01.00]Hello`
|
|
||||||
- Expected:
|
|
||||||
- `[00:00.00 - 00:01.00] Hello`
|
|
||||||
|
|
||||||
**Impact:**
|
|
||||||
- Output format becomes model-dependent instead of being guaranteed by the formatter
|
|
||||||
- Any future normalization in `transcriber.py` will immediately break transcript formatting
|
|
||||||
|
|
||||||
**Recommendation:**
|
|
||||||
- Normalize segment text inside the formatter, e.g. `seg.text.strip()` plus an explicit single space after `]`
|
|
||||||
- Add a test case where segment text has no leading whitespace
|
|
||||||
|
|
||||||
### LOW: Step 5 has no automated tests for the public CLI contract
|
|
||||||
|
|
||||||
**File:** `src/local_transcriber/cli.py:16`
|
|
||||||
**Test Coverage:** NO
|
|
||||||
|
|
||||||
The user-facing entrypoint is now wired end to end, but there is still no `test_cli.py` coverage for:
|
|
||||||
|
|
||||||
- option parsing
|
|
||||||
- warning path for empty speech
|
|
||||||
- default output-path generation
|
|
||||||
- exit-code behavior on failure
|
|
||||||
|
|
||||||
This already matters because the mocked CLI happy path is what exposed the missing-space formatting bug above.
|
|
||||||
|
|
||||||
## Test Coverage Analysis
|
|
||||||
|
|
||||||
**Executed checks:**
|
|
||||||
- `uv run pytest` -> 22 tests passed
|
|
||||||
- `.venv/bin/transcribe --help` -> works
|
|
||||||
- mocked `CliRunner` happy path -> exit code 0 and output file written
|
|
||||||
|
|
||||||
**Coverage gaps:**
|
|
||||||
|
|
||||||
| Area | Gap | Risk |
|
|
||||||
|------|-----|------|
|
|
||||||
| `formatter.py` | No edge-case test for centisecond carry | MEDIUM |
|
|
||||||
| `formatter.py` | No test for segment text without leading whitespace | MEDIUM |
|
|
||||||
| `cli.py` | No automated tests at all | LOW |
|
|
||||||
|
|
||||||
## Blast Radius Analysis
|
|
||||||
|
|
||||||
The blast radius is still low because this is a small CLI project, but `cli.py` is now the single public entrypoint. Any formatting or wiring defect directly affects all users.
|
|
||||||
|
|
||||||
| Function | Exposure | Risk | Priority |
|
|
||||||
|----------|----------|------|----------|
|
|
||||||
| `main()` | All CLI invocations | MEDIUM | P1 |
|
|
||||||
| `format_transcript()` | All saved transcript files | MEDIUM | P1 |
|
|
||||||
| `format_timestamp()` | Every segment line | MEDIUM | P1 |
|
|
||||||
|
|
||||||
## Recommendations
|
|
||||||
|
|
||||||
### Immediate
|
|
||||||
|
|
||||||
- [ ] Fix centisecond carry handling in `format_timestamp()`
|
|
||||||
- [ ] Stop relying on leading whitespace in `seg.text`
|
|
||||||
- [ ] Add formatter regression tests for both cases
|
|
||||||
|
|
||||||
### Before Step 6
|
|
||||||
|
|
||||||
- [ ] Add `tests/test_cli.py` with a mocked happy path and one error path
|
|
||||||
- [ ] Assert output file contents through the CLI layer, not only through direct formatter calls
|
|
||||||
Reference in New Issue
Block a user