Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 26 additions & 28 deletions plugins/platforms/discord/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -711,34 +711,32 @@ def check_silence(self) -> list:
@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

subprocess.run(
[
"ffmpeg", "-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
"""Convert raw PCM to 16kHz mono WAV via ffmpeg.

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

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.

Please retain resolve_ffmpeg_executable() here rather than restoring the literal "ffmpeg". Current main uses the resolver for this conversion so Windows winget installs and FFMPEG_PATH work when ffmpeg is absent from PATH (9b89da23fb8d76a7c208168c57c3eb5bec891d60; tests/gateway/test_voice_command.py:776).

subprocess.run(
[
"ffmpeg", "-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]:
Expand Down
51 changes: 51 additions & 0 deletions tests/gateway/test_voice_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -2974,3 +2974,54 @@ def test_per_chat_isolation(self):
fn, adapter = self._make_adapter(default=False, enabled={"chat1"})
assert fn(adapter, "chat1") is True
assert fn(adapter, "chat2") is False


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