Skip to content

fix(dub): async-ify _pitch_preserving_stretch (Greptile P1 from #133) - #152

Merged
debpalash merged 1 commit into
mainfrom
fix/dub-stretch-async
May 29, 2026
Merged

fix(dub): async-ify _pitch_preserving_stretch (Greptile P1 from #133)#152
debpalash merged 1 commit into
mainfrom
fix/dub-stretch-async

Conversation

@debpalash

@debpalash debpalash commented May 29, 2026

Copy link
Copy Markdown
Owner

Addresses the Greptile P1 flagged on #133.

Problem

_pitch_preserving_stretch ran a blocking subprocess.run() inside the _stream async generator (directly on the event loop, not in an executor). Each ffmpeg atempo invocation is ~50-100 ms, so a 100-segment time_stretch dub job froze the event loop for seconds — health-check polls, status SSE streams, and every concurrent API request stalled.

Fix

  • _pitch_preserving_stretchasync def, using asyncio.create_subprocess_exec + await proc.communicate(input=…) (mirrors run_proc_streaming_stderr in dub_pipeline.py).
  • await the single call site in _stream.
  • Dropped the now-unused import subprocess.

Behavior is otherwise identical (same ffmpeg command, same pad/trim, same RuntimeError-on-failure contract).

Tests

tests/test_pitch_stretch_async.py — asserts it returns a coroutine (can't block the loop), hits the target length via real ffmpeg, and the no-op (already-target-length) path. 15 pass with the existing timing-strategy suite.

No default-behavior change, no version bump.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Performance Improvements

    • Improved audio pitch-preservation functionality to eliminate blocking during audio processing operations, resulting in smoother and more responsive application behavior when handling audio files.
  • Tests

    • Added comprehensive tests for audio stretching to ensure reliable operation and correct output characteristics.

Review Change Stack

_pitch_preserving_stretch ran a blocking subprocess.run() inside the
`_stream` async generator (on the event loop). Each ffmpeg atempo call is
~50-100 ms, so on a multi-segment time_stretch dub job it froze health
checks, status SSE, and every other concurrent request for seconds.

Convert to asyncio.create_subprocess_exec + await communicate() (same
pattern as run_proc_streaming_stderr); await the call site in _stream.
Drop the now-unused `import subprocess`.

Tests: async coroutine + target-length + no-op cases (real ffmpeg).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5465522f-2def-42b9-bb3f-8d60f003f48c

📥 Commits

Reviewing files that changed from the base of the PR and between 79473c4 and 4493339.

📒 Files selected for processing (2)
  • backend/api/routers/dub_generate.py
  • tests/test_pitch_stretch_async.py

📝 Walkthrough

Walkthrough

This PR converts the _pitch_preserving_stretch helper from a synchronous subprocess-based ffmpeg call to an async implementation using asyncio.create_subprocess_exec. The function signature changes to async def, the call site is updated to await the function, and new async tests validate the behavior.

Changes

Async ffmpeg pitch stretch

Layer / File(s) Summary
Async implementation of _pitch_preserving_stretch
backend/api/routers/dub_generate.py
_pitch_preserving_stretch is converted to async def with docstring updated; ffmpeg execution logic replaces subprocess.run with asyncio.create_subprocess_exec, awaiting proc.communicate and raising RuntimeError on failure or no output.
Call site integration with await
backend/api/routers/dub_generate.py
time_stretch slot-fitting path is updated to await _pitch_preserving_stretch instead of calling it synchronously.
Async behavior validation
tests/test_pitch_stretch_async.py
New test module verifies _pitch_preserving_stretch returns a coroutine, that awaiting it produces the correct output shape and dtype, and that no-op behavior is preserved when target length matches input.

🎯 2 (Simple) | ⏱️ ~12 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description comprehensively covers the problem, solution, and testing approach with clear technical details, but is missing the required template structure (Summary, Changes list, Type checkbox, Testing section, Checklist items, and Screenshots). Restructure the description to follow the provided template by adding the required sections (Summary, Changes, Type, Testing, Checklist) with appropriate checkbox selections and completion of all applicable checklist items.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(dub): async-ify _pitch_preserving_stretch (Greptile P1 from #133)' clearly and specifically summarizes the main change: converting a synchronous function to async to fix blocking behavior.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 fix/dub-stretch-async

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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


import asyncio

import numpy as np
@greptile-apps

greptile-apps Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a blocking-subprocess bug in the dub generation pipeline by converting _pitch_preserving_stretch from a synchronous function (using subprocess.run) to an async one (using asyncio.create_subprocess_exec + proc.communicate()).

  • _pitch_preserving_stretch is now async def; the single call site in the _stream async generator is updated with await, and the now-unused import subprocess is removed.
  • New tests/test_pitch_stretch_async.py asserts the function returns a coroutine (regression guard against accidental sync revert), validates the real-ffmpeg stretch path hits the target sample count, and covers the noop early-return path.

Confidence Score: 5/5

Safe to merge — the change is a focused, mechanical conversion of one function from synchronous subprocess to async subprocess with no behavioral changes to the ffmpeg command, error handling, or pad/trim logic.

Both changed files are tightly scoped: the production change touches only _pitch_preserving_stretch and its single call site, and the test file correctly covers the async contract plus the real-ffmpeg output shape. No other callers exist in the codebase.

No files require special attention.

Important Files Changed

Filename Overview
backend/api/routers/dub_generate.py Converts _pitch_preserving_stretch to async using asyncio.create_subprocess_exec + proc.communicate(); single call site correctly updated with await; import subprocess removed. Implementation is correct and matches the established run_proc_streaming_stderr pattern.
tests/test_pitch_stretch_async.py New test file placed correctly in tests/ (conftest.py adds backend/ to sys.path). Covers the async contract assertion and the real-ffmpeg path. Noop test's shape check is slightly under-specified (shape[-1] only; see comment).

Sequence Diagram

sequenceDiagram
    participant EL as Event Loop
    participant SG as _stream (async gen)
    participant PPS as _pitch_preserving_stretch (async)
    participant FF as ffmpeg subprocess

    Note over EL,FF: Before this PR — blocking path
    EL->>SG: iterate segment
    SG->>PPS: subprocess.run(...) [BLOCKS event loop ~50-100ms]
    PPS-->>SG: bytes result
    Note over EL: health-checks / SSE / concurrent requests stall

    Note over EL,FF: After this PR — non-blocking path
    EL->>SG: iterate segment
    SG->>PPS: await _pitch_preserving_stretch(...)
    PPS->>FF: asyncio.create_subprocess_exec(ffmpeg ...)
    PPS->>FF: "await proc.communicate(input=pcm_bytes)"
    FF-->>PPS: (stdout, stderr)
    PPS-->>SG: stretched tensor
    Note over EL: other coroutines run freely while ffmpeg executes
Loading

Fix All in Claude Code

Reviews (1): Last reviewed commit: "fix(dub): async-ify _pitch_preserving_st..." | Re-trigger Greptile

Comment on lines +30 to +31
out = asyncio.run(_pitch_preserving_stretch(wav, sr, sr))
assert out.shape[-1] == sr

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 The noop test only asserts on the last dimension, so a hypothetical regression that changed the number of channels (e.g., accidentally returning a 1-D tensor) would silently pass. Asserting the full shape keeps the contract tight.

Suggested change
out = asyncio.run(_pitch_preserving_stretch(wav, sr, sr))
assert out.shape[-1] == sr
out = asyncio.run(_pitch_preserving_stretch(wav, sr, sr))
assert out.shape == (1, sr)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

@debpalash
debpalash merged commit 993e6cf into main May 29, 2026
15 checks passed
@debpalash
debpalash deleted the fix/dub-stretch-async branch May 29, 2026 17:43
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.

2 participants