diff --git a/packages/google-cloud-speech/google/cloud/speech/__init__.py b/packages/google-cloud-speech/google/cloud/speech/__init__.py index f1fc9250f3c9..dd054830659a 100644 --- a/packages/google-cloud-speech/google/cloud/speech/__init__.py +++ b/packages/google-cloud-speech/google/cloud/speech/__init__.py @@ -17,4 +17,5 @@ from google.cloud.speech.client import Client from google.cloud.speech.connection import Connection from google.cloud.speech.encoding import Encoding +from google.cloud.speech.operation import Operation from google.cloud.speech.transcript import Transcript diff --git a/packages/google-cloud-speech/google/cloud/speech/client.py b/packages/google-cloud-speech/google/cloud/speech/client.py index e254c56e4fa7..8355e31f8aee 100644 --- a/packages/google-cloud-speech/google/cloud/speech/client.py +++ b/packages/google-cloud-speech/google/cloud/speech/client.py @@ -17,10 +17,11 @@ from base64 import b64encode import os -from google.cloud.client import Client as BaseClient from google.cloud._helpers import _to_bytes from google.cloud._helpers import _bytes_to_unicode +from google.cloud.client import Client as BaseClient from google.cloud.environment_vars import DISABLE_GRPC + from google.cloud.speech.connection import Connection from google.cloud.speech.encoding import Encoding from google.cloud.speech.operation import Operation @@ -111,8 +112,8 @@ def async_recognize(self, sample, language_code=None, and phrases. This can also be used to add new words to the vocabulary of the recognizer. - :rtype: `~google.cloud.speech.operation.Operation` - :returns: ``Operation`` for asynchronous request to Google Speech API. + :rtype: :class:`~google.cloud.speech.operation.Operation` + :returns: Operation for asynchronous request to Google Speech API. """ if sample.encoding is not Encoding.LINEAR16: raise ValueError('Only LINEAR16 encoding is supported by ' @@ -279,15 +280,17 @@ def async_recognize(self, sample, language_code=None, and phrases. This can also be used to add new words to the vocabulary of the recognizer. - :rtype: `~google.cloud.speech.operation.Operation` - :returns: ``Operation`` for asynchronous request to Google Speech API. + :rtype: :class:`~google.cloud.speech.operation.Operation` + :returns: Operation for asynchronous request to Google Speech API. """ data = _build_request_data(sample, language_code, max_alternatives, profanity_filter, speech_context) api_response = self._connection.api_request( method='POST', path='speech:asyncrecognize', data=data) - return Operation.from_api_repr(self, api_response) + operation = Operation.from_dict(api_response, self._client) + operation.caller_metadata['request_type'] = 'AsyncRecognize' + return operation def sync_recognize(self, sample, language_code=None, max_alternatives=None, profanity_filter=None, speech_context=None): diff --git a/packages/google-cloud-speech/google/cloud/speech/encoding.py b/packages/google-cloud-speech/google/cloud/speech/encoding.py index 4fdaa3367834..d8812ce5f06d 100644 --- a/packages/google-cloud-speech/google/cloud/speech/encoding.py +++ b/packages/google-cloud-speech/google/cloud/speech/encoding.py @@ -1,4 +1,4 @@ -# Copyright 2016 Google Inc. All rights reserved. +# Copyright 2016 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/packages/google-cloud-speech/google/cloud/speech/metadata.py b/packages/google-cloud-speech/google/cloud/speech/metadata.py deleted file mode 100644 index 2cbc285c16c6..000000000000 --- a/packages/google-cloud-speech/google/cloud/speech/metadata.py +++ /dev/null @@ -1,78 +0,0 @@ -# Copyright 2016 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Metadata representation from Google Speech API""" - -from google.cloud._helpers import _rfc3339_to_datetime - - -class Metadata(object): - """Representation of metadata from a Google Speech API Operation. - - :type last_update: datetime - :param last_update: When the Speech operation was last updated. - - :type start_time: datetime - :param start_time: When the Speech operation was started. - - :type progress_percent: int - :param progress_percent: Percentage of operation that has been completed. - """ - def __init__(self, last_update, start_time, progress_percent): - self._last_update = last_update - self._start_time = start_time - self._progress_percent = progress_percent - - @classmethod - def from_api_repr(cls, response): - """Factory: construct representation of operation metadata. - - :type response: dict - :param response: Dictionary containing operation metadata. - - :rtype: :class:`~google.cloud.speech.metadata.Metadata` - :returns: Instance of operation Metadata. - """ - last_update = _rfc3339_to_datetime(response['lastUpdateTime']) - start_time = _rfc3339_to_datetime(response['startTime']) - progress_percent = response.get('progressPercent') - - return cls(last_update, start_time, progress_percent) - - @property - def last_update(self): - """Last time operation was updated. - - :rtype: datetime - :returns: Datetime when operation was last updated. - """ - return self._last_update - - @property - def start_time(self): - """Start time of operation. - - :rtype: datetime - :returns: Datetime when operation was started. - """ - return self._start_time - - @property - def progress_percent(self): - """Progress percentage completed of operation. - - :rtype: int - :returns: Percentage of operation completed. - """ - return self._progress_percent diff --git a/packages/google-cloud-speech/google/cloud/speech/operation.py b/packages/google-cloud-speech/google/cloud/speech/operation.py index cb3ca4c61275..7a91366bf5e0 100644 --- a/packages/google-cloud-speech/google/cloud/speech/operation.py +++ b/packages/google-cloud-speech/google/cloud/speech/operation.py @@ -1,4 +1,4 @@ -# Copyright 2016 Google Inc. All rights reserved. +# Copyright 2016 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,120 +14,55 @@ """Long running operation representation for Google Speech API""" -from google.cloud.speech.metadata import Metadata -from google.cloud.speech.transcript import Transcript +from google.cloud.grpc.speech.v1beta1 import cloud_speech_pb2 + from google.cloud import operation +from google.cloud.speech.transcript import Transcript -class Operation(operation.Operation): - """Representation of a Google API Long-Running Operation. +operation.register_type(cloud_speech_pb2.AsyncRecognizeMetadata) +operation.register_type(cloud_speech_pb2.AsyncRecognizeResponse) - :type client: :class:`~google.cloud.speech.client.Client` - :param client: Instance of speech client. - :type name: int - :param name: ID assigned to an operation. +class Operation(operation.Operation): + """Custom Long-Running Operation for Google Speech API. - :type complete: bool - :param complete: True if operation is complete, else False. + :type name: str + :param name: The fully-qualified path naming the operation. - :type metadata: :class:`~google.cloud.speech.metadata.Metadata` - :param metadata: Instance of ``Metadata`` with operation information. + :type client: :class:`~google.cloud.speech.client.Client` + :param client: Client that created the current operation. - :type results: dict - :param results: Dictionary with transcript and score of operation. + :type caller_metadata: dict + :param caller_metadata: caller-assigned metadata about the operation """ - def __init__(self, client, name, complete=False, metadata=None, - results=None): - self.client = client - self.name = name - self._complete = complete - self._metadata = metadata - self._results = results - - @classmethod - def from_api_repr(cls, client, response): - """Factory: construct an instance from Google Speech API. - - :type client: :class:`~google.cloud.speech.client.Client` - :param client: Instance of speech client. - - :type response: dict - :param response: Dictionary response from Google Speech Operations API. - - :rtype: :class:`Operation` - :returns: Instance of `~google.cloud.speech.operations.Operation`. - """ - name = response['name'] - complete = response.get('done', False) - operation_instance = cls(client, name, complete) - operation_instance._update(response) - return operation_instance + results = None + """List of transcriptions from the speech-to-text process.""" - @property - def complete(self): - """Completion state of the `Operation`. + def _update_state(self, operation_pb): + """Update the state of the current object based on operation. - :rtype: bool - :returns: True if already completed, else false. - """ - return self._complete + This mostly does what the base class does, but all populates + results. - @property - def metadata(self): - """Metadata of operation. + :type operation_pb: + :class:`~google.longrunning.operations_pb2.Operation` + :param operation_pb: Protobuf to be parsed. - :rtype: :class:`~google.cloud.speech.metadata.Metadata` - :returns: Instance of ``Metadata``. + :raises ValueError: If there is more than one entry in ``results``. """ - return self._metadata + super(Operation, self)._update_state(operation_pb) - @property - def results(self): - """Results dictionary with transcript information. + result_type = operation_pb.WhichOneof('result') + if result_type != 'response': + return - :rtype: dict - :returns: Dictionary with transcript and confidence score. - """ - return self._results - - def poll(self): - """Check if the operation has finished. - - :rtype: bool - :returns: A boolean indicating if the current operation has completed. - :raises: :class:`ValueError ` if the operation - has already completed. - """ - if self.complete: - raise ValueError('The operation has completed.') + pb_results = self.response.results + if len(pb_results) != 1: + raise ValueError('Expected exactly one result, found:', + pb_results) - path = 'operations/%s' % (self.name,) - api_response = self.client.connection.api_request(method='GET', - path=path) - self._update(api_response) - return self.complete - - def _update(self, response): - """Update Operation instance with latest data from Speech API. - - .. _speech_operations: https://cloud.google.com/speech/reference/\ - rest/v1beta1/operations - - :type response: dict - :param response: Response from Speech API Operations endpoint. - See: `speech_operations`_. - """ - metadata = response.get('metadata', None) - raw_results = response.get('response', {}).get('results', None) - results = [] - if raw_results: - for result in raw_results: - for alternative in result['alternatives']: - results.append(Transcript.from_api_repr(alternative)) - if metadata: - self._metadata = Metadata.from_api_repr(metadata) - - self._results = results - self._complete = response.get('done', False) + result = pb_results[0] + self.results = [Transcript.from_pb(alternative) + for alternative in result.alternatives] diff --git a/packages/google-cloud-speech/google/cloud/speech/transcript.py b/packages/google-cloud-speech/google/cloud/speech/transcript.py index 0360cf7eefee..0ac22e6d296f 100644 --- a/packages/google-cloud-speech/google/cloud/speech/transcript.py +++ b/packages/google-cloud-speech/google/cloud/speech/transcript.py @@ -52,7 +52,10 @@ def from_pb(cls, transcript): :rtype: :class:`Transcript` :returns: Instance of ``Transcript``. """ - return cls(transcript.transcript, transcript.confidence) + confidence = transcript.confidence + if confidence == 0.0: # In the protobof 0.0 means unset. + confidence = None + return cls(transcript.transcript, confidence) @property def transcript(self): diff --git a/packages/google-cloud-speech/unit_tests/test_client.py b/packages/google-cloud-speech/unit_tests/test_client.py index 929999fc7c38..0f9b8e1ed492 100644 --- a/packages/google-cloud-speech/unit_tests/test_client.py +++ b/packages/google-cloud-speech/unit_tests/test_client.py @@ -262,6 +262,9 @@ def test_async_recognize_no_gax(self): sample_rate=self.SAMPLE_RATE) operation = client.async_recognize(sample) self.assertIsInstance(operation, Operation) + self.assertIs(operation.client, client) + self.assertEqual(operation.caller_metadata, + {'request_type': 'AsyncRecognize'}) self.assertFalse(operation.complete) self.assertIsNone(operation.metadata) diff --git a/packages/google-cloud-speech/unit_tests/test_metadata.py b/packages/google-cloud-speech/unit_tests/test_metadata.py deleted file mode 100644 index 8e1dcd03e733..000000000000 --- a/packages/google-cloud-speech/unit_tests/test_metadata.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright 2016 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import unittest - - -class TestMetadata(unittest.TestCase): - OPERATION_ID = 123456789 - - def _getTargetClass(self): - from google.cloud.speech.metadata import Metadata - return Metadata - - def _makeOne(self, *args, **kwargs): - return self._getTargetClass()(*args, **kwargs) - - def test_ctor(self): - last_update = 'last_update' - start_time = 'start_time' - progress_percent = 23 - metadata = self._makeOne(last_update, start_time, progress_percent) - self.assertEqual('last_update', metadata.last_update) - self.assertEqual('start_time', metadata.start_time) - self.assertEqual(23, metadata.progress_percent) - - def test_from_api_repr(self): - import datetime - from google.cloud._helpers import _rfc3339_to_datetime - from unit_tests._fixtures import OPERATION_INCOMPLETE_RESPONSE as DATA - METADATA = DATA['metadata'] - - start_time = _rfc3339_to_datetime(METADATA['startTime']) - last_update = _rfc3339_to_datetime(METADATA['lastUpdateTime']) - metadata = self._getTargetClass().from_api_repr(METADATA) - self.assertIsInstance(metadata.last_update, datetime.datetime) - self.assertEqual(last_update, metadata.last_update) - self.assertIsInstance(metadata.start_time, datetime.datetime) - self.assertEqual(start_time, metadata.start_time) - self.assertEqual(27, metadata.progress_percent) diff --git a/packages/google-cloud-speech/unit_tests/test_operation.py b/packages/google-cloud-speech/unit_tests/test_operation.py index 6bf824a9d3d6..f067b47838e9 100644 --- a/packages/google-cloud-speech/unit_tests/test_operation.py +++ b/packages/google-cloud-speech/unit_tests/test_operation.py @@ -15,7 +15,7 @@ import unittest -class OperationTests(unittest.TestCase): +class TestOperation(unittest.TestCase): OPERATION_NAME = '123456789' @@ -26,97 +26,89 @@ def _getTargetClass(self): def _makeOne(self, *args, **kwargs): return self._getTargetClass()(*args, **kwargs) - def test_ctor_defaults(self): - client = _Client() - operation = self._makeOne(client, self.OPERATION_NAME) - self.assertEqual(operation.name, '123456789') - self.assertFalse(operation.complete) + def test_constructor(self): + client = object() + operation = self._makeOne( + self.OPERATION_NAME, client) + self.assertEqual(operation.name, self.OPERATION_NAME) + self.assertIs(operation.client, client) + self.assertIsNone(operation.target) + self.assertIsNone(operation.response) + self.assertIsNone(operation.results) + self.assertIsNone(operation.error) self.assertIsNone(operation.metadata) + self.assertEqual(operation.caller_metadata, {}) + self.assertTrue(operation._from_grpc) + + @staticmethod + def _make_result(transcript, confidence): + from google.cloud.grpc.speech.v1beta1 import cloud_speech_pb2 + + return cloud_speech_pb2.SpeechRecognitionResult( + alternatives=[ + cloud_speech_pb2.SpeechRecognitionAlternative( + transcript=transcript, + confidence=confidence, + ), + ], + ) + + def _make_operation_pb(self, *results): + from google.cloud.grpc.speech.v1beta1 import cloud_speech_pb2 + from google.longrunning import operations_pb2 + from google.protobuf.any_pb2 import Any + + any_pb = None + if results: + result_pb = cloud_speech_pb2.AsyncRecognizeResponse( + results=results, + ) + type_url = 'type.googleapis.com/%s' % ( + result_pb.DESCRIPTOR.full_name,) + any_pb = Any(type_url=type_url, + value=result_pb.SerializeToString()) + + return operations_pb2.Operation( + name=self.OPERATION_NAME, + response=any_pb) + + def test__update_state_no_response(self): + client = object() + operation = self._makeOne( + self.OPERATION_NAME, client) + + operation_pb = self._make_operation_pb() + operation._update_state(operation_pb) + self.assertIsNone(operation.response) self.assertIsNone(operation.results) - def test_from_api_repr(self): - from unit_tests._fixtures import OPERATION_COMPLETE_RESPONSE + def test__update_state_with_response(self): from google.cloud.speech.transcript import Transcript - from google.cloud.speech.metadata import Metadata - RESPONSE = OPERATION_COMPLETE_RESPONSE - client = _Client() - operation = self._getTargetClass().from_api_repr(client, RESPONSE) + client = object() + operation = self._makeOne( + self.OPERATION_NAME, client) - self.assertEqual('123456789', operation.name) - self.assertTrue(operation.complete) + text = 'hi mom' + confidence = 0.75 + result = self._make_result(text, confidence) + operation_pb = self._make_operation_pb(result) + operation._update_state(operation_pb) + self.assertIsNotNone(operation.response) self.assertEqual(len(operation.results), 1) - self.assertIsInstance(operation.results[0], Transcript) - self.assertEqual(operation.results[0].transcript, - 'how old is the Brooklyn Bridge') - self.assertEqual(operation.results[0].confidence, - 0.98267895) - self.assertTrue(operation.complete) - self.assertIsInstance(operation.metadata, Metadata) - self.assertEqual(operation.metadata.progress_percent, 100) - - def test_update_response(self): - from unit_tests._fixtures import ASYNC_RECOGNIZE_RESPONSE - from unit_tests._fixtures import OPERATION_COMPLETE_RESPONSE - RESPONSE = ASYNC_RECOGNIZE_RESPONSE - - client = _Client() - operation = self._getTargetClass().from_api_repr(client, RESPONSE) - self.assertEqual(operation.name, '123456789') - operation._update(OPERATION_COMPLETE_RESPONSE) - self.assertTrue(operation.complete) - - def test_poll(self): - from google.cloud.speech.operation import Metadata - from unit_tests._fixtures import ASYNC_RECOGNIZE_RESPONSE - from unit_tests._fixtures import OPERATION_COMPLETE_RESPONSE - RESPONSE = ASYNC_RECOGNIZE_RESPONSE - client = _Client() - connection = _Connection(OPERATION_COMPLETE_RESPONSE) - client.connection = connection - - operation = self._getTargetClass().from_api_repr(client, RESPONSE) - self.assertFalse(operation.complete) - operation.poll() - self.assertTrue(operation.complete) - self.assertIsInstance(operation.metadata, Metadata) - self.assertEqual(operation.metadata.progress_percent, 100) - requested = client.connection._requested - self.assertEqual(requested[0]['method'], 'GET') - self.assertEqual(requested[0]['path'], - 'operations/%s' % (operation.name,)) - - def test_poll_complete(self): - from unit_tests._fixtures import OPERATION_COMPLETE_RESPONSE - from unit_tests._fixtures import OPERATION_INCOMPLETE_RESPONSE - RESPONSE = OPERATION_INCOMPLETE_RESPONSE - - client = _Client() - connection = _Connection(OPERATION_COMPLETE_RESPONSE) - client.connection = connection - operation = self._getTargetClass().from_api_repr(client, RESPONSE) - - self.assertFalse(operation.complete) - operation.poll() # Update the operation with complete data. - + transcript = operation.results[0] + self.assertIsInstance(transcript, Transcript) + self.assertEqual(transcript.transcript, text) + self.assertEqual(transcript.confidence, confidence) + + def test__update_state_bad_response(self): + client = object() + operation = self._makeOne( + self.OPERATION_NAME, client) + + result1 = self._make_result('is this ok?', 0.625) + result2 = self._make_result('ease is ok', None) + operation_pb = self._make_operation_pb(result1, result2) with self.assertRaises(ValueError): - operation.poll() - requested = client.connection._requested - self.assertEqual(requested[0]['method'], 'GET') - self.assertEqual(requested[0]['path'], - 'operations/%s' % (operation.name,)) - - -class _Connection(object): - def __init__(self, response=None): - self.response = response - self._requested = [] - - def api_request(self, method, path): - self._requested.append({'method': method, 'path': path}) - return self.response - - -class _Client(object): - connection = None + operation._update_state(operation_pb) diff --git a/packages/google-cloud-speech/unit_tests/test_transcript.py b/packages/google-cloud-speech/unit_tests/test_transcript.py index 6e15cf79e327..a049eac6e815 100644 --- a/packages/google-cloud-speech/unit_tests/test_transcript.py +++ b/packages/google-cloud-speech/unit_tests/test_transcript.py @@ -16,6 +16,7 @@ class TestTranscript(unittest.TestCase): + def _getTargetClass(self): from google.cloud.speech.transcript import Transcript return Transcript @@ -23,20 +24,42 @@ def _getTargetClass(self): def _makeOne(self, *args, **kwargs): return self._getTargetClass()(*args, **kwargs) - def test_ctor(self): - from unit_tests._fixtures import OPERATION_COMPLETE_RESPONSE as DATA - TRANSCRIPT_DATA = DATA['response']['results'][0]['alternatives'][0] - transcript = self._makeOne(TRANSCRIPT_DATA['transcript'], - TRANSCRIPT_DATA['confidence']) - self.assertEqual('how old is the Brooklyn Bridge', - transcript.transcript) - self.assertEqual(0.98267895, transcript.confidence) + def test_constructor(self): + text = 'hello goodbye upstairs' + confidence = 0.5546875 + transcript = self._makeOne(text, confidence) + self.assertEqual(transcript._transcript, text) + self.assertEqual(transcript._confidence, confidence) + + def test_transcript_property(self): + text = 'is this thing on?' + transcript = self._makeOne(text, None) + self.assertEqual(transcript.transcript, text) + + def test_confidence_property(self): + confidence = 0.412109375 + transcript = self._makeOne(None, confidence) + self.assertEqual(transcript.confidence, confidence) def test_from_api_repr_with_no_confidence(self): data = { - 'transcript': 'testing 1 2 3' + 'transcript': 'testing 1 2 3', } - transcript = self._getTargetClass().from_api_repr(data) + klass = self._getTargetClass() + transcript = klass.from_api_repr(data) self.assertEqual(transcript.transcript, data['transcript']) self.assertIsNone(transcript.confidence) + + def test_from_pb_with_no_confidence(self): + from google.cloud.grpc.speech.v1beta1 import cloud_speech_pb2 + + text = 'the double trouble' + pb_value = cloud_speech_pb2.SpeechRecognitionAlternative( + transcript=text) + self.assertEqual(pb_value.confidence, 0.0) + + klass = self._getTargetClass() + transcript = klass.from_pb(pb_value) + self.assertEqual(transcript.transcript, text) + self.assertIsNone(transcript.confidence)