From d99aa606233e4c622835acd0578d779534b9310a Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 19 Jan 2026 02:15:43 +0000 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9A=A1=20Optimize=20video=20frame=20writ?= =?UTF-8?q?ing=20to=20ffmpeg?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Avoids unnecessary memory copying by passing numpy arrays/memoryviews directly to subprocess stdin instead of creating intermediate bytes objects. This reduces memory pressure and allocation overhead when processing high-resolution video frames. --- discord_video_node.py | 9 ++++--- test_output.txt | 0 tests/test_numpy_subprocess.py | 46 ++++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 test_output.txt create mode 100644 tests/test_numpy_subprocess.py diff --git a/discord_video_node.py b/discord_video_node.py index 94a0d05..29c0e1f 100644 --- a/discord_video_node.py +++ b/discord_video_node.py @@ -640,7 +640,7 @@ class DiscordSendSaveVideo: # Convert tensor images to bytes # Optimization: Use tensor_to_numpy_uint8 for faster conversion - images_bytes = map(lambda x: tensor_to_numpy_uint8(x).tobytes(), image_sequence) + images_bytes = map(lambda x: tensor_to_numpy_uint8(x), image_sequence) # Base ffmpeg arguments args = [ @@ -697,7 +697,7 @@ class DiscordSendSaveVideo: i_pix_fmt = 'rgb24' # Optimization: Use tensor_to_numpy_uint8 for faster conversion - images_bytes = map(lambda x: tensor_to_numpy_uint8(x).tobytes(), image_sequence) + images_bytes = map(lambda x: tensor_to_numpy_uint8(x), image_sequence) # Set up ffmpeg arguments based on format loop_args = [] @@ -855,10 +855,11 @@ class DiscordSendSaveVideo: output_file_with_audio_path ] - audio_data = a_waveform.squeeze(0).transpose(0,1).numpy().tobytes() + audio_data = a_waveform.squeeze(0).transpose(0,1).numpy() try: - res = subprocess.run(mux_args, input=audio_data, env=env, capture_output=True, check=True) + # Use memoryview to avoid copy while satisfying subprocess.run input check + res = subprocess.run(mux_args, input=memoryview(audio_data), env=env, capture_output=True, check=True) if res.stderr: print(res.stderr.decode(*ENCODE_ARGS), end="", file=sys.stderr) diff --git a/test_output.txt b/test_output.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_numpy_subprocess.py b/tests/test_numpy_subprocess.py new file mode 100644 index 0000000..ac171fb --- /dev/null +++ b/tests/test_numpy_subprocess.py @@ -0,0 +1,46 @@ +import unittest +import numpy as np +import subprocess +import sys +import os + +class TestNumpyToSubprocess(unittest.TestCase): + """ + Verify that subprocess.Popen.stdin.write accepts numpy arrays directly. + For subprocess.run(input=...), we need to be careful with numpy arrays due to ambiguity check in subprocess module. + """ + + def test_popen_stdin_write_numpy(self): + """Test writing numpy array to Popen.stdin""" + # Create a small numpy array + data = np.arange(256, dtype=np.uint8) + + # Use 'cat' to echo input to output + p = subprocess.Popen(['cat'], stdin=subprocess.PIPE, stdout=subprocess.PIPE) + + # Write numpy array directly + p.stdin.write(data) + out, _ = p.communicate() + + # Verify output matches input data bytes + self.assertEqual(out, data.tobytes()) + self.assertEqual(len(out), 256) + + def test_run_input_memoryview(self): + """ + Test passing numpy array as memoryview to subprocess.run input. + subprocess.run checks truthiness of input which fails for numpy arrays. + So for subprocess.run we should use memoryview(array) or array.tobytes(). + However, in discord_video_node.py we use subprocess.run for audio. + """ + data = np.arange(256, dtype=np.uint8) + + # Use 'cat' to echo input to output + # memoryview works and avoids copy + res = subprocess.run(['cat'], input=memoryview(data), capture_output=True) + + self.assertEqual(res.stdout, data.tobytes()) + self.assertEqual(len(res.stdout), 256) + +if __name__ == "__main__": + unittest.main() From b944db244e8579f2596a1f56222ee29fae16779c Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 19 Jan 2026 02:39:37 +0000 Subject: [PATCH 2/3] fix(video): ensure contiguous array for audio subprocess and rename var - Fix BufferError when passing non-contiguous transposed audio array to memoryview/subprocess by using np.ascontiguousarray - Rename 'images_bytes' to 'image_chunks' to better reflect that it contains numpy arrays, not bytes objects - Improve test portability by using sys.executable instead of 'cat' --- discord_video_node.py | 16 +++++++------- test_output.txt | 0 tests/test_numpy_subprocess.py | 38 +++++++++++++++++++++++++++------- 3 files changed, 40 insertions(+), 14 deletions(-) delete mode 100644 test_output.txt diff --git a/discord_video_node.py b/discord_video_node.py index 29c0e1f..71b461e 100644 --- a/discord_video_node.py +++ b/discord_video_node.py @@ -640,7 +640,7 @@ class DiscordSendSaveVideo: # Convert tensor images to bytes # Optimization: Use tensor_to_numpy_uint8 for faster conversion - images_bytes = map(lambda x: tensor_to_numpy_uint8(x), image_sequence) + image_chunks = map(lambda x: tensor_to_numpy_uint8(x), image_sequence) # Base ffmpeg arguments args = [ @@ -660,9 +660,9 @@ class DiscordSendSaveVideo: process = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env) # Feed frames to ffmpeg - for image_bytes in images_bytes: + for chunk in image_chunks: pbar.update(1) - process.stdin.write(image_bytes) + process.stdin.write(chunk) # Close stdin and get output process.stdin.close() @@ -697,7 +697,7 @@ class DiscordSendSaveVideo: i_pix_fmt = 'rgb24' # Optimization: Use tensor_to_numpy_uint8 for faster conversion - images_bytes = map(lambda x: tensor_to_numpy_uint8(x), image_sequence) + image_chunks = map(lambda x: tensor_to_numpy_uint8(x), image_sequence) # Set up ffmpeg arguments based on format loop_args = [] @@ -793,9 +793,9 @@ class DiscordSendSaveVideo: process = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env) # Feed frames to ffmpeg - for image_bytes in images_bytes: + for chunk in image_chunks: pbar.update(1) - process.stdin.write(image_bytes) + process.stdin.write(chunk) # Close stdin and get output process.stdin.close() @@ -855,7 +855,9 @@ class DiscordSendSaveVideo: output_file_with_audio_path ] - audio_data = a_waveform.squeeze(0).transpose(0,1).numpy() + # Use ascontiguousarray to ensure the transposed array is C-contiguous. + # This avoids BufferError in subprocess.run when using memoryview on non-contiguous arrays. + audio_data = np.ascontiguousarray(a_waveform.squeeze(0).transpose(0,1).numpy()) try: # Use memoryview to avoid copy while satisfying subprocess.run input check diff --git a/test_output.txt b/test_output.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_numpy_subprocess.py b/tests/test_numpy_subprocess.py index ac171fb..0d76689 100644 --- a/tests/test_numpy_subprocess.py +++ b/tests/test_numpy_subprocess.py @@ -15,8 +15,9 @@ class TestNumpyToSubprocess(unittest.TestCase): # Create a small numpy array data = np.arange(256, dtype=np.uint8) - # Use 'cat' to echo input to output - p = subprocess.Popen(['cat'], stdin=subprocess.PIPE, stdout=subprocess.PIPE) + # Use python to echo input to output (cross-platform) + cmd = [sys.executable, '-c', 'import sys; sys.stdout.buffer.write(sys.stdin.buffer.read())'] + p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) # Write numpy array directly p.stdin.write(data) @@ -29,18 +30,41 @@ class TestNumpyToSubprocess(unittest.TestCase): def test_run_input_memoryview(self): """ Test passing numpy array as memoryview to subprocess.run input. - subprocess.run checks truthiness of input which fails for numpy arrays. - So for subprocess.run we should use memoryview(array) or array.tobytes(). - However, in discord_video_node.py we use subprocess.run for audio. """ data = np.arange(256, dtype=np.uint8) - # Use 'cat' to echo input to output + # Use python to echo input to output (cross-platform) + cmd = [sys.executable, '-c', 'import sys; sys.stdout.buffer.write(sys.stdin.buffer.read())'] # memoryview works and avoids copy - res = subprocess.run(['cat'], input=memoryview(data), capture_output=True) + res = subprocess.run(cmd, input=memoryview(data), capture_output=True) self.assertEqual(res.stdout, data.tobytes()) self.assertEqual(len(res.stdout), 256) + def test_run_input_fixed_non_contiguous(self): + """ + Test that using ascontiguousarray makes the non-contiguous array accepted by subprocess.run + """ + # Create a 2D array and transpose it to make it non-contiguous + data = np.zeros((10, 10), dtype=np.uint8) + # Fill with some data + for i in range(10): + for j in range(10): + data[i, j] = i + j + + # Transpose creates a non-contiguous view + transposed_data = data.T + self.assertFalse(transposed_data.flags['C_CONTIGUOUS']) + + # Fix it using ascontiguousarray + contiguous_data = np.ascontiguousarray(transposed_data) + self.assertTrue(contiguous_data.flags['C_CONTIGUOUS']) + + # Now pass to subprocess + mv = memoryview(contiguous_data) + cmd = [sys.executable, '-c', 'import sys; sys.stdout.buffer.write(sys.stdin.buffer.read())'] + res = subprocess.run(cmd, input=mv, capture_output=True) + self.assertEqual(res.stdout, contiguous_data.tobytes()) + if __name__ == "__main__": unittest.main() From 8be6270aef035d8123e6247c8f1b37fb4df03771 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 19 Jan 2026 03:06:07 +0000 Subject: [PATCH 3/3] fix(video): ensure contiguous arrays for both audio and video paths - Fix potential BufferError in video frame writing path by enforcing C-contiguity with np.ascontiguousarray - Add regression tests for non-contiguous array writing to Popen.stdin - Rename variables for clarity (images_bytes -> image_chunks) - Ensure cross-platform compatibility in tests --- discord_video_node.py | 6 ++++-- tests/test_numpy_subprocess.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/discord_video_node.py b/discord_video_node.py index 71b461e..047eb32 100644 --- a/discord_video_node.py +++ b/discord_video_node.py @@ -640,7 +640,8 @@ class DiscordSendSaveVideo: # Convert tensor images to bytes # Optimization: Use tensor_to_numpy_uint8 for faster conversion - image_chunks = map(lambda x: tensor_to_numpy_uint8(x), image_sequence) + # Ensure contiguity to avoid ValueError in subprocess.stdin.write + image_chunks = map(lambda x: np.ascontiguousarray(tensor_to_numpy_uint8(x)), image_sequence) # Base ffmpeg arguments args = [ @@ -697,7 +698,8 @@ class DiscordSendSaveVideo: i_pix_fmt = 'rgb24' # Optimization: Use tensor_to_numpy_uint8 for faster conversion - image_chunks = map(lambda x: tensor_to_numpy_uint8(x), image_sequence) + # Ensure contiguity to avoid ValueError in subprocess.stdin.write + image_chunks = map(lambda x: np.ascontiguousarray(tensor_to_numpy_uint8(x)), image_sequence) # Set up ffmpeg arguments based on format loop_args = [] diff --git a/tests/test_numpy_subprocess.py b/tests/test_numpy_subprocess.py index 0d76689..246cb52 100644 --- a/tests/test_numpy_subprocess.py +++ b/tests/test_numpy_subprocess.py @@ -66,5 +66,36 @@ class TestNumpyToSubprocess(unittest.TestCase): res = subprocess.run(cmd, input=mv, capture_output=True) self.assertEqual(res.stdout, contiguous_data.tobytes()) + def test_popen_stdin_write_fixed_non_contiguous(self): + """ + Test writing fixed (made contiguous) numpy array to Popen.stdin. + """ + # Create a 2D array and transpose it to make it non-contiguous + data = np.zeros((10, 10), dtype=np.uint8) + # Fill with some data + for i in range(10): + for j in range(10): + data[i, j] = i + j + + transposed_data = data.T + self.assertFalse(transposed_data.flags['C_CONTIGUOUS']) + + # Fix it + contiguous_data = np.ascontiguousarray(transposed_data) + self.assertTrue(contiguous_data.flags['C_CONTIGUOUS']) + + # Use python to echo input to output (cross-platform) + cmd = [sys.executable, '-c', 'import sys; sys.stdout.buffer.write(sys.stdin.buffer.read())'] + p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) + + try: + # Should succeed now + p.stdin.write(contiguous_data) + out, _ = p.communicate() + self.assertEqual(out, contiguous_data.tobytes()) + + except Exception as e: + self.fail(f"Caught unexpected exception: {e}") + if __name__ == "__main__": unittest.main()