Dubbing's vocal-separation step died with `TypeError: An asyncio.Future,
a coroutine or an awaitable is required` whenever the backend ran on an
event loop without native async-subprocess support — notably the Windows
`SelectorEventLoop` that uvicorn forces under `--reload` (`bun desktop`).
On that loop `spawn_subprocess` returns a thread-backed `_AsyncCompatProc`
whose `.stderr` is a plain SYNC pipe, but `run_proc_streaming_stderr`
unconditionally did `await asyncio.wait_for(p.stderr.read(256), ...)`. A
sync `.read()` returns bytes, not a coroutine, so `wait_for` raised. The
extract step survived only because it uses `communicate()` (async-wrapped).
The wrapper now advertises `uses_sync_pipes`; the streaming reader keys off
it and, on that loop, runs the process to completion via the wrapper's async
`communicate()` and replays stderr as the same `('stderr', line)` events —
no live progress bar on the degraded loop, but demucs actually runs. The
native async path (Proactor/posix — every release build, macOS, Linux) is
untouched. Regression test covers the fallback (fails before, passes after)
and pins the native path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>