You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

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, 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".