diff --git a/docs/superpowers/plans/2026-08-30-transcription-desktop-milestone-1.md b/docs/superpowers/plans/2026-08-30-transcription-desktop-milestone-1.md new file mode 100644 index 0000000..9435c6d --- /dev/null +++ b/docs/superpowers/plans/2026-08-30-transcription-desktop-milestone-1.md @@ -0,0 +1,168 @@ +# Audio Transcription Desktop Milestone 1 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Deliver a Windows desktop GUI that selects and inspects media, manages secure settings, and exposes a clearly stubbed transcription action. + +**Architecture:** A `src/voice_transcriptor` package separates typed models and services from PySide6 UI modules. FFprobe and settings logic are synchronous, testable services; the UI runs media probing in a Qt thread pool and ignores stale results. + +**Tech Stack:** Python 3.12+, PySide6, keyring, OpenAI Python SDK, pytest, pytest-qt, FFmpeg/ffprobe. + +**Spec:** `docs/superpowers/specs/2026-08-29-transcription-desktop-milestone-1-design.md` + +## Global Constraints + +- Target Windows 11 and Python 3.12 or newer. +- Store API keys only through `keyring`; never serialize or log them. +- Default language is `pt-BR`. +- Transcription remains stubbed in milestone 1. +- Keep UI imports out of business services and retain PyInstaller-compatible entry points. +- Preserve the empty tracked `README.md` by filling it with project documentation. + +--- + +## File Map + +- `pyproject.toml`: package metadata, entry point, and pytest settings. +- `requirements.txt`: runtime and test dependencies for straightforward Windows setup. +- `.gitignore`: Python, test, environment, editor, and PyInstaller artifacts. +- `src/voice_transcriptor/models.py`: immutable shared data values. +- `src/voice_transcriptor/formatting.py`: file-size and duration formatting. +- `src/voice_transcriptor/services/media_probe.py`: tool discovery, file validation, ffprobe execution and parsing. +- `src/voice_transcriptor/services/settings.py`: defaults and atomic non-secret JSON persistence. +- `src/voice_transcriptor/services/credentials.py`: keyring-only API-key access. +- `src/voice_transcriptor/services/transcription.py`: explicit milestone stub. +- `src/voice_transcriptor/ui/settings_dialog.py`: settings form and validation. +- `src/voice_transcriptor/ui/main_window.py`: selection/drop UI, async probing, metadata and status presentation. +- `src/voice_transcriptor/app.py`, `__main__.py`: dependency wiring and executable entry points. +- `tests/`: focused unit tests and an offscreen Qt smoke test. +- `README.md`: Windows setup, execution, security, testing, and milestone limitations. + +### Task 1: Typed core, formatting, and project scaffolding + +**Files:** +- Create: `pyproject.toml`, `requirements.txt`, `.gitignore` +- Create: `src/voice_transcriptor/__init__.py`, `src/voice_transcriptor/models.py`, `src/voice_transcriptor/formatting.py` +- Test: `tests/test_formatting.py` + +**Interfaces:** +- Produces: `AppSettings(model: str, language: str, output_directory: Path)`, `ToolStatus(ffmpeg_path: Path | None, ffprobe_path: Path | None)`, `MediaInfo(path: Path, size_bytes: int, duration_seconds: float | None, audio_codec: str | None)`, `format_file_size(int) -> str`, and `format_duration(float | None) -> str`. + +- [ ] **Step 1: Write failing formatting tests** for byte units, multi-hour duration, and unavailable duration. +- [ ] **Step 2: Run `python -m pytest tests/test_formatting.py -v`** and confirm collection/import failure. +- [ ] **Step 3: Add package configuration, typed dataclasses, and minimal pure formatting functions.** Size uses binary units and duration renders `HH:MM:SS`; unavailable duration renders `Unknown`. +- [ ] **Step 4: Run `python -m pytest tests/test_formatting.py -v`** and confirm all cases pass. +- [ ] **Step 5: Commit with `git commit -m "build: scaffold typed application core"`.** + +### Task 2: Media inspection service + +**Files:** +- Create: `src/voice_transcriptor/services/__init__.py`, `src/voice_transcriptor/services/media_probe.py` +- Test: `tests/test_media_probe.py` + +**Interfaces:** +- Consumes: `MediaInfo`, `ToolStatus`. +- Produces: `detect_tools() -> ToolStatus`, `validate_media_path(path: Path) -> Path`, `parse_probe_output(path: Path, size_bytes: int, payload: str) -> MediaInfo`, `MediaProbeService.probe(path: Path) -> MediaInfo`, and typed `MediaProbeError` subclasses. + +- [ ] **Step 1: Write failing tests** for executable discovery, path rejection, audio-only and video-with-audio JSON, missing fields, malformed JSON, nonzero subprocess result, and timeout mapping. +- [ ] **Step 2: Run `python -m pytest tests/test_media_probe.py -v`** and confirm import failure. +- [ ] **Step 3: Implement safe probing** with `subprocess.run([...], capture_output=True, text=True, timeout=30, check=False, creationflags=CREATE_NO_WINDOW on Windows)` and no shell. +- [ ] **Step 4: Run the focused tests** and confirm they pass. +- [ ] **Step 5: Commit with `git commit -m "feat: add ffprobe media inspection"`.** + +### Task 3: Settings and credential persistence + +**Files:** +- Create: `src/voice_transcriptor/services/settings.py`, `src/voice_transcriptor/services/credentials.py` +- Test: `tests/test_settings.py`, `tests/test_credentials.py` + +**Interfaces:** +- Consumes: `AppSettings`. +- Produces: `SettingsRepository(path: Path | None = None)`, `.load() -> tuple[AppSettings, str | None]`, `.save(settings: AppSettings) -> None`; `CredentialService(backend=keyring)`, `.get_api_key() -> str | None`, `.has_api_key() -> bool`, `.set_api_key(value: str) -> None`; `SettingsError`, `CredentialError`. + +- [ ] **Step 1: Write failing tests** for defaults, round trip, malformed recovery warning, atomic replacement, absence of API-key fields, and mocked keyring calls/failures. +- [ ] **Step 2: Run both focused test files** and confirm import failure. +- [ ] **Step 3: Implement per-user configuration** under `%APPDATA%/VoiceTranscriptor/settings.json`, using UTF-8 JSON and `Path.replace` from a sibling temporary file. +- [ ] **Step 4: Implement the keyring adapter** with stable service `voice-transcriptor` and account `openai-api-key`; reject blank keys and sanitize exceptions. +- [ ] **Step 5: Run focused tests** and confirm they pass. +- [ ] **Step 6: Commit with `git commit -m "feat: persist settings and credentials securely"`.** + +### Task 4: Transcription boundary + +**Files:** +- Create: `src/voice_transcriptor/services/transcription.py` +- Test: `tests/test_transcription.py` + +**Interfaces:** +- Produces: `TranscriptionNotImplementedError` and `TranscriptionService.transcribe(media: MediaInfo, settings: AppSettings) -> None`. + +- [ ] **Step 1: Write a failing test** asserting the service raises the specific milestone exception with a user-readable message. +- [ ] **Step 2: Run the focused test** and confirm import failure. +- [ ] **Step 3: Implement the minimal typed stub.** +- [ ] **Step 4: Run the focused test** and confirm it passes. +- [ ] **Step 5: Commit with `git commit -m "feat: define transcription service boundary"`.** + +### Task 5: Settings dialog + +**Files:** +- Create: `src/voice_transcriptor/ui/__init__.py`, `src/voice_transcriptor/ui/settings_dialog.py` +- Test: `tests/ui/test_settings_dialog.py` + +**Interfaces:** +- Consumes: `AppSettings`, `SettingsRepository`, `CredentialService`. +- Produces: `SettingsDialog(settings, repository, credentials, parent=None)` and `settings_saved(AppSettings)` signal. + +- [ ] **Step 1: Write offscreen Qt tests** for populated defaults, masked key entry, preserving an existing credential when blank, saving a supplied key, output-directory validation, and no secret in widget/log output after save. +- [ ] **Step 2: Run the focused UI tests** and confirm import failure. +- [ ] **Step 3: Build a native `QDialog` form** with editable model/language, folder picker, password echo mode, Save/Cancel buttons, and concise message boxes on service failure. +- [ ] **Step 4: Run the focused tests** and confirm they pass. +- [ ] **Step 5: Commit with `git commit -m "feat: add secure settings dialog"`.** + +### Task 6: Main window and asynchronous probe flow + +**Files:** +- Create: `src/voice_transcriptor/ui/main_window.py` +- Test: `tests/ui/test_main_window.py` + +**Interfaces:** +- Consumes all service interfaces, formatting helpers, and shared models. +- Produces: `MainWindow(media_probe, settings_repository, credentials, transcription_service)` with `select_file(path: Path) -> None`; internal `ProbeSignals` and `ProbeWorker(QRunnable)` communicate results without blocking. + +- [ ] **Step 1: Write offscreen Qt tests** for the visible controls, supported single-file drops, rejected URLs/multiple files, populated metadata, missing-tool status, stale probe suppression, settings opening, validation on Transcribe, and stub status logging. +- [ ] **Step 2: Run focused UI tests** and confirm import failure. +- [ ] **Step 3: Implement the native main-window layout** with a drop frame, Browse and Settings buttons, four metadata rows, tool status, prominent Transcribe button, and read-only log. +- [ ] **Step 4: Implement QRunnable probing** tagged with monotonically increasing request IDs; callbacks update only when the ID matches the current selection. +- [ ] **Step 5: Implement button actions and readable error handling** without logging credentials or full tracebacks in the GUI. +- [ ] **Step 6: Run focused UI tests** and confirm they pass. +- [ ] **Step 7: Commit with `git commit -m "feat: add media selection desktop interface"`.** + +### Task 7: Application wiring and startup smoke test + +**Files:** +- Create: `src/voice_transcriptor/app.py`, `src/voice_transcriptor/__main__.py` +- Test: `tests/ui/test_app.py` + +**Interfaces:** +- Produces: `create_main_window() -> MainWindow` and `main() -> int`; console entry point `voice-transcriptor`. + +- [ ] **Step 1: Write a failing offscreen smoke test** that constructs, shows, processes events for, and closes the window. +- [ ] **Step 2: Run the focused test** and confirm import failure. +- [ ] **Step 3: Wire concrete services and add guarded startup error reporting.** Reuse an existing `QApplication` in tests and create one in `main()`. +- [ ] **Step 4: Run the smoke test and `python -m compileall -q src tests`.** +- [ ] **Step 5: Commit with `git commit -m "feat: wire desktop application startup"`.** + +### Task 8: Documentation and full validation + +**Files:** +- Modify: `README.md`, `requirements.txt` + +**Interfaces:** +- Documents the `python -m voice_transcriptor` and `voice-transcriptor` entry points and environment setup. + +- [ ] **Step 1: Write README instructions** for Python 3.12 virtual environments, dependency installation, FFmpeg installation/PATH checks, running, testing, Windows Credential Manager security, known milestone limitations, and future PyInstaller packaging. +- [ ] **Step 2: Install dependencies with `python -m pip install -r requirements.txt`** when they are not already available. +- [ ] **Step 3: Run `python -m pytest -v` and `python -m compileall -q src tests`.** +- [ ] **Step 4: Run `ffmpeg -version` and `ffprobe -version`** and record availability without treating absence as a test failure. +- [ ] **Step 5: Launch an offscreen controlled startup** that opens the main window, processes events, and closes it via a timer. +- [ ] **Step 6: Review `git diff --check`, `git status --short`, and scan tracked project text for accidental API-key-like values.** +- [ ] **Step 7: Commit with `git commit -m "docs: add Windows setup and validation guide"`.**