test_voice_stt.py 3.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. """Tests hors Discord : python -m unittest test_voice_stt."""
  2. import asyncio
  3. import time
  4. import unittest
  5. import wave
  6. from io import BytesIO
  7. from types import SimpleNamespace
  8. from unittest.mock import AsyncMock, Mock, patch
  9. import httpx
  10. from voice_stt import STTSink
  11. class VoiceTests(unittest.IsolatedAsyncioTestCase):
  12. async def asyncSetUp(self):
  13. self.reply = AsyncMock()
  14. self.sink = STTSink(self.reply, 'http://stt/transcribe')
  15. self.user = SimpleNamespace(id=1, bot=False)
  16. self.pcm = b'\x00\x10' * (48000 * 2 // 2) # 0,5 s stéréo
  17. async def asyncTearDown(self):
  18. await self.sink.close()
  19. async def test_silence_and_separate_speakers(self):
  20. self.sink.worker_task.cancel()
  21. with self.sink.lock:
  22. self.sink.add_pcm(self.user, bytes(len(self.pcm)), time.monotonic())
  23. self.assertFalse(self.sink.buffers)
  24. self.sink.add_pcm(self.user, self.pcm, time.monotonic() - 2)
  25. self.sink.add_pcm(SimpleNamespace(id=2), self.pcm, time.monotonic())
  26. await asyncio.sleep(0.15)
  27. user, pcm = self.sink.queue.get_nowait()
  28. self.sink.queue.task_done()
  29. self.assertEqual(user.id, 1)
  30. self.assertEqual(pcm, self.pcm)
  31. self.assertIn(2, self.sink.buffers)
  32. async def test_audio_is_bounded_and_short_noise_ignored(self):
  33. self.sink.worker_task.cancel()
  34. with self.sink.lock:
  35. self.sink.add_pcm(self.user, self.pcm * 50, time.monotonic())
  36. self.assertEqual(len(self.sink.buffers[1][1]), 48000 * 4 * 20)
  37. self.sink.add_pcm(SimpleNamespace(id=2), self.pcm[:1000], time.monotonic() - 2)
  38. await asyncio.sleep(0.15)
  39. self.assertEqual(self.sink.queue.qsize(), 1)
  40. self.assertNotIn(2, self.sink.buffers)
  41. async def test_dave_decrypted_before_opus(self):
  42. session = SimpleNamespace(ready=True, decrypt=Mock(return_value=b'opus'))
  43. self.sink._voice_client = SimpleNamespace(_connection=SimpleNamespace(
  44. dave_protocol_version=1, dave_session=session))
  45. data = SimpleNamespace(opus=b'encrypted', packet=SimpleNamespace(is_silence=lambda: False))
  46. decoder = Mock()
  47. decoder.decode.return_value = self.pcm
  48. with patch('voice_stt.discord.opus.Decoder', return_value=decoder):
  49. self.sink.write(self.user, data)
  50. session.decrypt.assert_called_once()
  51. decoder.decode.assert_called_once_with(b'opus', fec=False)
  52. self.assertEqual(bytes(self.sink.buffers[1][1]), self.pcm)
  53. async def test_http_recovery_and_wav_format(self):
  54. await self.sink.close()
  55. post = AsyncMock(side_effect=[
  56. httpx.ConnectError('offline'),
  57. httpx.Response(200, json={'text': 'Bonjour'}, request=httpx.Request('POST', 'http://stt')),
  58. ])
  59. http = AsyncMock()
  60. http.__aenter__.return_value.post = post
  61. with patch('voice_stt.httpx.AsyncClient', return_value=http):
  62. self.sink = STTSink(self.reply, 'http://stt/transcribe')
  63. self.sink.queue.put_nowait((self.user, self.pcm))
  64. self.sink.queue.put_nowait((self.user, self.pcm))
  65. await asyncio.wait_for(self.sink.queue.join(), 2)
  66. self.reply.assert_awaited_once_with(self.user, 'Bonjour')
  67. payload = post.call_args.kwargs['files']['file'][1]
  68. with wave.open(BytesIO(payload)) as wav:
  69. self.assertEqual((wav.getnchannels(), wav.getsampwidth(), wav.getframerate()), (2, 2, 48000))
  70. self.assertEqual(wav.readframes(wav.getnframes()), self.pcm)
  71. async def test_close_discards_pending_audio(self):
  72. self.sink.queue.put_nowait((self.user, self.pcm))
  73. await self.sink.close()
  74. self.assertTrue(self.sink.closed)
  75. self.assertTrue(self.sink.queue.empty())
  76. self.assertTrue(self.sink.flush_task.done())
  77. self.assertTrue(self.sink.worker_task.done())
  78. self.reply.assert_not_awaited()
  79. if __name__ == '__main__':
  80. unittest.main()