Skip to content

Commit

Permalink
Cloud Speech Veneer does not raise for > 1 result. (#2962)
Browse files Browse the repository at this point in the history
* Cloud Speech Veneer does not raise for > 1 result.

This commit also:

  - adds `transcript` and `confidence` convenience methods
    on result objects
  - updates unit tests appropriately
  - moves from google.cloud.grpc.* to google.cloud.proto.*
  - updates dependencies accordingly

* Fix linting error, and make imports follow PEP8.

* Fix two more linting issues.

* Address @daspecster suggestions.

Also finally fix all linting issues.

* Update speech usage RST file.

* Swap back to a list return, fixes coverage.

* Final lint change for @daspecster.

* Move imports to one per two lines.
  • Loading branch information
lukesneeringer authored and daspecster committed Jan 25, 2017
1 parent 9c416a8 commit 81183fb
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 78 deletions.
38 changes: 17 additions & 21 deletions packages/google-cloud-speech/google/cloud/speech/_gax.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,23 @@


from google.cloud.gapic.speech.v1beta1.speech_client import SpeechClient
from google.cloud.grpc.speech.v1beta1.cloud_speech_pb2 import RecognitionAudio
from google.cloud.grpc.speech.v1beta1.cloud_speech_pb2 import RecognitionConfig
from google.cloud.grpc.speech.v1beta1.cloud_speech_pb2 import SpeechContext
from google.cloud.grpc.speech.v1beta1.cloud_speech_pb2 import (
from google.cloud.proto.speech.v1beta1.cloud_speech_pb2 import RecognitionAudio
from google.cloud.proto.speech.v1beta1.cloud_speech_pb2 import (
RecognitionConfig)
from google.cloud.proto.speech.v1beta1.cloud_speech_pb2 import (
SpeechContext)
from google.cloud.proto.speech.v1beta1.cloud_speech_pb2 import (
StreamingRecognitionConfig)
from google.cloud.grpc.speech.v1beta1.cloud_speech_pb2 import (
from google.cloud.proto.speech.v1beta1.cloud_speech_pb2 import (
StreamingRecognizeRequest)
from google.longrunning import operations_grpc

from google.cloud._helpers import make_secure_channel
from google.cloud._helpers import make_secure_stub
from google.cloud._http import DEFAULT_USER_AGENT

from google.cloud.speech.alternative import Alternative
from google.cloud.speech.operation import Operation
from google.cloud.speech.result import Result

OPERATIONS_API_HOST = 'speech.googleapis.com'

Expand Down Expand Up @@ -235,15 +237,9 @@ def sync_recognize(self, sample, language_code=None, max_alternatives=None,
words to the vocabulary of the recognizer.
:rtype: list
:returns: A list of dictionaries. One dict for each alternative. Each
dictionary typically contains two keys (though not
all will be present in all cases)
:returns: List of :class:`google.cloud.speech.result.Result` objects.
* ``transcript``: The detected text from the audio recording.
* ``confidence``: The confidence in language detection, float
between 0 and 1.
:raises: ValueError if more than one result is returned or no results.
:raises: ValueError if there are no results.
"""
config = RecognitionConfig(
encoding=sample.encoding, sample_rate=sample.sample_rate,
Expand All @@ -254,13 +250,13 @@ def sync_recognize(self, sample, language_code=None, max_alternatives=None,
uri=sample.source_uri)
api = self._gapic_api
api_response = api.sync_recognize(config=config, audio=audio)
if len(api_response.results) == 1:
results = api_response.results.pop()
alternatives = results.alternatives
return [Alternative.from_pb(alternative)
for alternative in alternatives]
else:
raise ValueError('More than one result or none returned from API.')

# Sanity check: If we got no results back, raise an error.
if len(api_response.results) == 0:
raise ValueError('No results returned from the Speech API.')

# Iterate over any results that came back.
return [Result.from_pb(result) for result in api_response.results]


def _stream_requests(sample, language_code=None, max_alternatives=None,
Expand Down
6 changes: 5 additions & 1 deletion packages/google-cloud-speech/google/cloud/speech/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,11 @@ class Client(BaseClient):
def __init__(self, credentials=None, http=None, use_gax=None):
super(Client, self).__init__(credentials=credentials, http=http)
self._connection = Connection(
credentials=self._credentials, http=self._http)
credentials=self._credentials,
http=self._http,
)

# Save on the actual client class whether we use GAX or not.
if use_gax is None:
self._use_gax = _USE_GAX
else:
Expand Down
18 changes: 10 additions & 8 deletions packages/google-cloud-speech/google/cloud/speech/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@

"""Long running operation representation for Google Speech API"""

from google.cloud.grpc.speech.v1beta1 import cloud_speech_pb2
from google.cloud.proto.speech.v1beta1 import cloud_speech_pb2

from google.cloud import operation
from google.cloud.speech.alternative import Alternative
from google.cloud.speech.result import Result


operation.register_type(cloud_speech_pb2.AsyncRecognizeMetadata)
Expand Down Expand Up @@ -58,11 +58,13 @@ def _update_state(self, operation_pb):
if result_type != 'response':
return

# Retrieve the results.
# If there were no results at all, raise an exception.
pb_results = self.response.results
if len(pb_results) != 1:
raise ValueError('Expected exactly one result, found:',
pb_results)
if len(pb_results) == 0:
raise ValueError('Speech API returned no results.')

result = pb_results[0]
self.results = [Alternative.from_pb(alternative)
for alternative in result.alternatives]
# Save the results to the Operation object.
self.results = []
for pb_result in pb_results:
self.results.append(Result.from_pb(pb_result))
69 changes: 67 additions & 2 deletions packages/google-cloud-speech/google/cloud/speech/result.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,53 @@
from google.cloud.speech.alternative import Alternative


class Result(object):
"""Speech recognition result representation.
This is the object that comes back on sync or async requests
(but not streaming requests).
:type alternatives: list
:param alternatives: List of
:class:`~google.cloud.speech.alternative.Alternative`.
"""
def __init__(self, alternatives):
self.alternatives = alternatives

@classmethod
def from_pb(cls, result):
"""Factory: construct instance of ``SpeechRecognitionResult``.
:type result: :class:`~google.cloud.grpc.speech.v1beta1\
.cloud_speech_pb2.StreamingRecognizeResult`
:param result: Instance of ``StreamingRecognizeResult`` protobuf.
:rtype: :class:`~google.cloud.speech.result.SpeechRecognitionResult`
:returns: Instance of ``SpeechRecognitionResult``.
"""
alternatives = [Alternative.from_pb(result) for result
in result.alternatives]
return cls(alternatives=alternatives)

@property
def confidence(self):
"""Return the confidence for the most probable alternative.
:rtype: float
:returns: Confidence value, between 0 and 1.
"""
return self.alternatives[0].confidence

@property
def transcript(self):
"""Return the transcript for the most probable alternative.
:rtype: str
:returns: Speech transcript.
"""
return self.alternatives[0].transcript


class StreamingSpeechResult(object):
"""Streaming speech result representation.
Expand Down Expand Up @@ -46,9 +93,27 @@ def from_pb(cls, response):
:rtype: :class:`~google.cloud.speech.result.StreamingSpeechResult`
:returns: Instance of ``StreamingSpeechResult``.
"""
alternatives = [Alternative.from_pb(alternative)
for alternative in response.alternatives]
alternatives = [Alternative.from_pb(result) for result
in response.alternatives]
is_final = response.is_final
stability = response.stability
return cls(alternatives=alternatives, is_final=is_final,
stability=stability)

@property
def confidence(self):
"""Return the confidence for the most probable alternative.
:rtype: float
:returns: Confidence value, between 0 and 1.
"""
return self.alternatives[0].confidence

@property
def transcript(self):
"""Return the transcript for the most probable alternative.
:rtype: str
:returns: Speech transcript.
"""
return self.alternatives[0].transcript
2 changes: 1 addition & 1 deletion packages/google-cloud-speech/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
REQUIREMENTS = [
'google-cloud-core >= 0.22.1, < 0.23dev',
'grpcio >= 1.0.2, < 2.0dev',
'gapic-google-cloud-speech-v1beta1 >= 0.14.0, < 0.15dev',
'gapic-google-cloud-speech-v1beta1 >= 0.15.0, < 0.16dev',
]

setup(
Expand Down
15 changes: 4 additions & 11 deletions packages/google-cloud-speech/unit_tests/test__gax.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,8 @@ def _call_fut(self, sample, language_code, max_alternatives,
def test_ctor(self):
from google.cloud import speech
from google.cloud.speech.sample import Sample
from google.cloud.grpc.speech.v1beta1.cloud_speech_pb2 import (
SpeechContext)
from google.cloud.grpc.speech.v1beta1.cloud_speech_pb2 import (
RecognitionConfig)
from google.cloud.grpc.speech.v1beta1.cloud_speech_pb2 import (
StreamingRecognitionConfig)
from google.cloud.grpc.speech.v1beta1.cloud_speech_pb2 import (
from google.cloud.proto.speech.v1beta1.cloud_speech_pb2 import (
RecognitionConfig, SpeechContext, StreamingRecognitionConfig,
StreamingRecognizeRequest)

sample = Sample(content=self.AUDIO_CONTENT,
Expand Down Expand Up @@ -103,10 +98,8 @@ def test_stream_requests(self):
from io import BytesIO
from google.cloud import speech
from google.cloud.speech.sample import Sample
from google.cloud.grpc.speech.v1beta1.cloud_speech_pb2 import (
StreamingRecognitionConfig)
from google.cloud.grpc.speech.v1beta1.cloud_speech_pb2 import (
StreamingRecognizeRequest)
from google.cloud.proto.speech.v1beta1.cloud_speech_pb2 import (
StreamingRecognitionConfig, StreamingRecognizeRequest)

sample = Sample(stream=BytesIO(self.AUDIO_CONTENT),
encoding=speech.Encoding.FLAC,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def test_from_api_repr_with_no_confidence(self):
self.assertIsNone(alternative.confidence)

def test_from_pb_with_no_confidence(self):
from google.cloud.grpc.speech.v1beta1 import cloud_speech_pb2
from google.cloud.proto.speech.v1beta1 import cloud_speech_pb2

text = 'the double trouble'
pb_value = cloud_speech_pb2.SpeechRecognitionAlternative(
Expand Down
37 changes: 22 additions & 15 deletions packages/google-cloud-speech/unit_tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def _make_credentials():


def _make_result(alternatives=()):
from google.cloud.grpc.speech.v1beta1 import cloud_speech_pb2
from google.cloud.proto.speech.v1beta1 import cloud_speech_pb2

return cloud_speech_pb2.SpeechRecognitionResult(
alternatives=[
Expand All @@ -37,7 +37,7 @@ def _make_result(alternatives=()):


def _make_streaming_result(alternatives=(), is_final=True, stability=1.0):
from google.cloud.grpc.speech.v1beta1 import cloud_speech_pb2
from google.cloud.proto.speech.v1beta1 import cloud_speech_pb2

return cloud_speech_pb2.StreamingRecognitionResult(
alternatives=[
Expand All @@ -52,7 +52,7 @@ def _make_streaming_result(alternatives=(), is_final=True, stability=1.0):


def _make_streaming_response(*results):
from google.cloud.grpc.speech.v1beta1 import cloud_speech_pb2
from google.cloud.proto.speech.v1beta1 import cloud_speech_pb2

response = cloud_speech_pb2.StreamingRecognizeResponse(
results=results,
Expand All @@ -61,7 +61,7 @@ def _make_streaming_response(*results):


def _make_sync_response(*results):
from google.cloud.grpc.speech.v1beta1 import cloud_speech_pb2
from google.cloud.proto.speech.v1beta1 import cloud_speech_pb2

response = cloud_speech_pb2.SyncRecognizeResponse(
results=results,
Expand Down Expand Up @@ -202,7 +202,7 @@ def test_sync_recognize_source_uri_without_optional_params_no_gax(self):
sample = client.sample(source_uri=self.AUDIO_SOURCE_URI,
encoding=encoding, sample_rate=self.SAMPLE_RATE)

response = sample.sync_recognize()
response = [i for i in sample.sync_recognize()]

self.assertEqual(len(client._connection._requested), 1)
req = client._connection._requested[0]
Expand Down Expand Up @@ -231,7 +231,7 @@ def test_sync_recognize_with_empty_results_no_gax(self):
sample_rate=self.SAMPLE_RATE)

with self.assertRaises(ValueError):
sample.sync_recognize()
next(sample.sync_recognize())

def test_sync_recognize_with_empty_results_gax(self):
from google.cloud._testing import _Monkey
Expand Down Expand Up @@ -274,7 +274,7 @@ def speech_api(channel=None):
sample_rate=self.SAMPLE_RATE)

with self.assertRaises(ValueError):
sample.sync_recognize()
next(sample.sync_recognize())

def test_sync_recognize_with_gax(self):
from google.cloud._testing import _Monkey
Expand Down Expand Up @@ -326,16 +326,19 @@ def speech_api(channel=None):
self.assertEqual(
channel_args, [(creds, _gax.DEFAULT_USER_AGENT, host)])

results = sample.sync_recognize()
results = [i for i in sample.sync_recognize()]

self.assertEqual(len(results), 2)
self.assertEqual(len(results), 1)
self.assertEqual(len(results[0].alternatives), 2)
self.assertEqual(results[0].transcript,
results[0].alternatives[0].transcript,
alternatives[0]['transcript'])
self.assertEqual(results[0].confidence,
results[0].alternatives[0].confidence,
alternatives[0]['confidence'])
self.assertEqual(results[1].transcript,
self.assertEqual(results[0].alternatives[1].transcript,
alternatives[1]['transcript'])
self.assertEqual(results[1].confidence,
self.assertEqual(results[0].alternatives[1].confidence,
alternatives[1]['confidence'])

def test_async_supported_encodings(self):
Expand Down Expand Up @@ -535,19 +538,23 @@ def speech_api(channel=None):
self.assertEqual(results[0].stability, 0.122435)
self.assertEqual(results[1].stability, 0.1432343)
self.assertFalse(results[1].is_final)
self.assertEqual(results[1].alternatives[0].transcript,
self.assertEqual(results[1].transcript,
results[1].alternatives[0].transcript,
alternatives[0]['transcript'])
self.assertEqual(results[1].alternatives[0].confidence,
self.assertEqual(results[1].confidence,
results[1].alternatives[0].confidence,
alternatives[0]['confidence'])
self.assertEqual(results[1].alternatives[1].transcript,
alternatives[1]['transcript'])
self.assertEqual(results[1].alternatives[1].confidence,
alternatives[1]['confidence'])
self.assertTrue(results[2].is_final)
self.assertEqual(results[2].stability, 0.9834534)
self.assertEqual(results[2].alternatives[0].transcript,
self.assertEqual(results[2].transcript,
results[2].alternatives[0].transcript,
alternatives[0]['transcript'])
self.assertEqual(results[2].alternatives[0].confidence,
self.assertEqual(results[2].confidence,
results[2].alternatives[0].confidence,
alternatives[0]['confidence'])

def test_stream_recognize(self):
Expand Down
Loading

0 comments on commit 81183fb

Please sign in to comment.