diff --git a/Speech/src/SpeechHelpersTrait.php b/Speech/src/SpeechHelpersTrait.php new file mode 100644 index 000000000000..5945daba8a59 --- /dev/null +++ b/Speech/src/SpeechHelpersTrait.php @@ -0,0 +1,117 @@ +setUri($audio); + } else { + $recognitionAudio->setContent($audio); + } + } elseif (is_resource($audio)) { + $recognitionAudio->setContent(stream_get_contents($audio)); + } else { + throw new InvalidArgumentException( + 'Given $audio is not valid. ' . + 'Audio must be a RecognitionAudio ' . + 'object, a string of bytes, a valid ' . + 'Google Cloud Storage URI, or a resource.' + ); + } + return $recognitionAudio; + } + + /** + * @param string $requestClass + * @param iterable|resource|string $audio + * @return mixed An iterable of StreamingRecognizeRequest instances matching the requested version of Speech + */ + private function createStreamingRequestsHelper($requestClass, $audio) + { + // First, convert string/resource audio into an iterable + if (is_string($audio)) { + $audio = [$audio]; + } + if (is_resource($audio)) { + $audio = $this->createAudioStreamFromResource($audio); + } + + // For each chuck in iterable $audio, convert to a request if necessary + foreach ($audio as $audioChunk) { + if (is_object($audioChunk) && $audioChunk instanceof $requestClass) { + yield $audioChunk; + } elseif (is_string($audioChunk)) { + $request = new $requestClass(); + $request->setAudioContent($audioChunk); + yield $request; + } else { + throw new InvalidArgumentException( + 'Found invalid audio chunk in $audio. ' . + 'Audio must be a resource, a string of ' . + 'bytes, or an iterable of StreamingRecognizeRequest[] ' . + 'or string[].' + ); + } + } + } + + /** + * Convert a PHP resource instance into an iterable of data "chunks". + * + * @param resource $resource The resource object to read data from. + * @param int $chunkSize The chunk size to use, in bytes. Defaults to 32000 + * @return Generator An iterable of strings that have been read from the resource. + */ + public function createAudioStreamFromResource($resource, $chunkSize = 32000) + { + while (!feof($resource)) { + $chunk = fread($resource, $chunkSize); + if (strlen($chunk) > 0) { + yield $chunk; + } + } + } +} diff --git a/Speech/src/V1/SpeechClient.php b/Speech/src/V1/SpeechClient.php index 57370550ed45..9c885f700fc0 100644 --- a/Speech/src/V1/SpeechClient.php +++ b/Speech/src/V1/SpeechClient.php @@ -30,6 +30,7 @@ namespace Google\Cloud\Speech\V1; +use Google\Cloud\Speech\SpeechHelpersTrait; use Google\Cloud\Speech\V1\Gapic\SpeechGapicClient; /** @@ -37,6 +38,76 @@ */ class SpeechClient extends SpeechGapicClient { - // This class is intentionally empty, and is intended to hold manual - // additions to the generated {@see SpeechClientImpl} class. + use SpeechHelpersTrait; + + /** + * Helper method to create a RecognitionAudio object from audio data. + * + * @param resource|string|RecognitionAudio $audio *Required* The audio data to be recognized. This can be a RecognitionAudio + * object, a Google Cloud Storage URI, a resource object, or a string of bytes. + * @return RecognitionAudio + */ + public function createRecognitionAudio($audio) + { + return $this->createRecognitionAudioHelper(RecognitionAudio::class, $audio); + } + + /** + * Helper method to create a stream of StreamingRecognizeRequest objects from audio data. + * + * @param iterable|resource|string $audio *Required* The audio data to be converted into a stream of requests. This + * can be a resource, a string of bytes, or an iterable of + * StreamingRecognizeRequest[] or string[]. + * @return StreamingRecognizeRequest[] + */ + public function createStreamingRequests($audio) + { + return $this->createStreamingRequestsHelper(StreamingRecognizeRequest::class, $audio); + } + + /** + * Performs speech recognition on a stream of audio data. This method is only available via + * the gRPC API (not REST). + * + * Example: + * ``` + * use Google\Cloud\Speech\V1\RecognitionConfig_AudioEncoding; + * use Google\Cloud\Speech\V1\RecognitionConfig; + * use Google\Cloud\Speech\V1\StreamingRecognitionConfig; + * + * $recognitionConfig = new RecognitionConfig(); + * $recognitionConfig->setEncoding(RecognitionConfig_AudioEncoding::FLAC); + * $recognitionConfig->setSampleRateHertz(44100); + * $recognitionConfig->setLanguageCode('en-US'); + * $config = new StreamingRecognitionConfig(); + * $config->setConfig($recognitionConfig); + * + * $audioResource = fopen('path/to/audio.flac', 'r'); + * + * $responses = $speechClient->recognizeAudioStream($config, $audioResource); + * + * foreach ($responses as $element) { + * // doSomethingWith($element); + * } + * ``` + * + * @param StreamingRecognitionConfig $config *Required* Provides information to the recognizer that specifies how to + * process the request. + * @param iterable|resource|string $audio *Required* Audio data to be streamed. Can be a resource, a string of bytes, + * or an iterable of StreamingRecognizeRequest[] or string[]. + * @param array $optionalArgs { + * Optional. + * + * @type int $timeoutMillis + * Timeout to use for this call. + * } + * @return StreamingRecognizeResponse[] + */ + public function recognizeAudioStream($config, $audio, $optionalArgs = []) + { + $bidiStream = $this->streamingRecognize($optionalArgs); + $bidiStream->write((new StreamingRecognizeRequest())->setStreamingConfig($config)); + $bidiStream->writeAll($this->createStreamingRequestsHelper(StreamingRecognizeRequest::class, $audio)); + return $bidiStream->closeWriteAndReadAll(); + } } diff --git a/Speech/src/V1beta1/SpeechClient.php b/Speech/src/V1beta1/SpeechClient.php index 8308ec2e8b4e..2c1f690d406c 100644 --- a/Speech/src/V1beta1/SpeechClient.php +++ b/Speech/src/V1beta1/SpeechClient.php @@ -30,6 +30,7 @@ namespace Google\Cloud\Speech\V1beta1; +use Google\Cloud\Speech\SpeechHelpersTrait; use Google\Cloud\Speech\V1beta1\Gapic\SpeechGapicClient; /** @@ -37,6 +38,76 @@ */ class SpeechClient extends SpeechGapicClient { - // This class is intentionally empty, and is intended to hold manual - // additions to the generated {@see SpeechClientImpl} class. + use SpeechHelpersTrait; + + /** + * Helper method to create a RecognitionAudio object from audio data. + * + * @param resource|string|RecognitionAudio $audio *Required* The audio data to be recognized. This can be a RecognitionAudio + * object, a Google Cloud Storage URI, a resource object, or a string of bytes. + * @return RecognitionAudio + */ + public function createRecognitionAudio($audio) + { + return $this->createRecognitionAudioHelper(RecognitionAudio::class, $audio); + } + + /** + * Helper method to create a stream of StreamingRecognizeRequest objects from audio data. + * + * @param iterable|resource|string $audio *Required* The audio data to be converted into a stream of requests. This + * can be a resource, a string of bytes, or an iterable of + * StreamingRecognizeRequest[] or string[]. + * @return StreamingRecognizeRequest[] + */ + public function createStreamingRequests($audio) + { + return $this->createStreamingRequestsHelper(StreamingRecognizeRequest::class, $audio); + } + + /** + * Performs speech recognition on a stream of audio data. This method is only available via + * the gRPC API (not REST). + * + * Example: + * ``` + * use Google\Cloud\Speech\V1beta1\RecognitionConfig_AudioEncoding; + * use Google\Cloud\Speech\V1beta1\RecognitionConfig; + * use Google\Cloud\Speech\V1beta1\StreamingRecognitionConfig; + * + * $recognitionConfig = new RecognitionConfig(); + * $recognitionConfig->setEncoding(RecognitionConfig_AudioEncoding::FLAC); + * $recognitionConfig->setSampleRate(44100); + * $recognitionConfig->setLanguageCode('en-US'); + * $config = new StreamingRecognitionConfig(); + * $config->setConfig($recognitionConfig); + * + * $audioResource = fopen('path/to/audio.flac', 'r'); + * + * $responses = $speechClient->recognizeAudioStream($config, $audioResource); + * + * foreach ($responses as $element) { + * // doSomethingWith($element); + * } + * ``` + * + * @param StreamingRecognitionConfig $config *Required* Provides information to the recognizer that specifies how to + * process the request. + * @param iterable|resource|string $audio *Required* Audio data to be streamed. Can be a resource, a string of bytes, + * or an iterable of StreamingRecognizeRequest[] or string[]. + * @param array $optionalArgs { + * Optional. + * + * @type int $timeoutMillis + * Timeout to use for this call. + * } + * @return StreamingRecognizeResponse[] + */ + public function recognizeAudioStream($config, $audio, $optionalArgs = []) + { + $bidiStream = $this->streamingRecognize($optionalArgs); + $bidiStream->write((new StreamingRecognizeRequest())->setStreamingConfig($config)); + $bidiStream->writeAll($this->createStreamingRequestsHelper(StreamingRecognizeRequest::class, $audio)); + return $bidiStream->closeWriteAndReadAll(); + } } diff --git a/Speech/tests/Snippet/V1/SpeechClientTest.php b/Speech/tests/Snippet/V1/SpeechClientTest.php new file mode 100644 index 000000000000..4b1064c1c079 --- /dev/null +++ b/Speech/tests/Snippet/V1/SpeechClientTest.php @@ -0,0 +1,73 @@ +transport = $this->prophesize(TransportInterface::class); + $this->client = new SpeechClient([ + 'transport' => $this->transport->reveal(), + ]); + } + + public function testRecognizeAudioStream() + { + $snippet = $this->snippetFromMethod(SpeechClient::class, 'recognizeAudioStream'); + $snippet->addLocal('speechClient', $this->client); + + $snippet->replace( + "path/to/audio.flac", + "php://temp" + ); + + $expectedResponseStream = [ + new StreamingRecognizeResponse(), + new StreamingRecognizeResponse(), + ]; + + $mockBidiStreamingCall = new MockBidiStreamingCall($expectedResponseStream); + + $this->transport->startBidiStreamingCall(Argument::allOf( + Argument::type(Call::class), + Argument::which('getMethod', 'google.cloud.speech.v1.Speech/StreamingRecognize') + ), + Argument::type('array') + ) + ->shouldBeCalledTimes(1) + ->willReturn(new BidiStream($mockBidiStreamingCall)); + + $res = $snippet->invoke(); + } +} diff --git a/Speech/tests/Snippet/V1beta1/SpeechClientTest.php b/Speech/tests/Snippet/V1beta1/SpeechClientTest.php new file mode 100644 index 000000000000..259e93303328 --- /dev/null +++ b/Speech/tests/Snippet/V1beta1/SpeechClientTest.php @@ -0,0 +1,73 @@ +transport = $this->prophesize(TransportInterface::class); + $this->client = new SpeechClient([ + 'transport' => $this->transport->reveal(), + ]); + } + + public function testRecognizeAudioStream() + { + $snippet = $this->snippetFromMethod(SpeechClient::class, 'recognizeAudioStream'); + $snippet->addLocal('speechClient', $this->client); + + $snippet->replace( + "path/to/audio.flac", + "php://temp" + ); + + $expectedResponseStream = [ + new StreamingRecognizeResponse(), + new StreamingRecognizeResponse(), + ]; + + $mockBidiStreamingCall = new MockBidiStreamingCall($expectedResponseStream); + + $this->transport->startBidiStreamingCall(Argument::allOf( + Argument::type(Call::class), + Argument::which('getMethod', 'google.cloud.speech.v1beta1.Speech/StreamingRecognize') + ), + Argument::type('array') + ) + ->shouldBeCalledTimes(1) + ->willReturn(new BidiStream($mockBidiStreamingCall)); + + $res = $snippet->invoke(); + } +} diff --git a/Speech/tests/Unit/SpeechHelpersTraitTest.php b/Speech/tests/Unit/SpeechHelpersTraitTest.php new file mode 100644 index 000000000000..26cc55ccfaa8 --- /dev/null +++ b/Speech/tests/Unit/SpeechHelpersTraitTest.php @@ -0,0 +1,75 @@ +implementation = \Google\Cloud\Core\Testing\TestHelpers::impl(SpeechHelpersTrait::class); + } + + /** + * @dataProvider createAudioStreamDataProvider + */ + public function testCreateAudioStreamFromResource($resource, $chunkSize, $expectedData) + { + if (is_null($chunkSize)) { + $audioData = $this->implementation->call( + 'createAudioStreamFromResource', + [$resource] + ); + } else { + $audioData = $this->implementation->call( + 'createAudioStreamFromResource', + [$resource, $chunkSize] + ); + } + + $this->assertSame($expectedData, iterator_to_array($audioData)); + } + + public function createAudioStreamDataProvider() + { + $data = "abcdefghijklmnop"; + return [ + [$this->createResource($data), null, [$data]], + [$this->createResource($data), strlen($data), [$data]], + [$this->createResource($data), 5, ["abcde", "fghij", "klmno", "p"]], + ]; + } + + private function createResource($data) + { + $resource = fopen('php://memory','r+'); + fwrite($resource, $data); + rewind($resource); + return $resource; + } +} + diff --git a/Speech/tests/Unit/V1/SpeechClientTest.php b/Speech/tests/Unit/V1/SpeechClientTest.php new file mode 100644 index 000000000000..859175e358dc --- /dev/null +++ b/Speech/tests/Unit/V1/SpeechClientTest.php @@ -0,0 +1,160 @@ +transport = $this->prophesize(TransportInterface::class); + $this->client = new SpeechClient([ + 'transport' => $this->transport->reveal(), + ]); + } + + private function createRecognitionConfig() + { + $encoding = RecognitionConfig_AudioEncoding::FLAC; + $sampleRateHertz = 44100; + $languageCode = 'en-US'; + $recognitionConfig = new RecognitionConfig(); + $recognitionConfig->setEncoding($encoding); + $recognitionConfig->setSampleRateHertz($sampleRateHertz); + $recognitionConfig->setLanguageCode($languageCode); + return $recognitionConfig; + } + + /** + * @dataProvider createRecognitionAudioDataProvider + */ + public function testCreateRecognitionAudio($audio, $expectedRequestMessage) + { + $actualRequestMessage = $this->client->createRecognitionAudio($audio); + $this->assertEquals($expectedRequestMessage->serializeToJsonString(), $actualRequestMessage->serializeToJsonString()); + } + + public function createRecognitionAudioDataProvider() + { + $uri = 'gs://my-bucket/my-audio.flac'; + $data = 'abcdefgh'; + $resourceData = 'zyxwvuts'; + $resource = $this->createResource($resourceData); + $recognitionAudio = (new RecognitionAudio()) + ->setContent('directRequestData'); + return [ + [$uri, (new RecognitionAudio()) + ->setUri($uri)], + [$data, (new RecognitionAudio()) + ->setContent($data)], + [$resource, (new RecognitionAudio()) + ->setContent($resourceData)], + [$recognitionAudio, $recognitionAudio] + ]; + } + + /** + * @dataProvider recognizeAudioStreamData + */ + public function testRecognizeAudioStream($audio, $expectedContent) + { + $recognitionConfig = $this->createRecognitionConfig(); + $config = new StreamingRecognitionConfig(); + $config->setConfig($recognitionConfig); + + $expectedResponseStream = [ + new StreamingRecognizeResponse(), + new StreamingRecognizeResponse(), + ]; + + $mockBidiStreamingCall = new MockBidiStreamingCall($expectedResponseStream); + + $this->transport->startBidiStreamingCall(Argument::allOf( + Argument::type(Call::class), + Argument::which('getMethod', 'google.cloud.speech.v1.Speech/StreamingRecognize') + ), + Argument::type('array') + ) + ->shouldBeCalledTimes(1) + ->willReturn(new BidiStream($mockBidiStreamingCall)); + + $responseStream = $this->client->recognizeAudioStream($config, $audio); + + $this->assertSame($expectedResponseStream, iterator_to_array($responseStream)); + + /** @var StreamingRecognizeRequest[] $receivedCalls */ + $receivedCalls = $mockBidiStreamingCall->popReceivedCalls(); + + $expectedConfigMessage = new StreamingRecognizeRequest(); + $expectedConfigMessage->setStreamingConfig($config); + + // Expect one extra call, for the config message + $this->assertSame(count($expectedContent) + 1, count($receivedCalls)); + $initialReceivedCall = array_shift($receivedCalls); + $this->assertEquals($expectedConfigMessage, $initialReceivedCall); + for ($i = 0; $i < count($expectedContent); $i++) { + $this->assertSame($expectedContent[$i], $receivedCalls[$i]->getAudioContent()); + } + } + + public function recognizeAudioStreamData() + { + $data = 'abcdefgh'; + $iterableData = ['abcd', 'efgh']; + $resourceData = 'zyxwvuts'; + $streamingData = '12345678'; + $resource = $this->createResource($resourceData); + $streamingRecognizeRequest = new StreamingRecognizeRequest(); + $streamingRecognizeRequest->setAudioContent($streamingData); + return [ + [$data, [$data]], + [$iterableData, $iterableData], + [[$streamingRecognizeRequest], [$streamingData]], + [$resource, [$resourceData]] + ]; + } + + private function createResource($data) + { + $resource = fopen('php://memory','r+'); + fwrite($resource, $data); + rewind($resource); + return $resource; + } +} diff --git a/Speech/tests/Unit/V1beta1/SpeechClientTest.php b/Speech/tests/Unit/V1beta1/SpeechClientTest.php new file mode 100644 index 000000000000..90ede0c9da52 --- /dev/null +++ b/Speech/tests/Unit/V1beta1/SpeechClientTest.php @@ -0,0 +1,160 @@ +transport = $this->prophesize(TransportInterface::class); + $this->client = new SpeechClient([ + 'transport' => $this->transport->reveal(), + ]); + } + + private function createRecognitionConfig() + { + $encoding = RecognitionConfig_AudioEncoding::FLAC; + $sampleRateHertz = 44100; + $languageCode = 'en-US'; + $recognitionConfig = new RecognitionConfig(); + $recognitionConfig->setEncoding($encoding); + $recognitionConfig->setSampleRate($sampleRateHertz); + $recognitionConfig->setLanguageCode($languageCode); + return $recognitionConfig; + } + + /** + * @dataProvider createRecognitionAudioDataProvider + */ + public function testCreateRecognitionAudio($audio, $expectedRequestMessage) + { + $actualRequestMessage = $this->client->createRecognitionAudio($audio); + $this->assertEquals($expectedRequestMessage->serializeToJsonString(), $actualRequestMessage->serializeToJsonString()); + } + + public function createRecognitionAudioDataProvider() + { + $uri = 'gs://my-bucket/my-audio.flac'; + $data = 'abcdefgh'; + $resourceData = 'zyxwvuts'; + $resource = $this->createResource($resourceData); + $recognitionAudio = (new RecognitionAudio()) + ->setContent('directRequestData'); + return [ + [$uri, (new RecognitionAudio()) + ->setUri($uri)], + [$data, (new RecognitionAudio()) + ->setContent($data)], + [$resource, (new RecognitionAudio()) + ->setContent($resourceData)], + [$recognitionAudio, $recognitionAudio] + ]; + } + + /** + * @dataProvider recognizeAudioStreamData + */ + public function testRecognizeAudioStream($audio, $expectedContent) + { + $recognitionConfig = $this->createRecognitionConfig(); + $config = new StreamingRecognitionConfig(); + $config->setConfig($recognitionConfig); + + $expectedResponseStream = [ + new StreamingRecognizeResponse(), + new StreamingRecognizeResponse(), + ]; + + $mockBidiStreamingCall = new MockBidiStreamingCall($expectedResponseStream); + + $this->transport->startBidiStreamingCall(Argument::allOf( + Argument::type(Call::class), + Argument::which('getMethod', 'google.cloud.speech.v1beta1.Speech/StreamingRecognize') + ), + Argument::type('array') + ) + ->shouldBeCalledTimes(1) + ->willReturn(new BidiStream($mockBidiStreamingCall)); + + $responseStream = $this->client->recognizeAudioStream($config, $audio); + + $this->assertSame($expectedResponseStream, iterator_to_array($responseStream)); + + /** @var StreamingRecognizeRequest[] $receivedCalls */ + $receivedCalls = $mockBidiStreamingCall->popReceivedCalls(); + + $expectedConfigMessage = new StreamingRecognizeRequest(); + $expectedConfigMessage->setStreamingConfig($config); + + // Expect one extra call, for the config message + $this->assertSame(count($expectedContent) + 1, count($receivedCalls)); + $initialReceivedCall = array_shift($receivedCalls); + $this->assertEquals($expectedConfigMessage, $initialReceivedCall); + for ($i = 0; $i < count($expectedContent); $i++) { + $this->assertSame($expectedContent[$i], $receivedCalls[$i]->getAudioContent()); + } + } + + public function recognizeAudioStreamData() + { + $data = 'abcdefgh'; + $iterableData = ['abcd', 'efgh']; + $resourceData = 'zyxwvuts'; + $streamingData = '12345678'; + $resource = $this->createResource($resourceData); + $streamingRecognizeRequest = new StreamingRecognizeRequest(); + $streamingRecognizeRequest->setAudioContent($streamingData); + return [ + [$data, [$data]], + [$iterableData, $iterableData], + [[$streamingRecognizeRequest], [$streamingData]], + [$resource, [$resourceData]] + ]; + } + + private function createResource($data) + { + $resource = fopen('php://memory','r+'); + fwrite($resource, $data); + rewind($resource); + return $resource; + } +}