# Audio Preprocessing and Chunking 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:** Build a cancellable, incremental FFmpeg pipeline that creates duration-based overlapping speech-audio chunks, exact timestamp manifests, and GUI-visible progress. **Architecture:** Pure `Decimal` chunk calculations feed a service that probes once and launches one streaming FFmpeg process per chunk. Typed callbacks and cancellation stay UI-independent; a Qt worker adapts them into signals, while settings persistence supplies duration, overlap, and retention policy. **Tech Stack:** Python 3.12+, `decimal`, `dataclasses`, `subprocess`, FFmpeg/FFprobe, PySide6, pytest, pytest-qt. **Spec:** `docs/superpowers/specs/2026-08-30-audio-preprocessing-chunking-design.md` ## Global Constraints - Accept only `m4a`, `mp3`, `wav`, `mp4`, `mov`, `webm`, and `mkv` through the preprocessing boundary. - Use FFprobe before FFmpeg and require a positive finite duration plus an audio stream. - Default chunk duration is 900 seconds and overlap is 15 seconds; chunking is duration-based, not based on a presumed universal upload-size limit. - Encode direct-from-source mono 24 kHz AAC-LC at 64 kbps in `.m4a` without a large intermediate file. - Never load the recording into Python memory; one streaming FFmpeg child may run at a time. - Manifest timestamps use exact decimal seconds serialized as strings. - Job artifacts live in one verified job-specific temporary directory; remove it unless retention is enabled. - Do not call any transcription API. --- ## File Map - `src/voice_transcriptor/models.py`: shared immutable settings/media/chunk/progress/result values. - `src/voice_transcriptor/services/chunking.py`: pure exact chunk calculations and validation. - `src/voice_transcriptor/services/media_probe.py`: source stream metadata from FFprobe. - `src/voice_transcriptor/services/preprocessing.py`: process lifecycle, manifest, cancellation, progress, and cleanup. - `src/voice_transcriptor/services/settings.py`: backward-compatible advanced settings persistence. - `src/voice_transcriptor/ui/settings_dialog.py`: collapsible advanced preprocessing controls. - `src/voice_transcriptor/ui/main_window.py`: selection, metadata, worker, progress, and cancellation UI. - `src/voice_transcriptor/app.py`, `src/voice_transcriptor/__main__.py`: concrete wiring and entry points. - `tests/test_chunking.py`: exact calculation contracts. - `tests/test_preprocessing.py`: FFmpeg/manifest/lifecycle service contracts. - `tests/test_media_probe.py`, `tests/test_settings.py`: extended existing contracts. - `tests/ui/test_settings_dialog.py`, `tests/ui/test_main_window.py`, `tests/ui/test_app.py`: Qt behavior and startup. ### Task 1: Exact chunk model and calculations **Files:** - Modify: `src/voice_transcriptor/models.py` - Create: `src/voice_transcriptor/services/chunking.py` - Create: `tests/test_chunking.py` **Interfaces:** - Produces: `ChunkBoundary(index: int, start_seconds: Decimal, end_seconds: Decimal)` with derived `duration_seconds`. - Produces: `calculate_chunk_boundaries(total_duration, chunk_duration=Decimal("900"), overlap=Decimal("15")) -> tuple[ChunkBoundary, ...]`. - Produces: `total_chunk_duration(boundaries) -> Decimal` and `source_timestamp(boundary, local_seconds) -> Decimal`. - [ ] **Step 1: Write failing literal boundary tests.** Include `600 -> [(0, 600)]`, `900 -> [(0, 900)]`, `1800 -> [(0, 900), (885, 1785), (1770, 1800)]`, `901.25 -> [(0, 900), (885, 901.25)]`, and zero-overlap `1800 -> [(0, 900), (900, 1800)]`. - [ ] **Step 2: Write failing aggregate/offset/validation tests.** Assert the 1800-second default boundaries total `1830`, chunk two local `12.5` maps to source `897.5`, multi-hour `28800` ends exactly at `28800`, and invalid non-positive duration/chunk or overlap outside `[0, chunk)` raises `ValueError`. - [ ] **Step 3: Run `python -m pytest tests/test_chunking.py -v` and confirm failure because the module/interfaces do not exist.** - [ ] **Step 4: Implement exact calculation minimally.** Convert inputs with `Decimal(str(value))`, reject non-finite values, advance by `chunk - overlap`, and stop immediately when `end == total`. - [ ] **Step 5: Run the focused tests and confirm they pass; then run the existing suite to detect model regressions.** - [ ] **Step 6: Commit with `git commit -m "feat: add exact overlapping chunk calculations"`.** ### Task 2: Rich FFprobe source metadata **Files:** - Modify: `src/voice_transcriptor/models.py` - Modify: `src/voice_transcriptor/services/media_probe.py` - Modify: `tests/test_media_probe.py` **Interfaces:** - Extends: `MediaInfo(..., duration_text: str | None, has_audio: bool, has_video: bool)` while retaining existing fields. - `parse_probe_output` selects the first audio codec and preserves the raw positive duration string for exact chunk math. - [ ] **Step 1: Add failing tests for audio-only, video-with-audio, and video-without-audio FFprobe JSON.** Use complete payloads with `format.duration` and stream `codec_type`/`codec_name`; assert exact `duration_text`, `has_audio`, and `has_video` literals. - [ ] **Step 2: Run `python -m pytest tests/test_media_probe.py -v` and confirm missing-field/constructor failures.** - [ ] **Step 3: Extend `MediaInfo`, parser, and FFprobe `-show_entries` without changing shell/timeout/error safeguards.** - [ ] **Step 4: Run media-probe tests and the full suite.** - [ ] **Step 5: Commit with `git commit -m "feat: expose audio and video probe metadata"`.** ### Task 3: Preprocessing manifests and FFmpeg command contract **Files:** - Modify: `src/voice_transcriptor/models.py` - Create: `src/voice_transcriptor/services/preprocessing.py` - Create: `tests/test_preprocessing.py` **Interfaces:** - Produces: `PreprocessingOptions(chunk_duration_seconds=900, overlap_seconds=15, retain_temporary_files=False)`. - Produces: `CancellationToken.cancel()`, `.cancelled`, and `.raise_if_cancelled()`. - Produces: `PreprocessingProgress(percent: int, phase: str, message: str)`. - Produces: `PreprocessingResult(job_directory: Path, manifest_path: Path, retained: bool)` with `.cleanup()` and context-manager behavior. - Produces: `PreprocessingService(ffmpeg_path, probe_service, temporary_root=None, process_factory=subprocess.Popen).preprocess(source, options, token=None, progress=None) -> PreprocessingResult`. - [ ] **Step 1: Write failing tests for extension/settings/source validation.** Assert all seven extensions are accepted case-insensitively and unsupported extension, absent audio, absent/non-positive duration, and invalid overlap fail before process creation. - [ ] **Step 2: Write a failing command-contract test with a controlled process fake.** Assert list arguments include `-map 0:a:0`, `-vn`, `-sn`, `-dn`, exact `-ss`/`-t`, `-ac 1`, `-ar 24000`, `-c:a aac`, `-b:a 64k`, `-progress pipe:1`, and a job-owned `.m4a` path; assert one process per boundary. - [ ] **Step 3: Write failing manifest tests.** Read real JSON written under `tmp_path`; assert exact string timestamps, relative chunk paths, source/output metadata, completed count, and atomic terminal `completed` state. - [ ] **Step 4: Run `python -m pytest tests/test_preprocessing.py -v` and confirm import/interface failure.** - [ ] **Step 5: Implement validation, job creation, manifest serialization, atomic writes, command construction, and sequential process execution.** Use `mkdtemp`, `Popen` without a shell, text line buffering, `CREATE_NO_WINDOW` when available, and sanitized typed exceptions. - [ ] **Step 6: Run focused and full tests; refactor manifest helpers only while green.** - [ ] **Step 7: Commit with `git commit -m "feat: add streaming FFmpeg preprocessing"`.** ### Task 4: Progress, cancellation, and safe cleanup **Files:** - Modify: `src/voice_transcriptor/services/preprocessing.py` - Modify: `tests/test_preprocessing.py` **Interfaces:** - Progress callback receives monotonic `PreprocessingProgress` values. - Cancellation is reported with `PreprocessingCancelled`, distinct from `PreprocessingError`. - Cleanup only removes the resolved directory identity stored at creation. - [ ] **Step 1: Add failing progress tests.** Feed fake `out_time_us=450000000`, `progress=continue`, and `progress=end` lines; assert percentages are monotonic, current-chunk time is weighted against total planned chunk duration, and 100 occurs only after completion. - [ ] **Step 2: Add failing cancellation tests.** Cancel while the fake process emits progress; assert `terminate()` is called, later process factories are untouched, partial output is removed, and manifest state is `cancelled`. - [ ] **Step 3: Add failing lifecycle tests.** Assert default cleanup removes the exact job directory, retained mode preserves it, context-manager exit cleans it, and a tampered cleanup target raises rather than deleting another path. - [ ] **Step 4: Run the focused tests and confirm expected failures.** - [ ] **Step 5: Implement line-by-line progress parsing, cooperative termination/kill fallback, terminal manifest transitions, and identity-checked cleanup.** Never call `communicate()` in a way that buffers an unbounded stream. - [ ] **Step 6: Run focused and full tests.** - [ ] **Step 7: Commit with `git commit -m "feat: add preprocessing cancellation and cleanup"`.** ### Task 5: Persist advanced preprocessing settings **Files:** - Modify: `src/voice_transcriptor/models.py` - Modify: `src/voice_transcriptor/services/settings.py` - Modify: `tests/test_settings.py` **Interfaces:** - Extends: `AppSettings(model, language, output_directory, chunk_duration_seconds=900, chunk_overlap_seconds=15, retain_temporary_files=False)`. - Existing three-argument construction remains valid through defaults. - [ ] **Step 1: Add failing tests for defaults, new-value round trip, legacy three-key JSON, invalid overlap fallback, and continued absence of API-key fields.** Expected JSON literals include the three new snake-case keys. - [ ] **Step 2: Run `python -m pytest tests/test_settings.py -v` and confirm constructor/serialization failures.** - [ ] **Step 3: Implement backward-compatible loading and strict type/range validation.** Reject booleans where integer seconds are expected and require `0 <= overlap < duration`. - [ ] **Step 4: Run settings and full tests.** - [ ] **Step 5: Commit with `git commit -m "feat: persist preprocessing settings"`.** ### Task 6: Advanced settings UI **Files:** - Modify: `src/voice_transcriptor/ui/settings_dialog.py` - Modify: `tests/ui/test_settings_dialog.py` **Interfaces:** - Adds object-named widgets `advancedToggle`, `advancedPanel`, `chunkDurationInput`, `chunkOverlapInput`, and `retainTemporaryFilesInput`. - `settings_saved` emits the complete updated `AppSettings`. - [ ] **Step 1: Add failing Qt tests for initially hidden advanced panel, toggle visibility, populated defaults, complete save, and overlap-equals-duration warning without persistence.** - [ ] **Step 2: Run the focused UI test and confirm widgets are missing.** - [ ] **Step 3: Implement a checkable Advanced button, panel, two `QSpinBox` controls, and `QCheckBox`; preserve existing API-key handling and directory validation.** - [ ] **Step 4: Run focused UI and full tests.** - [ ] **Step 5: Commit with `git commit -m "feat: add advanced chunk settings"`.** ### Task 7: Main window and asynchronous preprocessing worker **Files:** - Create: `src/voice_transcriptor/ui/main_window.py` - Create: `tests/ui/test_main_window.py` **Interfaces:** - Produces: `MainWindow(media_probe, preprocessing_service, settings_repository, credentials)`. - Produces internal `PreprocessingWorker(QRunnable)` and signal object carrying progress/result/error/cancelled. - Provides `select_file(path: Path)`, `start_preprocessing()`, and `cancel_preprocessing()`. - [ ] **Step 1: Write failing Qt tests for controls and supported file selection.** Assert Browse, Settings, Prepare, progress bar, Cancel, metadata, and log exist; unsupported/multi-file inputs are rejected. - [ ] **Step 2: Write failing worker-state tests with a blocking fake service.** Start work, assert selection/settings/prepare disabled and Cancel enabled; emit progress and assert displayed value/message; cancel and assert the same token is cancelled; finish each terminal signal and assert controls restore. - [ ] **Step 3: Write failing stale-probe and close-during-work tests.** Ensure an older selection cannot overwrite newer metadata and close requests cancellation. - [ ] **Step 4: Run `python -m pytest tests/ui/test_main_window.py -v` and confirm import failure.** - [ ] **Step 5: Build the minimal native layout and async QRunnable orchestration.** Keep service calls off the GUI thread, connect queued signals, retain worker/signal references through completion, and clean results according to retention policy. - [ ] **Step 6: Run focused UI and full tests.** - [ ] **Step 7: Commit with `git commit -m "feat: add preprocessing desktop workflow"`.** ### Task 8: Application wiring and end-to-end verification **Files:** - Create: `src/voice_transcriptor/app.py` - Create: `src/voice_transcriptor/__main__.py` - Create: `tests/ui/test_app.py` - Modify: `README.md` **Interfaces:** - Produces: `create_main_window() -> MainWindow` and `main() -> int`. - Preserves console entry point `voice-transcriptor = voice_transcriptor.app:main`. - [ ] **Step 1: Write a failing offscreen smoke test that creates, shows, processes events for, and closes the concrete window.** - [ ] **Step 2: Run `python -m pytest tests/ui/test_app.py -v` and confirm bootstrap import failure.** - [ ] **Step 3: Wire detected FFmpeg/FFprobe paths, probe/preprocessing/settings/credential services, guarded startup messages, and `python -m voice_transcriptor`.** Missing tools must leave the window usable with preprocessing disabled. - [ ] **Step 4: Document supported formats, defaults, duration-based chunking, FFmpeg requirement, temporary retention, cancellation, and the absence of transcription API calls.** - [ ] **Step 5: Run `python -m pytest -v`, `python -m compileall -q src tests`, and `git diff --check`.** - [ ] **Step 6: Run `ffmpeg -version` and `ffprobe -version`. If present, generate a short sine/speech-like fixture under a job-owned temporary directory and run the real service, then inspect its manifest and clean the fixture; report absence separately.** - [ ] **Step 7: Review every specification requirement against code/tests and scan tracked text for credential-like values.** - [ ] **Step 8: Commit with `git commit -m "docs: document preprocessing workflow"`.**