Skip to content

Harden audio transcription input handling#2597

Merged
rmusser01 merged 5 commits into
devfrom
codex/audio-transcription-pipeline-hardening
Jul 4, 2026
Merged

Harden audio transcription input handling#2597
rmusser01 merged 5 commits into
devfrom
codex/audio-transcription-pipeline-hardening

Conversation

@rmusser01

@rmusser01 rmusser01 commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Summary

  • Reject audio uploads when canonical WAV conversion fails or returns empty, missing, or non-WAV output instead of falling back to compressed originals.
  • Canonicalize compressed direct-call inputs for Canary, Qwen3 ASR, VibeVoice, and Parakeet MLX, including base_dir checks for converted paths.
  • Normalize NeMo Parakeet and Canary ndarray input to mono 16 kHz, with fallback resampling and edge-case coverage.

Change summary

TODO: Human requester must replace this with the human-written change summary required by Docs/superpowers/AI_GENERATED_PR_CHANGE_SUMMARY_POLICY_2026_04_17.md before merge.

Test Plan

  • Focused pytest suite: 79 passed, 2 skipped
  • Ruff high-signal rules: passed
  • compileall on touched production files: passed
  • git diff --check: passed
  • Bandit on touched production files: ran; existing low-severity subprocess findings remain in Audio_Transcription_Lib.py outside this diff

Summary by cubic

Hardens the audio transcription pipeline by enforcing canonical 16 kHz mono WAV at the endpoint and adapters, validating RIFF/WAVE headers, and normalizing NumPy inputs for NeMo. Removes fallbacks to compressed inputs and forces fresh conversions (overwrite=True) to prevent spoofed or stale WAV reuse. Addresses TASK-12126.

  • Bug Fixes

    • Endpoint: rejects when WAV conversion import fails or conversion yields empty/missing/non-WAV output; validates .wav, existence, and RIFF/WAVE header; uses overwrite=True; returns 400 invalid_audio with no fallback.
    • Adapters: Canary, Qwen3 ASR, and VibeVoice convert compressed inputs to WAV with overwrite=True and enforce adapter base_dir; Parakeet MLX uses _ensure_wav_path_for_parakeet_mlx to validate existing/converted paths stay inside base_dir before buffered loading.
    • NeMo NumPy: _prepare_numpy_audio_for_nemo downmixes to mono float32 and resamples to 16 kHz; validates sample rate range; guards large ratios; uses polyphase when available and bounded linear fallback otherwise; handles empty/scalar arrays.
    • Tests: coverage for endpoint rejection (including header spoofing), adapter conversions (including overwrite=True), MLX base_dir, and NeMo resampling with SciPy/polyphase failure fallbacks; focused suite: 85 passed, 2 skipped.
  • Migration

    • Clients may now receive 400 invalid_audio when uploads cannot be converted to WAV. Upload canonical WAV or a decodable format.

Written for commit 8f5ecc6. Summary will update on new commits.

Review in cubic

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The required human-written change summary is still a TODO, and the template's Why and Risk & Rollback sections are missing. Replace the TODO with a human-written change summary, add explicit What changed and Why sections, and include Risk & Rollback details.
✅ Passed checks (4 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise and accurately summarizes the main change: hardening audio transcription input handling.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/audio-transcription-pipeline-hardening

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Harden STT transcription input canonicalization and NumPy resampling

🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Reject /audio/transcriptions uploads when canonical WAV conversion fails or is unusable.
• Canonicalize compressed inputs to WAV for soundfile-backed STT providers (incl. MLX base_dir
 safety).
• Normalize NeMo Canary/Parakeet NumPy inputs to mono 16 kHz with robust resampling fallback.
Diagram

graph TD
A["POST /audio/transcriptions"] --> B{"Canonical WAV conversion OK?"} -->|"yes"| C["Canonical .wav path"] --> D["STT provider registry"] --> E["Soundfile-backed adapters"] --> F["Provider transcribe"]
B -->|"no"| G["400 invalid_audio"]
C --> H["NumPy direct path"] --> I["Mono + 16kHz normalize"] --> F
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Centralize all WAV canonicalization at the API boundary only
  • ➕ Single enforcement point; fewer conversion call sites.
  • ➕ Simpler adapter/provider code.
  • ➖ Does not protect non-endpoint/direct-call code paths (e.g., internal adapters invoked with paths).
  • ➖ Harder to guarantee provider preconditions when inputs originate outside the endpoint.
2. Adopt a dedicated audio I/O layer (torchaudio/resampy) for decode+resample
  • ➕ More consistent decoding and resampling behavior across environments.
  • ➕ Potentially better audio-quality resampling defaults and fewer edge cases.
  • ➖ Adds or deepens heavyweight dependencies and platform-specific constraints.
  • ➖ Migration risk across many existing providers and ingestion flows.

Recommendation: Keep the current approach: enforce canonical WAV validity at the endpoint (no fallback to compressed originals), and also canonicalize within soundfile-backed adapters for direct-call safety. The dual-layer guard is justified because not all provider code paths originate at the FastAPI upload boundary. The NumPy resampling helper is a pragmatic fix that avoids hard dependency on SciPy while still using it when present.

Files changed (14) +1732 / -18

Bug fix (4) +192 / -18
audio_transcriptions.pyReject uploads when canonical WAV conversion fails or output is unusable +52/-5

Reject uploads when canonical WAV conversion fails or output is unusable

• Changes endpoint behavior to raise HTTP 400 invalid_audio when WAV conversion import fails or conversion fails. Adds validation for empty conversion results, non-.wav outputs, and missing output paths, and reuses the validated canonical Path for base_dir derivation.

tldw_Server_API/app/api/v1/endpoints/audio/audio_transcriptions.py

Audio_Transcription_Lib.pyCanonicalize Parakeet MLX buffered inputs to WAV with base_dir containment +40/-0

Canonicalize Parakeet MLX buffered inputs to WAV with base_dir containment

• Adds a helper to convert non-WAV inputs to WAV for Parakeet MLX, raising a clear STTTranscriptionError on conversion failures or unusable outputs. Enforces that converted paths remain within base_dir when provided, then applies the helper before MLX buffered transcription proceeds.

tldw_Server_API/app/core/Ingestion_Media_Processing/Audio/Audio_Transcription_Lib.py

Audio_Transcription_Nemo.pyNormalize ndarray inputs to mono 16 kHz for NeMo Canary/Parakeet +45/-4

Normalize ndarray inputs to mono 16 kHz for NeMo Canary/Parakeet

• Introduces a shared NumPy preparation helper that folds channels to mono, handles scalar/empty arrays, and resamples non-16 kHz audio (SciPy polyphase when available; linear interpolation fallback). Updates Canary and Parakeet direct NumPy transcription paths to use the normalized sample rate for temp WAV fallback.

tldw_Server_API/app/core/Ingestion_Media_Processing/Audio/Audio_Transcription_Nemo.py

stt_provider_adapter.pyCanonicalize soundfile-backed provider inputs to WAV (incl. base_dir safety) +55/-9

Canonicalize soundfile-backed provider inputs to WAV (incl. base_dir safety)

• Adds shared helpers to resolve input paths safely within base_dir and to convert compressed inputs to validated WAV outputs before provider loading. Applies the canonicalization to Canary, Qwen3 ASR, and VibeVoice adapter paths so they never receive compressed originals that soundfile cannot decode.

tldw_Server_API/app/core/Ingestion_Media_Processing/Audio/stt_provider_adapter.py

Tests (4) +437 / -0
test_audio_transcriptions_adapter_path.pyAdd endpoint regression tests for conversion failure and unusable outputs +142/-0

Add endpoint regression tests for conversion failure and unusable outputs

• Adds tests verifying the transcription endpoint returns HTTP 400 invalid_audio when conversion fails, returns empty output, or produces a non-WAV/missing output, and asserts adapters are not invoked with the original compressed upload in these cases.

tldw_Server_API/tests/Audio/test_audio_transcriptions_adapter_path.py

test_stt_provider_adapter.pyAdd unit tests ensuring adapters canonicalize compressed inputs to WAV +173/-0

Add unit tests ensuring adapters canonicalize compressed inputs to WAV

• Adds focused tests for Canary, Qwen3 ASR, and VibeVoice adapters to ensure convert_to_wav is invoked for compressed inputs and that downstream provider calls receive the converted WAV path (with base_dir propagated).

tldw_Server_API/tests/Audio/test_stt_provider_adapter.py

test_nemo_transcription.pyAdd tests for NumPy resampling and empty-array handling in NeMo paths +55/-0

Add tests for NumPy resampling and empty-array handling in NeMo paths

• Adds tests that patch model transcribe calls to confirm non-16 kHz ndarray inputs are resampled to 16 kHz for Canary and Parakeet direct NumPy transcription. Adds an empty-array coverage test to ensure preprocessing does not crash before provider validation.

tldw_Server_API/tests/Media_Ingestion_Modification/test_nemo_transcription.py

test_parakeet_mlx.pyAdd MLX buffered-path tests for conversion and base_dir containment +67/-0

Add MLX buffered-path tests for conversion and base_dir containment

• Adds tests asserting the MLX buffered transcription path converts compressed inputs to WAV before duration/loading and rejects conversion outputs that resolve outside base_dir. Uses monkeypatching to avoid requiring real MLX/librosa behavior.

tldw_Server_API/tests/Media_Ingestion_Modification/test_parakeet_mlx.py

Documentation (6) +1103 / -0
2026-07-03-audio-cpp-tts-provider-implementation-plan.mdAdd staged implementation plan for audio.cpp TTS provider +574/-0

Add staged implementation plan for audio.cpp TTS provider

• Introduces a detailed, test-driven plan for implementing an optional audio.cpp-backed TTS provider (audio_cpp), including registry routing, config scaffolding, HTTP client, adapter behavior, managed sidecar supervision, installer helper, and documentation updates.

Docs/superpowers/plans/2026-07-03-audio-cpp-tts-provider-implementation-plan.md

2026-07-03-audio-transcription-pipeline-hardening-plan.mdDocument completed hardening stages for STT transcription pipeline +25/-0

Document completed hardening stages for STT transcription pipeline

• Adds a concise plan/closeout record describing the endpoint canonicalization changes, provider boundary guards, NumPy resampling normalization, and verification commands/results.

Docs/superpowers/plans/2026-07-03-audio-transcription-pipeline-hardening-plan.md

2026-07-03-audio-cpp-tts-integration-design.mdAdd audio.cpp TTS integration design spec +312/-0

Add audio.cpp TTS integration design spec

• Adds an accepted design for integrating audio.cpp via an audio_cpp TTS provider, covering routing through TTSServiceV2, external vs managed sidecar modes, config schema constraints, security boundaries, error mapping, and test strategy.

Docs/superpowers/specs/2026-07-03-audio-cpp-tts-integration-design.md

task-12124 - Design-audio.cpp-TTS-provider-and-setup-integration.mdAdd completed backlog task for audio.cpp TTS design +70/-0

Add completed backlog task for audio.cpp TTS design

• Creates a task entry capturing acceptance criteria and implementation notes for the audio.cpp TTS integration design deliverable, linking to the new spec.

backlog/tasks/task-12124 - Design-audio.cpp-TTS-provider-and-setup-integration.md

task-12125 - Implement-audio.cpp-TTS-provider-and-setup-integration.mdAdd in-progress backlog task for audio.cpp TTS implementation +68/-0

Add in-progress backlog task for audio.cpp TTS implementation

• Creates an implementation tracking task that references the design and the new staged plan, including acceptance criteria for provider registration, adapter/client behavior, sidecar safety, installer scope, and verification.

backlog/tasks/task-12125 - Implement-audio.cpp-TTS-provider-and-setup-integration.md

task-12126 - Harden-audio-transcription-pipeline-input-handling.mdAdd completed backlog task for STT transcription hardening +54/-0

Add completed backlog task for STT transcription hardening

• Adds a completed task record describing the precise hardening behaviors implemented (endpoint conversion rejection, adapter canonicalization, MLX base_dir safety, NumPy resampling) and the executed verification suite.

backlog/tasks/task-12126 - Harden-audio-transcription-pipeline-input-handling.md

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request hardens the audio transcription pipeline and adds design and implementation plans for the audio_cpp TTS provider. Key improvements include rejecting audio uploads when WAV conversion fails, canonicalizing compressed inputs to WAV for soundfile-backed providers (Canary, Qwen3 ASR, VibeVoice, and Parakeet MLX), and resampling non-16 kHz NumPy audio to 16 kHz for in-memory NeMo transcriptions. Feedback highlights a security issue in _ensure_wav_path_for_parakeet_mlx where existing .wav files bypass the base_dir containment check, potentially allowing path traversal.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +404 to +407
def _ensure_wav_path_for_parakeet_mlx(audio_file_path: str, base_dir: Optional[Path] = None) -> str:
path_obj = Path(audio_file_path)
if path_obj.suffix.lower() == ".wav":
return str(path_obj)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

If base_dir is provided, existing .wav files bypass the directory containment check because the function returns early on line 407. To prevent path traversal or unauthorized file access, validate the .wav path against base_dir using resolve_safe_local_path before returning it.

def _ensure_wav_path_for_parakeet_mlx(audio_file_path: str, base_dir: Optional[Path] = None) -> str:
    path_obj = Path(audio_file_path)
    if path_obj.suffix.lower() == ".wav":
        if base_dir is not None:
            safe_path = resolve_safe_local_path(path_obj, base_dir)
            if safe_path is None:
                raise STTTranscriptionError(
                    "Parakeet MLX audio loading failed: WAV path is outside the allowed directory."
                )
            return str(safe_path)
        return str(path_obj)

@qodo-code-review

qodo-code-review Bot commented Jul 3, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 74 rules

Grey Divider


Action required

1. Spoofed WAV bypasses canonicalization ✓ Resolved 🐞 Bug ⛨ Security
Description
In create_transcription, the temp file suffix is derived from the upload filename before sniffing,
and convert_to_wav is a no-op for .wav inputs when overwrite=False; a non-WAV upload named *.wav can
therefore bypass canonical conversion while still passing the suffix/exists checks. This undermines
the “canonical WAV or 400 invalid_audio” guarantee and can either feed compressed/non-canonical
bytes downstream or trigger internal errors later in the pipeline.
Code

tldw_Server_API/app/api/v1/endpoints/audio/audio_transcriptions.py[R751-782]

+        if not canonical_path:
+            logger.debug(
+                'convert_to_wav returned empty output; rejecting audio upload: input_path={}',
+                temp_audio_path,
+            )
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail=_dictation_error_detail(
+                    http_status=status.HTTP_400_BAD_REQUEST,
+                    detail_status="invalid_audio",
+                    message="Audio file could not be converted to canonical WAV.",
+                ),
+            )
+
+        canonical_path_obj = PathLib(canonical_path)
+        if (
+            canonical_path_obj.suffix.lower() != ".wav"
+            or not canonical_path_obj.exists()
+        ):
+            logger.debug(
+                'convert_to_wav returned unusable output; rejecting audio upload: input_path={}, output_path={}',
+                temp_audio_path,
+                canonical_path,
+            )
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail=_dictation_error_detail(
+                    http_status=status.HTTP_400_BAD_REQUEST,
+                    detail_status="invalid_audio",
+                    message="Audio file could not be converted to canonical WAV.",
+                ),
+            )
Evidence
The endpoint stages uploads using the filename suffix first, then calls convert_to_wav with
overwrite=False and only validates output by suffix/exists; convert_to_wav explicitly skips work for
.wav inputs when overwrite=False, so a spoofed .wav upload can bypass canonicalization without
being detected.

tldw_Server_API/app/api/v1/endpoints/audio/audio_transcriptions.py[162-179]
tldw_Server_API/app/api/v1/endpoints/audio/audio_transcriptions.py[681-783]
tldw_Server_API/app/core/Ingestion_Media_Processing/Audio/Audio_Transcription_Lib.py[4038-4076]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`/audio/transcriptions` trusts the uploaded filename suffix and `convert_to_wav(..., overwrite=False)` can skip conversion for `.wav`-suffixed inputs. This allows non-WAV content uploaded as `*.wav` to bypass canonical conversion while still passing the current suffix/exists checks.

## Issue Context
- `_resolve_audio_upload_suffix()` prefers the filename suffix when present.
- `convert_to_wav()` returns early (no conversion/resample) when input already resolves to `.wav` and `overwrite=False`.
- The endpoint’s post-conversion validation only checks suffix and file existence, not the WAV container/header.

## Fix Focus Areas
- Ensure staged-upload suffix cannot be spoofed to skip conversion (prefer sniffed type when sample bytes disagree with filename, or explicitly validate WAV magic bytes).
- After conversion (or no-op), validate that the canonical file is truly a WAV container (e.g., `RIFF....WAVE` header and non-empty) before proceeding.
- Optionally, for `.wav` inputs, force conversion via `overwrite=True` (safe for temp uploads because the endpoint already deletes `canonical_path` when it differs from `temp_audio_path`).

### References
- tldw_Server_API/app/api/v1/endpoints/audio/audio_transcriptions.py[162-179]
- tldw_Server_API/app/api/v1/endpoints/audio/audio_transcriptions.py[681-783]
- tldw_Server_API/app/core/Ingestion_Media_Processing/Audio/Audio_Transcription_Lib.py[4038-4076]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. _canonicalize_wav_for_soundfile_adapter docstring missing ✓ Resolved 📘 Rule violation ✧ Quality
Description
The newly added helper _canonicalize_wav_for_soundfile_adapter has no function docstring. This
violates the docstring requirement and obscures important path-safety and conversion behavior for
providers.
Code

tldw_Server_API/app/core/Ingestion_Media_Processing/Audio/stt_provider_adapter.py[R155-203]

+def _resolve_adapter_audio_path(audio_path: str, base_dir: Path | None) -> Path:
+    path_obj = Path(audio_path)
+    if base_dir is None:
+        return path_obj
+
+    safe_path = resolve_safe_local_path(path_obj, base_dir)
+    if safe_path is None:
+        raise BadRequestError(f"Audio path rejected outside base_dir: {audio_path}")
+    return safe_path
+
+
+def _canonicalize_wav_for_soundfile_adapter(audio_path: str, base_dir: Path | None) -> Path:
+    path_obj = _resolve_adapter_audio_path(audio_path, base_dir)
+    if path_obj.suffix.lower() == ".wav":
+        return path_obj
+
+    try:
+        from tldw_Server_API.app.core.Ingestion_Media_Processing.Audio.Audio_Transcription_Lib import (  # type: ignore
+            ConversionError,
+            convert_to_wav,
+        )
+    except ImportError as exc:
+        raise BadRequestError("Audio WAV conversion is not available for this STT provider") from exc
+
+    try:
+        converted_path = convert_to_wav(
+            str(path_obj),
+            offset=0,
+            overwrite=False,
+            base_dir=base_dir,
+        )
+    except (ConversionError, OSError, RuntimeError, ValueError) as exc:
+        raise BadRequestError(f"Failed to convert audio file to WAV: {exc}") from exc
+
+    if not converted_path:
+        raise BadRequestError("Audio conversion did not produce a usable WAV file")
+
+    converted_obj = Path(converted_path)
+    if base_dir is not None:
+        safe_converted = resolve_safe_local_path(converted_obj, base_dir)
+        if safe_converted is None:
+            raise BadRequestError(f"Converted audio path rejected outside base_dir: {converted_path}")
+        converted_obj = safe_converted
+
+    if converted_obj.suffix.lower() != ".wav" or not converted_obj.exists():
+        raise BadRequestError("Audio conversion did not produce a usable WAV file")
+
+    return converted_obj
+
Evidence
PR Compliance ID 224214 requires docstrings for all functions. The added helpers
_resolve_adapter_audio_path and _canonicalize_wav_for_soundfile_adapter contain executable
statements immediately and include no docstring.

Rule 224214: Require docstrings for all modules, classes, and functions
tldw_Server_API/app/core/Ingestion_Media_Processing/Audio/stt_provider_adapter.py[155-203]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New helper functions were added without docstrings.

## Issue Context
These helpers enforce `base_dir` containment and WAV conversion behavior, and should be documented per the project policy.

## Fix Focus Areas
- tldw_Server_API/app/core/Ingestion_Media_Processing/Audio/stt_provider_adapter.py[155-203]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. test_audio_transcriptions_rejects_upload... docstring missing ✓ Resolved 📘 Rule violation ✧ Quality
Description
New top-level test functions were added without docstrings. This violates the requirement that all
functions include docstrings, making the intent of the test cases less clear.
Code

tldw_Server_API/tests/Audio/test_audio_transcriptions_adapter_path.py[R359-363]

+def test_audio_transcriptions_rejects_upload_when_wav_conversion_fails(
+    monkeypatch,
+    bypass_api_limits,
+):
+    monkeypatch.setenv("TEST_MODE", "true")
Evidence
PR Compliance ID 224214 requires docstrings for all functions. The new test function begins with
code (monkeypatch.setenv(...)) rather than a leading string-literal docstring.

Rule 224214: Require docstrings for all modules, classes, and functions
tldw_Server_API/tests/Audio/test_audio_transcriptions_adapter_path.py[359-367]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New test functions were added without docstrings.

## Issue Context
The policy requires a docstring as the first statement of each function.

## Fix Focus Areas
- tldw_Server_API/tests/Audio/test_audio_transcriptions_adapter_path.py[359-548]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (5)
4. test_audio_transcriptions_rejects_upload... missing type hints ✓ Resolved 📘 Rule violation ✧ Quality
Description
New test functions were added without type annotations for parameters and return values. This
violates the requirement for type hints on all function parameters and return values, reducing
static-checking value even in tests.
Code

tldw_Server_API/tests/Audio/test_audio_transcriptions_adapter_path.py[R359-362]

+def test_audio_transcriptions_rejects_upload_when_wav_conversion_fails(
+    monkeypatch,
+    bypass_api_limits,
+):
Evidence
PR Compliance ID 224215 requires explicit type hints for all parameters and return values. The added
test function signature introduces unannotated parameters and no return type annotation.

Rule 224215: Require type hints on all function parameters and return values
tldw_Server_API/tests/Audio/test_audio_transcriptions_adapter_path.py[359-362]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New test functions were added without parameter and return type annotations.

## Issue Context
The policy requires explicit parameter annotations and an explicit return annotation (typically `-> None`) for all functions, including tests.

## Fix Focus Areas
- tldw_Server_API/tests/Audio/test_audio_transcriptions_adapter_path.py[359-432]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. _ensure_wav_path_for_parakeet_mlx docstring missing ✓ Resolved 📘 Rule violation ✧ Quality
Description
The new helper function _ensure_wav_path_for_parakeet_mlx has no function docstring. This violates
the requirement that all functions include docstrings, reducing maintainability and auditability of
audio-handling logic.
Code

tldw_Server_API/app/core/Ingestion_Media_Processing/Audio/Audio_Transcription_Lib.py[R404-440]

+def _ensure_wav_path_for_parakeet_mlx(audio_file_path: str, base_dir: Optional[Path] = None) -> str:
+    path_obj = Path(audio_file_path)
+    if path_obj.suffix.lower() == ".wav":
+        return str(path_obj)
+
+    try:
+        wav_file_path = convert_to_wav(
+            str(path_obj),
+            offset=0,
+            overwrite=False,
+            base_dir=base_dir,
+        )
+    except (ConversionError, OSError, RuntimeError, TypeError, ValueError) as exc:
+        logging.debug(f"Parakeet MLX WAV conversion failed: {type(exc).__name__}")
+        raise STTTranscriptionError(
+            "Parakeet MLX audio loading failed: WAV conversion failed for compressed input."
+        ) from exc
+
+    if not wav_file_path:
+        raise STTTranscriptionError(
+            "Parakeet MLX audio loading failed: WAV conversion did not produce a usable WAV file."
+        )
+
+    wav_path = Path(wav_file_path)
+    if base_dir is not None:
+        safe_wav_path = resolve_safe_local_path(wav_path, base_dir)
+        if safe_wav_path is None:
+            raise STTTranscriptionError(
+                "Parakeet MLX audio loading failed: converted WAV path is outside the allowed directory."
+            )
+        wav_path = safe_wav_path
+
+    if wav_path.suffix.lower() != ".wav" or not wav_path.exists():
+        raise STTTranscriptionError(
+            "Parakeet MLX audio loading failed: WAV conversion did not produce a usable WAV file."
+        )
+    return str(wav_path)
Evidence
PR Compliance ID 224214 requires docstrings for all functions. The added function
_ensure_wav_path_for_parakeet_mlx begins executing code immediately with no leading string literal
docstring.

Rule 224214: Require docstrings for all modules, classes, and functions
tldw_Server_API/app/core/Ingestion_Media_Processing/Audio/Audio_Transcription_Lib.py[404-440]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A new function was added without a docstring, which violates the docstring-required policy.

## Issue Context
`_ensure_wav_path_for_parakeet_mlx` performs WAV canonicalization and safety checks; it should be documented for future maintenance.

## Fix Focus Areas
- tldw_Server_API/app/core/Ingestion_Media_Processing/Audio/Audio_Transcription_Lib.py[404-440]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. test_transcribe_batch_canary... docstring missing ✓ Resolved 📘 Rule violation ✧ Quality
Description
New test functions were added without docstrings. This violates the docstring requirement and
reduces readability of the newly introduced conversion-guard regression tests.
Code

tldw_Server_API/tests/Audio/test_stt_provider_adapter.py[R580-586]

+def test_transcribe_batch_canary_converts_compressed_input_before_soundfile(monkeypatch, tmp_path):
+    spa = _import_module()
+
+    import numpy as np
+    import soundfile as sf
+    import sys
+
Evidence
PR Compliance ID 224214 requires docstrings for all functions. The newly added test function starts
immediately with executable statements and includes no docstring.

Rule 224214: Require docstrings for all modules, classes, and functions
tldw_Server_API/tests/Audio/test_stt_provider_adapter.py[579-587]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New test functions were introduced without docstrings.

## Issue Context
Docstrings are required as the first statement in each function.

## Fix Focus Areas
- tldw_Server_API/tests/Audio/test_stt_provider_adapter.py[579-750]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. _prepare_numpy_audio_for_nemo docstring missing ✓ Resolved 📘 Rule violation ✧ Quality
Description
The new helper _prepare_numpy_audio_for_nemo has no function docstring. This violates the
docstring requirement and makes the resampling/normalization behavior harder to understand and
review.
Code

tldw_Server_API/app/core/Ingestion_Media_Processing/Audio/Audio_Transcription_Nemo.py[R143-176]

+def _prepare_numpy_audio_for_nemo(
+    audio_data: np.ndarray,
+    sample_rate: int,
+    *,
+    target_sample_rate: int = 16000,
+) -> tuple[np.ndarray, int]:
+    audio_np = np.asarray(audio_data, dtype=np.float32)
+    if audio_np.ndim == 0:
+        audio_np = audio_np.reshape(1)
+    elif audio_np.ndim > 1:
+        audio_np = np.mean(audio_np, axis=1).astype(np.float32, copy=False)
+    if audio_np.size == 0:
+        return np.ascontiguousarray(audio_np, dtype=np.float32), target_sample_rate
+    if sample_rate == target_sample_rate or sample_rate <= 0:
+        return np.ascontiguousarray(audio_np, dtype=np.float32), target_sample_rate
+
+    try:
+        from scipy import signal
+
+        divisor = math.gcd(int(sample_rate), int(target_sample_rate))
+        up = int(target_sample_rate) // divisor
+        down = int(sample_rate) // divisor
+        if up <= 1000 and down <= 1000:
+            resampled = signal.resample_poly(audio_np, up, down)
+            return np.ascontiguousarray(resampled, dtype=np.float32), target_sample_rate
+    except _NEMO_RESAMPLE_EXCEPTIONS as exc:
+        logging.debug(f"NeMo numpy audio polyphase resample failed; using linear fallback: {type(exc).__name__}")
+
+    ratio = float(target_sample_rate) / float(sample_rate)
+    new_len = max(1, int(round(len(audio_np) * ratio)))
+    x_old = np.linspace(0.0, 1.0, num=len(audio_np), endpoint=False, dtype=np.float32)
+    x_new = np.linspace(0.0, 1.0, num=new_len, endpoint=False, dtype=np.float32)
+    resampled = np.interp(x_new, x_old, audio_np)
+    return np.ascontiguousarray(resampled, dtype=np.float32), target_sample_rate
Evidence
PR Compliance ID 224214 requires all functions to have docstrings. _prepare_numpy_audio_for_nemo
is newly added and does not include a docstring as its first statement.

Rule 224214: Require docstrings for all modules, classes, and functions
tldw_Server_API/app/core/Ingestion_Media_Processing/Audio/Audio_Transcription_Nemo.py[143-176]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A new function was introduced without a docstring.

## Issue Context
`_prepare_numpy_audio_for_nemo` normalizes channel layout and resamples to a target sample rate; this behavior should be documented.

## Fix Focus Areas
- tldw_Server_API/app/core/Ingestion_Media_Processing/Audio/Audio_Transcription_Nemo.py[143-176]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. test_transcribe_batch_canary... missing type hints ✓ Resolved 📘 Rule violation ✧ Quality
Description
New test functions were added without type annotations for parameters and return values. This
violates the type-hints requirement and makes these tests harder to statically analyze and refactor
safely.
Code

tldw_Server_API/tests/Audio/test_stt_provider_adapter.py[580]

+def test_transcribe_batch_canary_converts_compressed_input_before_soundfile(monkeypatch, tmp_path):
Evidence
PR Compliance ID 224215 requires type annotations for all parameters and return values. The new test
test_transcribe_batch_canary_converts_compressed_input_before_soundfile is introduced with untyped
parameters and no return type annotation.

Rule 224215: Require type hints on all function parameters and return values
tldw_Server_API/tests/Audio/test_stt_provider_adapter.py[579-584]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New test functions were added without parameter/return type annotations.

## Issue Context
The compliance rule requires explicit annotations (e.g., `monkeypatch: pytest.MonkeyPatch`, `tmp_path: pathlib.Path`, `-> None`).

## Fix Focus Areas
- tldw_Server_API/tests/Audio/test_stt_provider_adapter.py[579-749]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

9. Stale WAV reuse risk ✓ Resolved 🐞 Bug ≡ Correctness
Description
The new _canonicalize_wav_for_soundfile_adapter uses convert_to_wav with overwrite=False, which will
reuse an existing <basename>.wav without checking freshness/provenance. In directories where files
collide or inputs change, this can silently transcribe the wrong audio (stale or unrelated WAV).
Code

tldw_Server_API/app/core/Ingestion_Media_Processing/Audio/stt_provider_adapter.py[R166-201]

+def _canonicalize_wav_for_soundfile_adapter(audio_path: str, base_dir: Path | None) -> Path:
+    path_obj = _resolve_adapter_audio_path(audio_path, base_dir)
+    if path_obj.suffix.lower() == ".wav":
+        return path_obj
+
+    try:
+        from tldw_Server_API.app.core.Ingestion_Media_Processing.Audio.Audio_Transcription_Lib import (  # type: ignore
+            ConversionError,
+            convert_to_wav,
+        )
+    except ImportError as exc:
+        raise BadRequestError("Audio WAV conversion is not available for this STT provider") from exc
+
+    try:
+        converted_path = convert_to_wav(
+            str(path_obj),
+            offset=0,
+            overwrite=False,
+            base_dir=base_dir,
+        )
+    except (ConversionError, OSError, RuntimeError, ValueError) as exc:
+        raise BadRequestError(f"Failed to convert audio file to WAV: {exc}") from exc
+
+    if not converted_path:
+        raise BadRequestError("Audio conversion did not produce a usable WAV file")
+
+    converted_obj = Path(converted_path)
+    if base_dir is not None:
+        safe_converted = resolve_safe_local_path(converted_obj, base_dir)
+        if safe_converted is None:
+            raise BadRequestError(f"Converted audio path rejected outside base_dir: {converted_path}")
+        converted_obj = safe_converted
+
+    if converted_obj.suffix.lower() != ".wav" or not converted_obj.exists():
+        raise BadRequestError("Audio conversion did not produce a usable WAV file")
+
Evidence
The adapter canonicalization wrapper passes overwrite=False, and convert_to_wav explicitly returns
an existing output WAV when overwrite=False. Without an mtime/hash check, an existing sibling WAV
can be reused even if it’s stale or unrelated.

tldw_Server_API/app/core/Ingestion_Media_Processing/Audio/stt_provider_adapter.py[166-202]
tldw_Server_API/app/core/Ingestion_Media_Processing/Audio/Audio_Transcription_Lib.py[4055-4086]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`_canonicalize_wav_for_soundfile_adapter()` converts compressed inputs by calling `convert_to_wav(..., overwrite=False)`. `convert_to_wav` will return an existing sibling WAV output without verifying it corresponds to the current input, which can lead to wrong-audio transcription.

## Issue Context
- The new helper is used by Canary/Qwen3 ASR/VibeVoice adapter paths.
- `convert_to_wav`’s skip path is purely based on output file existence when `overwrite=False`.

## Fix Focus Areas
- Add freshness/provenance validation before reusing an existing output WAV (e.g., if `out_path.mtime < input_path.mtime`, reconvert).
- Consider updating `convert_to_wav`’s skip logic to check `mtime` (or size/header) so all callers benefit.
- If you need stronger guarantees than `mtime`, consider emitting a unique output name (hash-based) for conversions to avoid basename collisions.

### References
- tldw_Server_API/app/core/Ingestion_Media_Processing/Audio/stt_provider_adapter.py[166-202]
- tldw_Server_API/app/core/Ingestion_Media_Processing/Audio/Audio_Transcription_Lib.py[4055-4086]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. Unbounded resample length ✓ Resolved 🐞 Bug ➹ Performance
Description
_prepare_numpy_audio_for_nemo’s linear interpolation fallback computes new_len directly from the
sample-rate ratio with no upper bound or sample_rate sanity checks. For implausible/misreported
sample rates, this can allocate extremely large arrays and cause high memory/CPU usage.
Code

tldw_Server_API/app/core/Ingestion_Media_Processing/Audio/Audio_Transcription_Nemo.py[R171-176]

+    ratio = float(target_sample_rate) / float(sample_rate)
+    new_len = max(1, int(round(len(audio_np) * ratio)))
+    x_old = np.linspace(0.0, 1.0, num=len(audio_np), endpoint=False, dtype=np.float32)
+    x_new = np.linspace(0.0, 1.0, num=new_len, endpoint=False, dtype=np.float32)
+    resampled = np.interp(x_new, x_old, audio_np)
+    return np.ascontiguousarray(resampled, dtype=np.float32), target_sample_rate
Evidence
The code computes ratio and new_len and then allocates x_new of length new_len without any
bounds. If sample_rate is very small, ratio becomes very large and new_len grows
proportionally.

tldw_Server_API/app/core/Ingestion_Media_Processing/Audio/Audio_Transcription_Nemo.py[143-176]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The linear resampling fallback in `_prepare_numpy_audio_for_nemo()` can produce extremely large arrays because `new_len` is unbounded and derived only from `(target_sr / sample_rate)`.

## Issue Context
The polyphase path mitigates many normal cases, but the fallback path will still run when SciPy is unavailable or ratios are large.

## Fix Focus Areas
- Add sample_rate validation (e.g., reject or clamp when `sample_rate` is outside a reasonable range like 8000–192000).
- Add a hard cap on `new_len` (or on multiplier) and fail fast with a clear error when exceeded.
- Consider capping by maximum supported audio duration in samples to prevent accidental/hostile allocations.

### References
- tldw_Server_API/app/core/Ingestion_Media_Processing/Audio/Audio_Transcription_Nemo.py[143-176]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread tldw_Server_API/tests/Audio/test_audio_transcriptions_adapter_path.py Outdated
Comment thread tldw_Server_API/tests/Audio/test_stt_provider_adapter.py Outdated
Comment thread tldw_Server_API/tests/Audio/test_stt_provider_adapter.py Outdated
Comment thread tldw_Server_API/app/api/v1/endpoints/audio/audio_transcriptions.py Outdated
Comment on lines +171 to +176
ratio = float(target_sample_rate) / float(sample_rate)
new_len = max(1, int(round(len(audio_np) * ratio)))
x_old = np.linspace(0.0, 1.0, num=len(audio_np), endpoint=False, dtype=np.float32)
x_new = np.linspace(0.0, 1.0, num=new_len, endpoint=False, dtype=np.float32)
resampled = np.interp(x_new, x_old, audio_np)
return np.ascontiguousarray(resampled, dtype=np.float32), target_sample_rate

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

10. Unbounded resample length 🐞 Bug ➹ Performance

_prepare_numpy_audio_for_nemo’s linear interpolation fallback computes new_len directly from the
sample-rate ratio with no upper bound or sample_rate sanity checks. For implausible/misreported
sample rates, this can allocate extremely large arrays and cause high memory/CPU usage.
Agent Prompt
## Issue description
The linear resampling fallback in `_prepare_numpy_audio_for_nemo()` can produce extremely large arrays because `new_len` is unbounded and derived only from `(target_sr / sample_rate)`.

## Issue Context
The polyphase path mitigates many normal cases, but the fallback path will still run when SciPy is unavailable or ratios are large.

## Fix Focus Areas
- Add sample_rate validation (e.g., reject or clamp when `sample_rate` is outside a reasonable range like 8000–192000).
- Add a hard cap on `new_len` (or on multiplier) and fail fast with a clear error when exceeded.
- Consider capping by maximum supported audio duration in samples to prevent accidental/hostile allocations.

### References
- tldw_Server_API/app/core/Ingestion_Media_Processing/Audio/Audio_Transcription_Nemo.py[143-176]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tldw_Server_API/app/api/v1/endpoints/audio/audio_transcriptions.py (1)

720-782: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consolidate the four identical invalid_audio rejections.

The import-failure, conversion-failure, empty-output, and non-WAV-output branches all raise the same 400 payload with the same message. Extracting a small local helper removes ~30 lines of duplication and keeps the failure contract in one place.

♻️ Proposed helper
def _raise_invalid_audio(*, cause: Optional[Exception] = None) -> None:
    exc = HTTPException(
        status_code=status.HTTP_400_BAD_REQUEST,
        detail=_dictation_error_detail(
            http_status=status.HTTP_400_BAD_REQUEST,
            detail_status="invalid_audio",
            message="Audio file could not be converted to canonical WAV.",
        ),
    )
    if cause is not None:
        raise exc from cause
    raise exc

Each branch then becomes a single _raise_invalid_audio(cause=e) / _raise_invalid_audio() call after its logger.debug(...).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tldw_Server_API/app/api/v1/endpoints/audio/audio_transcriptions.py` around
lines 720 - 782, The audio transcription flow repeats the same invalid_audio 400
rejection in several branches around _convert_to_wav and the post-conversion
validation, so extract a small local helper in audio_transcriptions.py to
centralize the HTTPException construction and reuse it from each failure path.
Keep the existing logger.debug calls, then replace each duplicated raise block
with a single helper call such as _raise_invalid_audio(cause=e) for exception
branches and _raise_invalid_audio() for empty/unusable output branches,
preserving the same _dictation_error_detail payload and exception chaining
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@tldw_Server_API/app/core/Ingestion_Media_Processing/Audio/Audio_Transcription_Nemo.py`:
- Around line 143-178: Add direct unit tests for _prepare_numpy_audio_for_nemo
to cover its edge cases instead of relying only on
transcribe_with_canary/transcribe_with_parakeet. Create tests that call the
helper directly for stereo input downmixing (ndim > 1), the resample_poly path
guard when up/down exceed 1000, and the scipy ImportError/exception fallback
path in the resampling block. Use the function name
_prepare_numpy_audio_for_nemo as the main target so the tests stay stable if
call sites change.
- Around line 143-178: The early-return in _prepare_numpy_audio_for_nemo
incorrectly treats non-positive sample_rate as if the audio were already at
target_sample_rate. Split the condition so only sample_rate ==
target_sample_rate returns unchanged audio and handle sample_rate <= 0 as
invalid input in a separate branch, either by raising a clear error or by
avoiding any resampled-rate return. Make sure the return value from
_prepare_numpy_audio_for_nemo, and the downstream callers such as
_temp_wav_from_numpy, only label audio as target_sample_rate when it has
actually been resampled.

---

Outside diff comments:
In `@tldw_Server_API/app/api/v1/endpoints/audio/audio_transcriptions.py`:
- Around line 720-782: The audio transcription flow repeats the same
invalid_audio 400 rejection in several branches around _convert_to_wav and the
post-conversion validation, so extract a small local helper in
audio_transcriptions.py to centralize the HTTPException construction and reuse
it from each failure path. Keep the existing logger.debug calls, then replace
each duplicated raise block with a single helper call such as
_raise_invalid_audio(cause=e) for exception branches and _raise_invalid_audio()
for empty/unusable output branches, preserving the same _dictation_error_detail
payload and exception chaining behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f75b45e3-86b6-4e24-b9d5-76c5c1444ef3

📥 Commits

Reviewing files that changed from the base of the PR and between 032a2d2 and 9f97f17.

⛔ Files ignored due to path filters (3)
  • Docs/superpowers/plans/2026-07-03-audio-cpp-tts-provider-implementation-plan.md is excluded by !docs/**
  • Docs/superpowers/plans/2026-07-03-audio-transcription-pipeline-hardening-plan.md is excluded by !docs/**
  • Docs/superpowers/specs/2026-07-03-audio-cpp-tts-integration-design.md is excluded by !docs/**
📒 Files selected for processing (11)
  • backlog/tasks/task-12124 - Design-audio.cpp-TTS-provider-and-setup-integration.md
  • backlog/tasks/task-12125 - Implement-audio.cpp-TTS-provider-and-setup-integration.md
  • backlog/tasks/task-12126 - Harden-audio-transcription-pipeline-input-handling.md
  • tldw_Server_API/app/api/v1/endpoints/audio/audio_transcriptions.py
  • tldw_Server_API/app/core/Ingestion_Media_Processing/Audio/Audio_Transcription_Lib.py
  • tldw_Server_API/app/core/Ingestion_Media_Processing/Audio/Audio_Transcription_Nemo.py
  • tldw_Server_API/app/core/Ingestion_Media_Processing/Audio/stt_provider_adapter.py
  • tldw_Server_API/tests/Audio/test_audio_transcriptions_adapter_path.py
  • tldw_Server_API/tests/Audio/test_stt_provider_adapter.py
  • tldw_Server_API/tests/Media_Ingestion_Modification/test_nemo_transcription.py
  • tldw_Server_API/tests/Media_Ingestion_Modification/test_parakeet_mlx.py

Comment on lines +143 to +178
def _prepare_numpy_audio_for_nemo(
audio_data: np.ndarray,
sample_rate: int,
*,
target_sample_rate: int = 16000,
) -> tuple[np.ndarray, int]:
audio_np = np.asarray(audio_data, dtype=np.float32)
if audio_np.ndim == 0:
audio_np = audio_np.reshape(1)
elif audio_np.ndim > 1:
audio_np = np.mean(audio_np, axis=1).astype(np.float32, copy=False)
if audio_np.size == 0:
return np.ascontiguousarray(audio_np, dtype=np.float32), target_sample_rate
if sample_rate == target_sample_rate or sample_rate <= 0:
return np.ascontiguousarray(audio_np, dtype=np.float32), target_sample_rate

try:
from scipy import signal

divisor = math.gcd(int(sample_rate), int(target_sample_rate))
up = int(target_sample_rate) // divisor
down = int(sample_rate) // divisor
if up <= 1000 and down <= 1000:
resampled = signal.resample_poly(audio_np, up, down)
return np.ascontiguousarray(resampled, dtype=np.float32), target_sample_rate
except _NEMO_RESAMPLE_EXCEPTIONS as exc:
logging.debug(f"NeMo numpy audio polyphase resample failed; using linear fallback: {type(exc).__name__}")

ratio = float(target_sample_rate) / float(sample_rate)
new_len = max(1, int(round(len(audio_np) * ratio)))
x_old = np.linspace(0.0, 1.0, num=len(audio_np), endpoint=False, dtype=np.float32)
x_new = np.linspace(0.0, 1.0, num=new_len, endpoint=False, dtype=np.float32)
resampled = np.interp(x_new, x_old, audio_np)
return np.ascontiguousarray(resampled, dtype=np.float32), target_sample_rate


Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add direct unit tests for _prepare_numpy_audio_for_nemo.

This helper is only exercised indirectly via transcribe_with_canary/transcribe_with_parakeet at a single 8kHz→16kHz ratio. Edge cases such as stereo (ndim > 1) downmix, the up > 1000 or down > 1000 guard that skips resample_poly, and the scipy ImportError fallback path remain untested.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@tldw_Server_API/app/core/Ingestion_Media_Processing/Audio/Audio_Transcription_Nemo.py`
around lines 143 - 178, Add direct unit tests for _prepare_numpy_audio_for_nemo
to cover its edge cases instead of relying only on
transcribe_with_canary/transcribe_with_parakeet. Create tests that call the
helper directly for stereo input downmixing (ndim > 1), the resample_poly path
guard when up/down exceed 1000, and the scipy ImportError/exception fallback
path in the resampling block. Use the function name
_prepare_numpy_audio_for_nemo as the main target so the tests stay stable if
call sites change.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Non-positive sample_rate is mislabeled as already resampled to target_sample_rate.

Line 156 combines two very different conditions: sample_rate == target_sample_rate (correctly no-op) and sample_rate <= 0 (invalid input). In the latter case, the raw un-resampled audio_np is returned but the function claims the audio is now at target_sample_rate. If this path is later hit by the fallback (_temp_wav_from_numpy(audio_np, model_sample_rate) at lines 553/642), the temp WAV would be tagged with an incorrect sample rate, silently corrupting playback speed/duration without raising any error.

🐛 Proposed fix
-    if sample_rate == target_sample_rate or sample_rate <= 0:
-        return np.ascontiguousarray(audio_np, dtype=np.float32), target_sample_rate
+    if sample_rate == target_sample_rate:
+        return np.ascontiguousarray(audio_np, dtype=np.float32), target_sample_rate
+    if sample_rate <= 0:
+        logging.debug(
+            f"NeMo numpy audio prep received non-positive sample_rate={sample_rate}; skipping resample"
+        )
+        return np.ascontiguousarray(audio_np, dtype=np.float32), sample_rate
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _prepare_numpy_audio_for_nemo(
audio_data: np.ndarray,
sample_rate: int,
*,
target_sample_rate: int = 16000,
) -> tuple[np.ndarray, int]:
audio_np = np.asarray(audio_data, dtype=np.float32)
if audio_np.ndim == 0:
audio_np = audio_np.reshape(1)
elif audio_np.ndim > 1:
audio_np = np.mean(audio_np, axis=1).astype(np.float32, copy=False)
if audio_np.size == 0:
return np.ascontiguousarray(audio_np, dtype=np.float32), target_sample_rate
if sample_rate == target_sample_rate or sample_rate <= 0:
return np.ascontiguousarray(audio_np, dtype=np.float32), target_sample_rate
try:
from scipy import signal
divisor = math.gcd(int(sample_rate), int(target_sample_rate))
up = int(target_sample_rate) // divisor
down = int(sample_rate) // divisor
if up <= 1000 and down <= 1000:
resampled = signal.resample_poly(audio_np, up, down)
return np.ascontiguousarray(resampled, dtype=np.float32), target_sample_rate
except _NEMO_RESAMPLE_EXCEPTIONS as exc:
logging.debug(f"NeMo numpy audio polyphase resample failed; using linear fallback: {type(exc).__name__}")
ratio = float(target_sample_rate) / float(sample_rate)
new_len = max(1, int(round(len(audio_np) * ratio)))
x_old = np.linspace(0.0, 1.0, num=len(audio_np), endpoint=False, dtype=np.float32)
x_new = np.linspace(0.0, 1.0, num=new_len, endpoint=False, dtype=np.float32)
resampled = np.interp(x_new, x_old, audio_np)
return np.ascontiguousarray(resampled, dtype=np.float32), target_sample_rate
def _prepare_numpy_audio_for_nemo(
audio_data: np.ndarray,
sample_rate: int,
*,
target_sample_rate: int = 16000,
) -> tuple[np.ndarray, int]:
audio_np = np.asarray(audio_data, dtype=np.float32)
if audio_np.ndim == 0:
audio_np = audio_np.reshape(1)
elif audio_np.ndim > 1:
audio_np = np.mean(audio_np, axis=1).astype(np.float32, copy=False)
if audio_np.size == 0:
return np.ascontiguousarray(audio_np, dtype=np.float32), target_sample_rate
if sample_rate == target_sample_rate:
return np.ascontiguousarray(audio_np, dtype=np.float32), target_sample_rate
if sample_rate <= 0:
logging.debug(
f"NeMo numpy audio prep received non-positive sample_rate={sample_rate}; skipping resample"
)
return np.ascontiguousarray(audio_np, dtype=np.float32), sample_rate
try:
from scipy import signal
divisor = math.gcd(int(sample_rate), int(target_sample_rate))
up = int(target_sample_rate) // divisor
down = int(sample_rate) // divisor
if up <= 1000 and down <= 1000:
resampled = signal.resample_poly(audio_np, up, down)
return np.ascontiguousarray(resampled, dtype=np.float32), target_sample_rate
except _NEMO_RESAMPLE_EXCEPTIONS as exc:
logging.debug(f"NeMo numpy audio polyphase resample failed; using linear fallback: {type(exc).__name__}")
ratio = float(target_sample_rate) / float(sample_rate)
new_len = max(1, int(round(len(audio_np) * ratio)))
x_old = np.linspace(0.0, 1.0, num=len(audio_np), endpoint=False, dtype=np.float32)
x_new = np.linspace(0.0, 1.0, num=new_len, endpoint=False, dtype=np.float32)
resampled = np.interp(x_new, x_old, audio_np)
return np.ascontiguousarray(resampled, dtype=np.float32), target_sample_rate
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@tldw_Server_API/app/core/Ingestion_Media_Processing/Audio/Audio_Transcription_Nemo.py`
around lines 143 - 178, The early-return in _prepare_numpy_audio_for_nemo
incorrectly treats non-positive sample_rate as if the audio were already at
target_sample_rate. Split the condition so only sample_rate ==
target_sample_rate returns unchanged audio and handle sample_rate <= 0 as
invalid input in a separate branch, either by raising a clear error or by
avoiding any resampled-rate return. Make sure the return value from
_prepare_numpy_audio_for_nemo, and the downstream callers such as
_temp_wav_from_numpy, only label audio as target_sample_rate when it has
actually been resampled.

@rmusser01 rmusser01 force-pushed the codex/audio-transcription-pipeline-hardening branch from fdc2e24 to 23bb566 Compare July 3, 2026 23:44
@rmusser01 rmusser01 force-pushed the codex/audio-transcription-pipeline-hardening branch from 461a324 to 8f5ecc6 Compare July 4, 2026 01:20
@rmusser01 rmusser01 merged commit 800a81b into dev Jul 4, 2026
17 of 18 checks passed
@rmusser01 rmusser01 deleted the codex/audio-transcription-pipeline-hardening branch July 4, 2026 01:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant