perf(discord): stream voice PCM to ffmpeg instead of a temp file
pcm_to_wav staged every captured utterance in a NamedTemporaryFile just to hand ffmpeg an input path, then unlinked it. Feed the PCM to ffmpeg's stdin instead: one fewer file created, written, read back and removed per voice utterance, and the try/finally cleanup goes away with it. The WAV output deliberately still goes to output_path rather than being captured from stdout. ffmpeg cannot seek on a pipe, so a piped WAV is written with placeholder 0xFFFFFFFF RIFF/data chunk sizes -- Python's wave module then reports 2147483647 frames for a 1s clip, and strict readers misjudge the length. Writing to the real path lets ffmpeg seek back and patch the header. Tests cover both halves: that the PCM goes over stdin with no temp file, and (when ffmpeg is installed) that the resulting header reports the true frame count. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
579672d87d
commit
70a3c2d9c9
|
|
@ -838,34 +838,32 @@ class VoiceReceiver:
|
|||
@staticmethod
|
||||
def pcm_to_wav(pcm_data: bytes, output_path: str,
|
||||
src_rate: int = 48000, src_channels: int = 2):
|
||||
"""Convert raw PCM to 16kHz mono WAV via ffmpeg."""
|
||||
with tempfile.NamedTemporaryFile(suffix=".pcm", delete=False) as f:
|
||||
f.write(pcm_data)
|
||||
pcm_path = f.name
|
||||
try:
|
||||
from hermes_cli._subprocess_compat import windows_hide_flags
|
||||
"""Convert raw PCM to 16kHz mono WAV via ffmpeg.
|
||||
|
||||
subprocess.run(
|
||||
[
|
||||
resolve_ffmpeg_executable(), "-y", "-loglevel", "error",
|
||||
"-f", "s16le",
|
||||
"-ar", str(src_rate),
|
||||
"-ac", str(src_channels),
|
||||
"-i", pcm_path,
|
||||
"-ar", "16000",
|
||||
"-ac", "1",
|
||||
output_path,
|
||||
],
|
||||
check=True,
|
||||
timeout=10,
|
||||
stdin=subprocess.DEVNULL,
|
||||
creationflags=windows_hide_flags(),
|
||||
)
|
||||
finally:
|
||||
try:
|
||||
os.unlink(pcm_path)
|
||||
except OSError:
|
||||
pass
|
||||
The PCM is fed straight to ffmpeg's stdin, which avoids staging it in a
|
||||
temp file on every utterance. The WAV is still written to *output_path*
|
||||
rather than captured from stdout: ffmpeg cannot seek on a pipe, so a
|
||||
piped WAV carries placeholder (0xFFFFFFFF) RIFF/data sizes that make
|
||||
strict readers misreport the length.
|
||||
"""
|
||||
from hermes_cli._subprocess_compat import windows_hide_flags
|
||||
|
||||
subprocess.run(
|
||||
[
|
||||
resolve_ffmpeg_executable(), "-y", "-loglevel", "error",
|
||||
"-f", "s16le",
|
||||
"-ar", str(src_rate),
|
||||
"-ac", str(src_channels),
|
||||
"-i", "pipe:0",
|
||||
"-ar", "16000",
|
||||
"-ac", "1",
|
||||
output_path,
|
||||
],
|
||||
input=pcm_data,
|
||||
check=True,
|
||||
timeout=10,
|
||||
creationflags=windows_hide_flags(),
|
||||
)
|
||||
|
||||
|
||||
def _read_dm_role_auth_guild() -> Optional[int]:
|
||||
|
|
|
|||
|
|
@ -1947,3 +1947,54 @@ class TestStreamTtsTempfileFallback:
|
|||
)
|
||||
# And the temp file is cleaned up afterwards.
|
||||
assert not os.path.exists(played[0]), "temp WAV was not unlinked"
|
||||
|
||||
|
||||
class TestPcmToWav:
|
||||
"""pcm_to_wav streams PCM through ffmpeg's stdin, not a temp file."""
|
||||
|
||||
def test_pcm_is_piped_to_stdin_not_staged_on_disk(self, tmp_path):
|
||||
from plugins.platforms.discord.adapter import VoiceReceiver
|
||||
|
||||
out = tmp_path / "out.wav"
|
||||
with patch("plugins.platforms.discord.adapter.subprocess.run") as run:
|
||||
VoiceReceiver.pcm_to_wav(b"\x00\x01" * 16, str(out))
|
||||
|
||||
args, kwargs = run.call_args
|
||||
cmd = args[0]
|
||||
assert kwargs["input"] == b"\x00\x01" * 16, "PCM must be fed via stdin"
|
||||
assert "pipe:0" in cmd, "ffmpeg must read the PCM from stdin"
|
||||
assert cmd[-1] == str(out), (
|
||||
"the WAV must be written to the real path; ffmpeg cannot seek on a "
|
||||
"pipe, so a piped WAV gets placeholder RIFF/data sizes"
|
||||
)
|
||||
assert not any(str(a).endswith(".pcm") for a in cmd), (
|
||||
"no temp .pcm file should be staged"
|
||||
)
|
||||
|
||||
@pytest.mark.skipif(
|
||||
__import__("shutil").which("ffmpeg") is None, reason="ffmpeg not installed",
|
||||
)
|
||||
def test_output_wav_header_reports_true_length(self, tmp_path):
|
||||
"""A piped-stdout WAV reports 0xFFFFFFFF sizes; the written file must not."""
|
||||
import math
|
||||
import struct
|
||||
import wave
|
||||
|
||||
from plugins.platforms.discord.adapter import VoiceReceiver
|
||||
|
||||
frames = 48000 # 1s @ 48kHz stereo
|
||||
pcm = b"".join(
|
||||
struct.pack("<hh", v, v)
|
||||
for v in (
|
||||
int(20000 * math.sin(2 * math.pi * 440 * i / 48000))
|
||||
for i in range(frames)
|
||||
)
|
||||
)
|
||||
out = tmp_path / "out.wav"
|
||||
VoiceReceiver.pcm_to_wav(pcm, str(out))
|
||||
|
||||
with wave.open(str(out)) as w:
|
||||
assert w.getnchannels() == 1
|
||||
assert w.getframerate() == 16000
|
||||
# 48kHz -> 16kHz is a 3x decimation of a 1s clip.
|
||||
assert w.getnframes() == 16000
|
||||
|
|
|
|||
Loading…
Reference in New Issue