14 KiB
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, andmkvthrough 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
.m4awithout 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 derivedduration_seconds. -
Produces:
calculate_chunk_boundaries(total_duration, chunk_duration=Decimal("900"), overlap=Decimal("15")) -> tuple[ChunkBoundary, ...]. -
Produces:
total_chunk_duration(boundaries) -> Decimalandsource_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-overlap1800 -> [(0, 900), (900, 1800)]. -
Step 2: Write failing aggregate/offset/validation tests. Assert the 1800-second default boundaries total
1830, chunk two local12.5maps to source897.5, multi-hour28800ends exactly at28800, and invalid non-positive duration/chunk or overlap outside[0, chunk)raisesValueError. -
Step 3: Run
python -m pytest tests/test_chunking.py -vand 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 bychunk - overlap, and stop immediately whenend == 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_outputselects 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.durationand streamcodec_type/codec_name; assert exactduration_text,has_audio, andhas_videoliterals. -
Step 2: Run
python -m pytest tests/test_media_probe.py -vand confirm missing-field/constructor failures. -
Step 3: Extend
MediaInfo, parser, and FFprobe-show_entrieswithout 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.m4apath; 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 terminalcompletedstate. -
Step 4: Run
python -m pytest tests/test_preprocessing.py -vand confirm import/interface failure. -
Step 5: Implement validation, job creation, manifest serialization, atomic writes, command construction, and sequential process execution. Use
mkdtemp,Popenwithout a shell, text line buffering,CREATE_NO_WINDOWwhen 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
PreprocessingProgressvalues. -
Cancellation is reported with
PreprocessingCancelled, distinct fromPreprocessingError. -
Cleanup only removes the resolved directory identity stored at creation.
-
Step 1: Add failing progress tests. Feed fake
out_time_us=450000000,progress=continue, andprogress=endlines; 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 iscancelled. -
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 -vand 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, andretainTemporaryFilesInput. -
settings_savedemits the complete updatedAppSettings. -
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
QSpinBoxcontrols, andQCheckBox; 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(), andcancel_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 -vand 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() -> MainWindowandmain() -> 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 -vand 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, andgit diff --check. -
Step 6: Run
ffmpeg -versionandffprobe -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".