feat: Add new gcloud commands, API clients, and third-party libraries across various services.

This commit is contained in:
2026-01-01 20:26:35 +01:00
parent 5e23cbece0
commit a19e592eb7
25221 changed files with 8324611 additions and 0 deletions

View File

@@ -0,0 +1,70 @@
# -*- coding: utf-8 -*- #
# Copyright 2017 Google LLC. All Rights Reserved.
#
# 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.
"""Flags for gcloud ml language commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import os
from googlecloudsdk.api_lib.storage import storage_util
from googlecloudsdk.core import exceptions
from googlecloudsdk.core.util import files
LANGUAGE_API = 'language'
class Error(exceptions.Error):
"""Exceptions for this module."""
class ContentFileError(Error):
"""Error if content file can't be read and isn't a GCS URL."""
class ContentError(Error):
"""Error if content is not given."""
def UpdateRequestWithInput(unused_ref, args, request):
"""The Python hook for yaml commands to inject content into the request."""
content = args.content
content_file = args.content_file
document = request.document
if content_file:
if content:
raise ValueError('Either a file or content must be provided for '
'analysis by the Natural Language API, not both.')
if os.path.isfile(content_file):
document.content = files.ReadFileContents(content_file)
elif storage_util.ObjectReference.IsStorageUrl(content_file):
document.gcsContentUri = content_file
else:
raise ContentFileError(
'Could not find --content-file [{}]. Content file must be a path '
'to a local file or a Google Cloud Storage URL (format: '
'`gs://bucket_name/object_name`)'.format(content_file))
elif content:
document.content = content
else:
# Either content_file or content are required. If content is an empty
# string, raise an error.
raise ContentError('The content provided is empty. Please provide '
'language content to analyze.')
return request

View File

@@ -0,0 +1,17 @@
project:
name: project
collection: ml.projects
attributes:
- &project
parameter_name: projectsId
attribute_name: project
help: The project name.
model:
name: model
collection: ml.projects.models
attributes:
- *project
- parameter_name: modelsId
attribute_name: model
help: The name of the model.

View File

@@ -0,0 +1,96 @@
# -*- coding: utf-8 -*- #
# Copyright 2024 Google LLC. All Rights Reserved.
#
# 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.
"""Validation functions for speech commands flags."""
import os
from googlecloudsdk.api_lib.storage import storage_util
from googlecloudsdk.calliope import exceptions
EXPLICIT_ENCODING_OPTIONS = ('LINEAR16', 'MULAW', 'ALAW')
ENCODING_OPTIONS = frozenset(EXPLICIT_ENCODING_OPTIONS) | {'AUTO'}
def ValidateSpeakerDiarization(args):
"""Validates speaker diarization flag input."""
if (
args.min_speaker_count is not None and args.max_speaker_count is not None
) and (args.min_speaker_count > args.max_speaker_count):
raise exceptions.InvalidArgumentException(
'--max-speaker-count',
'[--max-speaker-count] must be equal to or larger than'
' min-speaker-count.',
)
def ValidateAudioSource(args, batch=False):
"""Validates audio source flag input."""
if storage_util.ObjectReference.IsStorageUrl(args.audio):
return
if batch:
raise exceptions.InvalidArgumentException(
'--audio',
'Invalid audio source [{}]. The source must be a Google Cloud'
' Storage URL (such as gs://bucket/object).'.format(args.audio),
)
if not os.path.isfile(args.audio):
raise exceptions.InvalidArgumentException(
'--audio',
'Invalid audio source [{}]. The source must either be a local '
'path or a Google Cloud Storage URL '
'(such as gs://bucket/object).'.format(args.audio),
)
def ValidateDecodingConfig(args):
"""Validates encoding flag input."""
if args.encoding is None:
return
if args.encoding not in ENCODING_OPTIONS:
raise exceptions.InvalidArgumentException(
'--encoding',
'[--encoding] must be set to one of '
+ ', '.join(sorted(ENCODING_OPTIONS)),
)
if args.encoding == 'AUTO':
if args.sample_rate is not None or args.audio_channel_count is not None:
raise exceptions.InvalidArgumentException(
'--sample-rate'
if args.sample_rate is not None
else '--audio-channel-count',
'AUTO encoding does not support setting sample rate or audio'
' channel count.',
)
else:
if args.sample_rate is None:
raise exceptions.InvalidArgumentException(
'--sample-rate',
'[--sample-rate] must be specified when configuring explicit'
' encoding options '
+ ', '.join(sorted(EXPLICIT_ENCODING_OPTIONS))
+ '.',
)
if args.audio_channel_count is None:
raise exceptions.InvalidArgumentException(
'--audio-channel-count',
(
'[--audio-channel-count] must be specified when configuring'
' explicit encoding options '
+ ', '.join(sorted(EXPLICIT_ENCODING_OPTIONS))
),
)

View File

@@ -0,0 +1,486 @@
# -*- coding: utf-8 -*- #
# Copyright 2022 Google LLC. All Rights Reserved.
#
# 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.
"""Flags for speech commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.calliope import actions
from googlecloudsdk.calliope import arg_parsers
from googlecloudsdk.calliope import exceptions
from googlecloudsdk.command_lib.ml.speech import util
from googlecloudsdk.command_lib.util.apis import arg_utils
def GetEncodingTypeMapper(version):
messages = apis.GetMessagesModule(util.SPEECH_API, version)
return arg_utils.ChoiceEnumMapper(
'--encoding',
messages.RecognitionConfig.EncodingValueValuesEnum,
default='encoding-unspecified',
help_str='The type of encoding of the file. Required if the file format '
'is not WAV or FLAC.')
class RecognizeArgsToRequestMapper:
"""Utility class to map arguments to Recognize request."""
def __init__(self):
self._encoding_type_mapper = None
self._original_media_type_mapper = None
self._interaction_type_mapper = None
self._microphone_distance_type_mapper = None
self._device_type_mapper = None
def AddRecognizeArgsToParser(self, parser, api_version):
"""Add common, GA level flags for recognize commands."""
parser.add_argument(
'audio',
help='The location of the audio file to transcribe. '
'Must be a local path or a Google Cloud Storage URL '
'(in the format gs://bucket/object).')
language_args = parser.add_group(mutex=True, required=True)
language_args.add_argument(
'--language-code',
help='The language of the supplied audio as a BCP-47 '
'(https://www.rfc-editor.org/rfc/bcp/bcp47.txt) language tag. Example: '
'"en-US". See https://cloud.google.com/speech/docs/languages for a list '
'of the currently supported language codes.')
language_args.add_argument(
'--language',
action=actions.DeprecationAction(
'--language',
warn=('The `--language` flag is deprecated. '
'Use the `--language-code` flag instead.')),
hidden=True,
help='The language of the supplied audio as a BCP-47 '
'(https://www.rfc-editor.org/rfc/bcp/bcp47.txt) language tag. Example: '
'"en-US". See https://cloud.google.com/speech/docs/languages for a list '
'of the currently supported language codes.')
self._encoding_type_mapper = GetEncodingTypeMapper(api_version)
self._encoding_type_mapper.choice_arg.AddToParser(parser)
parser.add_argument(
'--sample-rate',
type=int,
required=False,
help='The sample rate in Hertz. For best results, set the sampling rate '
'of the audio source to 16000 Hz. If that\'s not possible, '
'use the native sample rate of the audio source '
'(instead of re-sampling).')
audio_channel_args = parser.add_group(
required=False, help='Audio channel settings.')
audio_channel_args.add_argument(
'--audio-channel-count',
type=int,
required=True,
help='The number of channels in the input audio data. Set this for '
'separate-channel-recognition. Valid values are: '
'1)LINEAR16 and FLAC are 1-8 '
'2)OGG_OPUS are 1-254 '
'3) MULAW, AMR, AMR_WB and SPEEX_WITH_HEADER_BYTE is only `1`.')
audio_channel_args.add_argument(
'--separate-channel-recognition',
action='store_true',
required=True,
help='Recognition result will contain a `channel_tag` field to state '
'which channel that result belongs to. If this is not true, only '
'the first channel will be recognized.')
parser.add_argument(
'--model',
choices={
'default': (
'audio that is not one of the specific audio models. '
'For example, long-form audio. '
'Ideally the audio is high-fidelity, recorded at a 16khz '
'or greater sampling rate.'
),
'command_and_search': (
'short queries such as voice commands or voice search.'
),
'latest_long': (
'Use this model for any kind of long form content such as media'
' or spontaneous speech and conversations. Consider using this'
' model in place of the video model, especially if the video'
' model is not available in your target language. You can also'
' use this in place of the default model.'
),
'latest_short': (
'Use this model for short utterances that are a few seconds in'
' length. It is useful for trying to capture commands or other'
' single shot directed speech use cases. Consider using this'
' model instead of the command and search model.'
),
'medical_conversation': (
'Best for audio that originated from a conversation between a '
'medical provider and patient.'
),
'medical_dictation': (
'Best for audio that originated from dictation notes by a'
' medical provider.'
),
'phone_call': (
'audio that originated from a phone call (typically recorded at'
' an 8khz sampling rate).'
),
'phone_call_enhanced': (
'audio that originated from a phone call (typically recorded at'
' an 8khz sampling rate). This is a premium model and can'
' produce better results but costs more than the standard rate.'
),
'telephony': (
'Improved version of the "phone_call" model, best for audio '
'that originated from a phone call, typically recorded at an '
'8kHz sampling rate.'
),
'telephony_short': (
'Dedicated version of the modern "telephony" model for short '
'or even single-word utterances for audio that originated from '
'a phone call, typically recorded at an 8kHz sampling rate.'
),
'video_enhanced': (
'audio that originated from video or includes multiple'
' speakers. Ideally the audio is recorded at a 16khz or greater'
' sampling rate. This is a premium model that costs more than'
' the standard rate.'
),
},
help=(
'Select the model best suited to your domain to get best results.'
' If you do not explicitly specify a model, Speech-to-Text will'
' auto-select a model based on your other specified parameters.'
' Some models are premium and cost more than standard models'
' (although you can reduce the price by opting into'
' https://cloud.google.com/speech-to-text/docs/data-logging)'
),
)
parser.add_argument(
'--max-alternatives',
type=int,
default=1,
help='Maximum number of recognition hypotheses to be returned. '
'The server may return fewer than max_alternatives. '
'Valid values are 0-30. A value of 0 or 1 will return a maximum '
'of one.')
parser.add_argument(
'--hints',
type=arg_parsers.ArgList(),
metavar='HINT',
default=[],
help='A list of strings containing word and phrase "hints" so that the '
'speech recognition is more likely to recognize them. This can be '
'used to improve the accuracy for specific words and phrases, '
'for example, if specific commands are typically spoken by '
'the user. This can also be used to add additional words to the '
'vocabulary of the recognizer. '
'See https://cloud.google.com/speech/limits#content.')
parser.add_argument(
'--include-word-time-offsets',
action='store_true',
default=False,
help='If True, the top result includes a list of words with the start '
'and end time offsets (timestamps) for those words. If False, '
'no word-level time offset information is returned.')
parser.add_argument(
'--filter-profanity',
action='store_true',
default=False,
help='If True, the server will attempt to filter out profanities, '
'replacing all but the initial character in each filtered word with '
'asterisks, e.g. ```f***```.')
parser.add_argument(
'--enable-automatic-punctuation',
action='store_true',
help='Adds punctuation to recognition result hypotheses.')
def MakeRecognitionConfig(self, args, messages):
"""Make RecognitionConfig message from given arguments."""
config = messages.RecognitionConfig(
languageCode=args.language_code
if args.language_code else args.language,
encoding=self._encoding_type_mapper.GetEnumForChoice(
args.encoding.replace('_', '-').lower()),
sampleRateHertz=args.sample_rate,
audioChannelCount=args.audio_channel_count,
maxAlternatives=args.max_alternatives,
enableWordTimeOffsets=args.include_word_time_offsets,
enableSeparateRecognitionPerChannel=args.separate_channel_recognition,
profanityFilter=args.filter_profanity,
speechContexts=[messages.SpeechContext(phrases=args.hints)])
if args.enable_automatic_punctuation:
config.enableAutomaticPunctuation = args.enable_automatic_punctuation
if args.model is not None:
if args.model in [
'default',
'command_and_search',
'phone_call',
'latest_long',
'latest_short',
'medical_conversation',
'medical_dictation',
'telephony',
'telephony_short',
]:
config.model = args.model
elif args.model == 'phone_call_enhanced':
config.model = 'phone_call'
config.useEnhanced = True
elif args.model == 'video_enhanced':
config.model = 'video'
config.useEnhanced = True
return config
def AddBetaRecognizeArgsToParser(self, parser):
"""Add beta arguments."""
parser.add_argument(
'--additional-language-codes',
type=arg_parsers.ArgList(),
default=[],
metavar='LANGUAGE_CODE',
help="""\
The BCP-47 language tags of other languages that the speech may be in.
Up to 3 can be provided.
If alternative languages are listed, recognition result will contain recognition
in the most likely language detected including the main language-code.""")
speaker_args = parser.add_group(required=False)
speaker_args.add_argument(
'--diarization-speaker-count',
type=int,
hidden=True,
action=actions.DeprecationAction(
'--diarization-speaker-count',
warn=('The `--diarization-speaker-count` flag is deprecated. '
'Use the `--min-diarization-speaker-count` and/or '
'`--max-diarization-speaker-count` flag instead.')),
help='Estimated number of speakers in the conversation '
'being recognized.')
speaker_args.add_argument(
'--min-diarization-speaker-count',
type=int,
help='Minimum estimated number of speakers in the conversation '
'being recognized.')
speaker_args.add_argument(
'--max-diarization-speaker-count',
type=int,
help='Maximum estimated number of speakers in the conversation '
'being recognized.')
speaker_args.add_argument(
'--enable-speaker-diarization',
action='store_true',
required=True,
help='Enable speaker detection for each recognized word in the top '
'alternative of the recognition result using an integer '
'speaker_tag provided in the WordInfo.')
parser.add_argument(
'--include-word-confidence',
action='store_true',
help='Include a list of words and the confidence for those words in '
'the top result.')
def UpdateBetaArgsInRecognitionConfig(self, args, config):
"""Updates config from command line arguments."""
config.alternativeLanguageCodes = args.additional_language_codes
# If any of diarization flags are used enable diarization.
if (args.enable_speaker_diarization or args.min_diarization_speaker_count or
args.max_diarization_speaker_count or args.diarization_speaker_count):
speaker_config = config.diarizationConfig = config.field_by_name(
'diarizationConfig').message_type(enableSpeakerDiarization=True)
if args.min_diarization_speaker_count:
speaker_config.minSpeakerCount = args.min_diarization_speaker_count
if args.max_diarization_speaker_count:
speaker_config.maxSpeakerCount = args.max_diarization_speaker_count
# Only use legacy flag if min/max fields were not used.
if args.diarization_speaker_count:
if (args.min_diarization_speaker_count or
args.max_diarization_speaker_count):
raise exceptions.InvalidArgumentException(
'--diarization-speaker-count',
'deprecated flag cannot be used with '
'--max/min_diarization_speaker_count flags')
speaker_config.minSpeakerCount = args.diarization_speaker_count
speaker_config.maxSpeakerCount = args.diarization_speaker_count
config.enableWordConfidence = args.include_word_confidence
def AddAlphaRecognizeArgsToParser(self, parser, api_version):
"""Add alpha arguments."""
meta_args = parser.add_group(
required=False,
help='Description of audio data to be recognized. '
'Note that the Google Cloud Speech-to-text-api does not use this '
'information, and only passes it through back into response.')
meta_args.add_argument(
'--naics-code',
action=MakeDeprecatedRecgonitionFlagAction('naics-code'),
type=int,
help='The industry vertical to which this speech recognition request '
'most closely applies.')
self._original_media_type_mapper = GetOriginalMediaTypeMapper(api_version)
self._original_media_type_mapper.choice_arg.AddToParser(meta_args)
self._interaction_type_mapper = GetInteractionTypeMapper(api_version)
self._interaction_type_mapper.choice_arg.AddToParser(meta_args)
self._microphone_distance_type_mapper = GetMicrophoneDistanceTypeMapper(
api_version)
self._microphone_distance_type_mapper.choice_arg.AddToParser(meta_args)
self._device_type_mapper = GetRecordingDeviceTypeMapper(api_version)
self._device_type_mapper.choice_arg.AddToParser(meta_args)
meta_args.add_argument(
'--recording-device-name',
action=MakeDeprecatedRecgonitionFlagAction('recording-device-name'),
help='The device used to make the recording. Examples: `Nexus 5X`, '
'`Polycom SoundStation IP 6000`')
meta_args.add_argument(
'--original-mime-type',
action=MakeDeprecatedRecgonitionFlagAction('original-mime-type'),
help='Mime type of the original audio file. Examples: `audio/m4a`, '
' `audio/mp3`.')
meta_args.add_argument(
'--audio-topic',
action=MakeDeprecatedRecgonitionFlagAction('audio-topic'),
help='Description of the content, e.g. "Recordings of federal supreme '
'court hearings from 2012".')
def UpdateAlphaArgsInRecognitionConfig(self, args, config):
"""Update RecognitionConfig with args."""
if (args.interaction_type is not None or
args.original_media_type is not None or args.naics_code is not None or
args.microphone_distance is not None or
args.recording_device_type is not None or
args.recording_device_name is not None or
args.original_mime_type is not None or args.audio_topic is not None):
if config.metadata is None:
config.metadata = config.field_by_name('metadata').message_type()
config.metadata.interactionType = (
self._interaction_type_mapper.GetEnumForChoice(args.interaction_type))
config.metadata.originalMediaType = (
self._original_media_type_mapper.GetEnumForChoice(
args.original_media_type))
config.metadata.industryNaicsCodeOfAudio = args.naics_code
config.metadata.microphoneDistance = (
self._microphone_distance_type_mapper.GetEnumForChoice(
args.microphone_distance))
config.metadata.recordingDeviceType = (
self._device_type_mapper.GetEnumForChoice(args.recording_device_type))
config.metadata.recordingDeviceName = args.recording_device_name
config.metadata.originalMimeType = args.original_mime_type
config.metadata.audioTopic = args.audio_topic
def MakeDeprecatedRecgonitionFlagAction(flag_name):
return actions.DeprecationAction(
'--' + flag_name,
warn='The `{}` flag is deprecated and will be removed. '
'The Google Cloud Speech-to-text api does not use it, and only '
'passes it through back into response.'.format(flag_name))
def GetRecordingDeviceTypeMapper(version):
messages = apis.GetMessagesModule(util.SPEECH_API, version)
return arg_utils.ChoiceEnumMapper(
'--recording-device-type',
messages.RecognitionMetadata.RecordingDeviceTypeValueValuesEnum,
action=MakeDeprecatedRecgonitionFlagAction('recording-device-type'),
custom_mappings={
'SMARTPHONE': ('smartphone', 'Speech was recorded on a smartphone.'),
'PC': ('pc',
'Speech was recorded using a personal computer or tablet.'),
'PHONE_LINE':
('phone-line', 'Speech was recorded over a phone line.'),
'VEHICLE': ('vehicle', 'Speech was recorded in a vehicle.'),
'OTHER_OUTDOOR_DEVICE': ('outdoor', 'Speech was recorded outdoors.'),
'OTHER_INDOOR_DEVICE': ('indoor', 'Speech was recorded indoors.')
},
help_str='The device type through which the original audio was '
'recorded on.',
include_filter=lambda x: not x.endswith('UNSPECIFIED'))
def GetMicrophoneDistanceTypeMapper(version):
messages = apis.GetMessagesModule(util.SPEECH_API, version)
return arg_utils.ChoiceEnumMapper(
'--microphone-distance',
messages.RecognitionMetadata.MicrophoneDistanceValueValuesEnum,
action=MakeDeprecatedRecgonitionFlagAction('microphone-distance'),
custom_mappings={
'NEARFIELD': ('nearfield', """\
The audio was captured from a microphone close to the speaker, generally within
1 meter. Examples include a phone, dictaphone, or handheld microphone."""),
'MIDFIELD':
('midfield', 'The speaker is within 3 meters of the microphone.'),
'FARFIELD':
('farfield',
'The speaker is more than 3 meters away from the microphone.'),
},
help_str='The distance at which the audio device is placed to record '
'the conversation.',
include_filter=lambda x: not x.endswith('UNSPECIFIED'))
def GetInteractionTypeMapper(version):
messages = apis.GetMessagesModule(util.SPEECH_API, version)
return arg_utils.ChoiceEnumMapper(
'--interaction-type',
messages.RecognitionMetadata.InteractionTypeValueValuesEnum,
action=MakeDeprecatedRecgonitionFlagAction('interaction-type'),
custom_mappings={
'DICTATION': (
'dictation',
'Transcribe speech to text to create a written document, such as '
+ 'a text-message, email or report.'),
'DISCUSSION': ('discussion',
'Multiple people in a conversation or discussion.'),
'PRESENTATION': ('presentation',
'One or more persons lecturing or presenting to ' +
'others, mostly uninterrupted.'),
'PHONE_CALL': (
'phone-call',
'A phone-call or video-conference in which two or more people, ' +
'who are not in the same room, are actively participating.'),
'PROFESSIONALLY_PRODUCED':
('professionally-produced',
'Professionally produced audio (eg. TV Show, Podcast).'),
'VOICE_COMMAND':
('voice-command',
'Transcribe voice commands, such as for controlling a device.'),
'VOICE_SEARCH':
('voice-search',
'Transcribe spoken questions and queries into text.'),
'VOICEMAIL':
('voicemail',
'A recorded message intended for another person to listen to.'),
},
help_str='Determining the interaction type in the conversation.',
include_filter=lambda x: not x.endswith('UNSPECIFIED'))
def GetOriginalMediaTypeMapper(version):
messages = apis.GetMessagesModule(util.SPEECH_API, version)
return arg_utils.ChoiceEnumMapper(
'--original-media-type',
messages.RecognitionMetadata.OriginalMediaTypeValueValuesEnum,
action=MakeDeprecatedRecgonitionFlagAction('original-media-type'),
custom_mappings={
'AUDIO': ('audio', 'The speech data is an audio recording.'),
'VIDEO': ('video', 'The speech data originally recorded on a video.'),
},
help_str='The media type of the original audio conversation.',
include_filter=lambda x: not x.endswith('UNSPECIFIED'))

View File

@@ -0,0 +1,330 @@
# -*- coding: utf-8 -*- #
# Copyright 2022 Google LLC. All Rights Reserved.
#
# 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.
"""Flags for speech commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import arg_parsers
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope.concepts import concepts
from googlecloudsdk.command_lib.util.apis import yaml_data
from googlecloudsdk.command_lib.util.concepts import concept_parsers
from googlecloudsdk.command_lib.util.concepts import presentation_specs
SPEAKER_COUNT_MAX_VALUE = 6
SPEAKER_COUNT_MIN_VALUE = 1
ALTERNATIVES_MAX_VALUE = 30
ALTERNATIVES_MIN_VALUE = 1
AUDIO_CHANNEL_COUNT_MAX_VALUE = 8
AUDIO_CHANNEL_COUNT_MIN_VALUE = 1
SAMPLE_RATE_MAX_VALUE = 48000
SAMPLE_RATE_MIN_VALUE = 8000
def AddRecognizerArgToParser(parser):
"""Sets up an argument for the recognizer resource."""
resource_data = yaml_data.ResourceYAMLData.FromPath('ml.speech.recognizer')
resource_spec = concepts.ResourceSpec.FromYaml(
resource_data.GetData(), api_version='v2'
)
presentation_spec = presentation_specs.ResourcePresentationSpec(
name='recognizer',
concept_spec=resource_spec,
required=True,
group_help='recognizer.',
)
return concept_parsers.ConceptParser([presentation_spec]).AddToParser(parser)
def AddLocationArgToParser(parser):
"""Parses location flag."""
location_data = yaml_data.ResourceYAMLData.FromPath('ml.speech.location')
resource_spec = concepts.ResourceSpec.FromYaml(location_data.GetData())
presentation_spec = presentation_specs.ResourcePresentationSpec(
name='--location',
concept_spec=resource_spec,
required=True,
group_help='location.',
)
return concept_parsers.ConceptParser([presentation_spec]).AddToParser(parser)
def AddLocationPositionalArgToParser(parser):
"""Parses location when there is no flag."""
location_data = yaml_data.ResourceYAMLData.FromPath('ml.speech.location')
resource_spec = concepts.ResourceSpec.FromYaml(location_data.GetData())
presentation_spec = presentation_specs.ResourcePresentationSpec(
name='location',
concept_spec=resource_spec,
required=True,
group_help='location.',
)
return concept_parsers.ConceptParser([presentation_spec]).AddToParser(parser)
def AddAllFlagsToParser(
parser, require_base_recognizer_attributes=False, use_store_true=False
):
"""Parses all flags for v2 STT API."""
AddRecognizerArgToParser(parser)
AddAsyncFlagToParser(parser)
parser.add_argument(
'--display-name',
help="""\
Name of this recognizer as it appears in UIs.
""",
)
AddBaseRecognizerAttributeFlagsToParser(
parser, required=require_base_recognizer_attributes
)
AddFeatureFlagsToParser(parser, use_store_true)
AddDecodingConfigFlagsToParser(parser)
def AddRecognizeRequestFlagsToParser(parser, add_async_flag=False):
"""Parses all flags for v2 STT API for command run-batch."""
AddRecognizerArgToParser(parser)
parser.add_argument(
'--audio',
required=True,
help=(
'Location of the audio file to transcribe. '
'Must be a audio data bytes, local file, or Google Cloud Storage URL '
'(in the format gs://bucket/object).'
),
)
AddFeatureFlagsToParser(parser)
AddDecodingConfigFlagsToParser(parser)
AddBaseRecognizerAttributeFlagsToParser(parser)
parser.add_argument(
'--hint-phrases',
metavar='PHRASE',
type=arg_parsers.ArgList(),
help="""\
A list of strings containing word and phrase "hints" so that the '
'speech recognition is more likely to recognize them. This can be '
'used to improve the accuracy for specific words and phrases, '
'for example, if specific commands are typically spoken by '
'the user. This can also be used to add additional words to the '
'vocabulary of the recognizer. '
'See https://cloud.google.com/speech/limits#content.
""",
)
parser.add_argument(
'--hint-phrase-sets',
metavar='PHRASE_SET',
type=arg_parsers.ArgList(),
help="""\
A list of phrase set resource names to use for speech recognition.
""",
)
parser.add_argument(
'--hint-boost',
type=arg_parsers.BoundedFloat(1, 20),
help="""\
Boost value for the phrases passed to --phrases.
Can have a value between 1 and 20.
""",
)
if add_async_flag:
AddAsyncFlagToParser(parser)
def AddAsyncFlagToParser(parser):
"""Adds async flag to parser."""
base.ASYNC_FLAG.AddToParser(parser)
base.ASYNC_FLAG.SetDefault(parser, False)
def AddBaseRecognizerAttributeFlagsToParser(parser, required=False):
"""Adds base recognizer attribute flags to parser."""
parser.add_argument(
'--model',
required=required,
help="""\
Which model to use for recognition requests.
Select the model best suited to your domain to get best results.
Guidance for choosing which model to use can be found in the
[Transcription Models Documentation](https://cloud.google.com/speech-to-text/v2/docs/transcription-model)
and the models supported in each region can be found in the
[Table Of Supported Models](https://cloud.google.com/speech-to-text/v2/docs/speech-to-text-supported-languages).
""",
)
parser.add_argument(
'--language-codes',
metavar='LANGUAGE_CODE',
required=required,
type=arg_parsers.ArgList(),
help="""\
Language code is one of `en-US`, `en-GB`, `fr-FR`.
Check [documentation](https://cloud.google.com/speech-to-text/docs/multiple-languages)
for using more than one language code.
""",
)
def AddDecodingConfigFlagsToParser(parser):
"""Adds decoding config flags to parser."""
decoding_config_group = parser.add_group(help='Encoding format')
decoding_config_group.add_argument(
'--encoding',
help="""\
Encoding format of the provided audio.
For headerless formats, must be set to `LINEAR16`, `MULAW,` or `ALAW`.
For other formats, set to `AUTO`. Overrides the recognizer
configuration if present, else uses recognizer encoding.
""",
)
sample_rate_help = (
'Sample rate in Hertz of the audio data sent for recognition. '
'Required if --encoding flag is specified and is not AUTO. '
'Must be set to a value between {} and {}.'.format(
SAMPLE_RATE_MIN_VALUE, SAMPLE_RATE_MAX_VALUE
)
)
decoding_config_group.add_argument(
'--sample-rate',
type=arg_parsers.BoundedInt(SAMPLE_RATE_MIN_VALUE, SAMPLE_RATE_MAX_VALUE),
help=sample_rate_help,
)
audio_channel_count_help = (
'Number of channels present in the audio data sent for recognition. '
'Required if --encoding flag is specified and is not AUTO. '
'Must be set to a value between {} and {}.'.format(
AUDIO_CHANNEL_COUNT_MIN_VALUE, AUDIO_CHANNEL_COUNT_MAX_VALUE
)
)
decoding_config_group.add_argument(
'--audio-channel-count',
type=arg_parsers.BoundedInt(
AUDIO_CHANNEL_COUNT_MIN_VALUE, AUDIO_CHANNEL_COUNT_MAX_VALUE
),
help=audio_channel_count_help,
)
def AddFeatureFlagsToParser(parser, use_store_true=False):
"""Adds feature flags to parser."""
features_group = parser.add_group(help='ASR Features')
speaker_diarization_group = features_group.add_group(
help='Speaker Diarization'
)
features_group.add_argument(
'--profanity-filter',
action='store_true'
if use_store_true
else arg_parsers.StoreTrueFalseAction,
help="""\
If set, the server will censor profanities.
""",
)
features_group.add_argument(
'--enable-word-time-offsets',
action='store_true'
if use_store_true
else arg_parsers.StoreTrueFalseAction,
help="""\
If set, the top result includes a list of words and their timestamps.
""",
)
features_group.add_argument(
'--enable-word-confidence',
action='store_true'
if use_store_true
else arg_parsers.StoreTrueFalseAction,
help="""\
If set, the top result includes a list of words and the confidence for
those words.
""",
)
features_group.add_argument(
'--enable-automatic-punctuation',
action='store_true'
if use_store_true
else arg_parsers.StoreTrueFalseAction,
help="""\
If set, adds punctuation to recognition result hypotheses.
""",
)
features_group.add_argument(
'--enable-spoken-punctuation',
action='store_true'
if use_store_true
else arg_parsers.StoreTrueFalseAction,
help="""\
If set, replaces spoken punctuation with the corresponding symbols in the request.
""",
)
features_group.add_argument(
'--enable-spoken-emojis',
action='store_true'
if use_store_true
else arg_parsers.StoreTrueFalseAction,
help="""\
If set, adds spoken emoji formatting.
""",
)
min_speaker_count_help = (
'Minimum number of speakers in the conversation. Must be less than or'
' equal to --max-speaker-count. Must be set to a value between {} and {}.'
.format(SPEAKER_COUNT_MIN_VALUE, SPEAKER_COUNT_MAX_VALUE)
)
max_speaker_count_help = (
'Maximum number of speakers in the conversation. Must be greater than or'
' equal to --min-speaker-count. Must be set to a value between {} and {}.'
.format(SPEAKER_COUNT_MIN_VALUE, SPEAKER_COUNT_MAX_VALUE)
)
speaker_diarization_group.add_argument(
'--min-speaker-count',
required=True,
type=arg_parsers.BoundedInt(
SPEAKER_COUNT_MIN_VALUE, SPEAKER_COUNT_MAX_VALUE
),
help=min_speaker_count_help,
)
speaker_diarization_group.add_argument(
'--max-speaker-count',
required=True,
type=arg_parsers.BoundedInt(
SPEAKER_COUNT_MIN_VALUE, SPEAKER_COUNT_MAX_VALUE
),
help=max_speaker_count_help,
)
features_group.add_argument(
'--separate-channel-recognition',
action='store_true'
if use_store_true
else arg_parsers.StoreTrueFalseAction,
help="""\
Mode for recognizing multi-channel audio using Separate Channel Recognition.
When set, the service will recognize each channel independently.
""",
)
max_alternatives_help = (
'Maximum number of recognition hypotheses to be returned. Must be set to'
' a value between {} and {}.'.format(
ALTERNATIVES_MIN_VALUE, ALTERNATIVES_MAX_VALUE
)
)
features_group.add_argument(
'--max-alternatives',
type=arg_parsers.BoundedInt(
ALTERNATIVES_MIN_VALUE, ALTERNATIVES_MAX_VALUE
),
help=max_alternatives_help,
)

View File

@@ -0,0 +1,40 @@
project:
name: project
collection: speech.projects
attributes:
- &project
parameter_name: projectsId
attribute_name: project
help: |
Project of the {resource}.
property: core/project
location:
name: location
collection: speech.projects.locations
attributes:
- *project
- &location
parameter_name: locationsId
attribute_name: location
help: |
Location of the {resource}.
operation:
name: operation
collection: speech.operations
attributes:
- parameter_name: operationsId
attribute_name: operation
help: The ID of the operation
recognizer:
name: recognizer
collection: speech.projects.locations.recognizers
attributes:
- *project
- *location
- &recognizer
parameter_name: recognizersId
attribute_name: recognizer
help: Speech-to-text recognizer.

View File

@@ -0,0 +1,142 @@
# -*- coding: utf-8 -*- #
# Copyright 2017 Google LLC. All Rights Reserved.
#
# 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.
"""Wrapper for interacting with speech API."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import os
from googlecloudsdk.api_lib.storage import storage_util
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import log
from googlecloudsdk.core import properties
from googlecloudsdk.core.console import console_io
from googlecloudsdk.core.util import files
from six.moves import urllib
SPEECH_API = 'speech'
SPEECH_API_VERSION = 'v1'
OUTPUT_ERROR_MESSAGE = ('[{}] is not a valid format for result output. Must be '
'a Google Cloud Storage URI '
'(format: gs://bucket/file).')
class Error(exceptions.Error):
"""Exceptions for this module."""
class AudioException(Error):
"""Raised if audio is not found."""
class UriFormatError(Error):
"""Error if the specified URI is invalid."""
def GetRecognitionAudioFromPath(path, version):
"""Determine whether path to audio is local, set RecognitionAudio message."""
messages = apis.GetMessagesModule(SPEECH_API, version)
audio = messages.RecognitionAudio()
if os.path.isfile(path):
audio.content = files.ReadBinaryFileContents(path)
elif storage_util.ObjectReference.IsStorageUrl(path):
audio.uri = path
else:
raise AudioException(
'Invalid audio source [{}]. The source must either be a local path '
'or a Google Cloud Storage URL (such as gs://bucket/object).'.format(
path))
return audio
def GetAudioHook(version=SPEECH_API_VERSION):
"""Returns a hook to get the RecognitionAudio message for an API version."""
def GetAudioFromPath(path):
"""Determine whether path to audio is local, build RecognitionAudio message.
Args:
path: str, the path to the audio.
Raises:
AudioException: If audio is not found locally and does not appear to be
Google Cloud Storage URL.
Returns:
speech_v1_messages.RecognitionAudio, the audio message.
"""
return GetRecognitionAudioFromPath(path, version)
return GetAudioFromPath
def ValidateOutputUri(output_uri):
"""Validates given output URI against validator function.
Args:
output_uri: str, the output URI for the analysis.
Raises:
UriFormatError: if the URI is not valid.
Returns:
str, The same output_uri.
"""
if output_uri and not storage_util.ObjectReference.IsStorageUrl(output_uri):
raise UriFormatError(OUTPUT_ERROR_MESSAGE.format(output_uri))
return output_uri
def MaybePrintSttUiLink(request):
"""Print Url to the Speech-to-text UI console for given recognize request."""
if (console_io.IsRunFromShellScript() or
properties.VALUES.core.disable_prompts.GetBool()):
return
audio_uri = request.audio.uri
if not audio_uri:
return
payload = {
'audio':
urllib.parse.quote_plus(
audio_uri[5:] if audio_uri.startswith('gs://') else audio_uri),
'encoding':
request.config.encoding,
'model':
request.config.model,
'locale':
request.config.languageCode,
'sampling':
request.config.sampleRateHertz,
'channels':
request.config.audioChannelCount,
'enhanced':
request.config.useEnhanced,
}
params = ';'.join('{}={}'.format(key, value)
for (key, value) in sorted(payload.items())
if value and ('unspecified' not in str(value).lower()))
url_tuple = ('https', 'console.cloud.google.com',
'/speech/transcriptions/create', params, '', '')
target_url = urllib.parse.urlunparse(url_tuple)
log.status.Print(
'Try this using the Speech-to-Text UI at {}'.format(target_url))

View File

@@ -0,0 +1,6 @@
zone:
arg_name: zone
default: global
help_text: |
Location to make calls. Non-global location is required for requests using AutoML models.
Currently, only 'us-central1' is supported as a non-global location. Defaults to 'global'.

View File

@@ -0,0 +1,161 @@
# -*- coding: utf-8 -*- #
# Copyright 2019 Google LLC. All Rights Reserved.
#
# 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.
"""Declarative hooks for ml speech."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import os
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.calliope import base as calliope_base
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import properties
from googlecloudsdk.core.util import files
SPEECH_API = 'translate'
def _GetApiVersion(args):
if args.calliope_command.ReleaseTrack() == calliope_base.ReleaseTrack.BETA:
return 'v3'
else:
return 'v3beta1'
class Error(exceptions.Error):
"""Exceptions for this module."""
class ContentFileError(Error):
"""Error if content file can't be read and isn't a GCS URL."""
def UpdateRequestLangDetection(unused_instance_ref, args, request):
"""The hook to inject content into the language detection request."""
content = args.content
content_file = args.content_file
messages = apis.GetMessagesModule(SPEECH_API, _GetApiVersion(args))
detect_language_request = messages.DetectLanguageRequest()
project = properties.VALUES.core.project.GetOrFail()
request.parent = 'projects/{}/locations/{}'.format(project, args.zone)
if args.IsSpecified('model'):
project = properties.VALUES.core.project.GetOrFail()
model = 'projects/{}/locations/{}/models/language-detection/{}'.format(
project, args.zone, args.model)
detect_language_request.model = model
if content_file:
if os.path.isfile(content_file):
detect_language_request.content = files.ReadFileContents(content_file)
else:
raise ContentFileError(
'Could not find --content-file [{}]. Content file must be a path '
'to a local file)'.format(content_file))
else:
detect_language_request.content = content
if args.IsSpecified('mime_type'):
detect_language_request.mimeType = args.mime_type
request.detectLanguageRequest = detect_language_request
return request
def UpdateRequestTranslateText(unused_instance_ref, args, request):
"""The hook to inject content into the translate request."""
content = args.content
content_file = args.content_file
messages = apis.GetMessagesModule(SPEECH_API, _GetApiVersion(args))
translate_text_request = messages.TranslateTextRequest()
project = properties.VALUES.core.project.GetOrFail()
request.parent = 'projects/{}/locations/{}'.format(project, args.zone)
if args.IsSpecified('model'):
project = properties.VALUES.core.project.GetOrFail()
model = 'projects/{}/locations/{}/models/{}'.format(
project, args.zone, args.model)
translate_text_request.model = model
if content_file:
if os.path.isfile(content_file):
translate_text_request.contents = [files.ReadFileContents(content_file)]
else:
raise ContentFileError(
'Could not find --content-file [{}]. Content file must be a path '
'to a local file)'.format(content_file))
else:
translate_text_request.contents = [content]
if args.IsSpecified('mime_type'):
translate_text_request.mimeType = args.mime_type
if args.IsSpecified('glossary_config'):
translate_text_request.glossaryConfig = \
messages.TranslateTextGlossaryConfig(glossary=args.glossaryConfig)
if args.IsSpecified('source_language'):
translate_text_request.sourceLanguageCode = args.source_language
translate_text_request.targetLanguageCode = args.target_language
request.translateTextRequest = translate_text_request
return request
def UpdateRequestGetSupportedLanguages(unused_instance_ref, args, request):
"""The hook to inject content into the getSupportedLanguages request."""
project = properties.VALUES.core.project.GetOrFail()
request.parent = 'projects/{}/locations/{}'.format(project, args.zone)
if args.IsSpecified('model'):
model = 'projects/{}/locations/{}/models/{}'.format(
project, args.zone, args.model)
request.model = model
return request
def UpdateRequestBatchTranslateText(unused_instance_ref, args, request):
"""The hook to inject content into the batch translate request."""
messages = apis.GetMessagesModule(SPEECH_API, _GetApiVersion(args))
batch_translate_text_request = messages.BatchTranslateTextRequest()
project = properties.VALUES.core.project.GetOrFail()
request.parent = 'projects/{}/locations/{}'.format(project, args.zone)
batch_translate_text_request.sourceLanguageCode = args.source_language
batch_translate_text_request.targetLanguageCodes = args.target_language_codes
batch_translate_text_request.outputConfig = messages.OutputConfig(
gcsDestination=messages.GcsDestination(outputUriPrefix=args.destination))
batch_translate_text_request.inputConfigs = \
[messages.InputConfig(gcsSource=messages.GcsSource(inputUri=k),
mimeType=v if v else None)
for k, v in sorted(args.source.items())]
if args.IsSpecified('models'):
batch_translate_text_request.models = \
messages.BatchTranslateTextRequest.ModelsValue(
additionalProperties=[
messages.BatchTranslateTextRequest.ModelsValue.AdditionalProperty(
key=k, value='projects/{}/locations/{}/models/{}'.format(
project, args.zone, v)) for k, v in sorted(args.models.items())
]
)
if args.IsSpecified('glossaries'):
additional_properties = \
[messages.BatchTranslateTextRequest.GlossariesValue.AdditionalProperty(
key=k, value=messages.TranslateTextGlossaryConfig(
glossary='projects/{}/locations/{}/glossaries/{}'.format(project, args.zone, v))) for k, v in sorted(args.glossaries.items())]
batch_translate_text_request.glossaries = \
messages.BatchTranslateTextRequest.GlossariesValue(
additionalProperties=additional_properties)
request.batchTranslateTextRequest = batch_translate_text_request
return request

View File

@@ -0,0 +1,9 @@
project:
name: project
collection: translate.projects
attributes:
- &project
parameter_name: projectsId
attribute_name: project
help: The project name of {resource}.
property: core/project

View File

@@ -0,0 +1,13 @@
operation:
name: operation
collection: videointelligence.projects.locations.operations
attributes:
- parameter_name: projectsId
attribute_name: project
help: Project of the {resource}.
- parameter_name: locationsId
attribute_name: location
help: Location of the {resource}.
- parameter_name: operationsId
attribute_name: operation
help: The ID of the operation

View File

@@ -0,0 +1,188 @@
# -*- coding: utf-8 -*- #
# Copyright 2017 Google LLC. All Rights Reserved.
#
# 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.
"""Utilities for gcloud ml video-intelligence commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import os
from googlecloudsdk.api_lib.storage import storage_util
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import log
from googlecloudsdk.core.util import files
from googlecloudsdk.core.util import iso_duration
from googlecloudsdk.core.util import times
VIDEO_API = 'videointelligence'
VIDEO_API_VERSION = 'v1'
INPUT_ERROR_MESSAGE = ('[{}] is not a valid format for video input. Must be a '
'local path or a Google Cloud Storage URI '
'(format: gs://bucket/file).')
OUTPUT_ERROR_MESSAGE = ('[{}] is not a valid format for result output. Must be '
'a Google Cloud Storage URI '
'(format: gs://bucket/file).')
SEGMENT_ERROR_MESSAGE = ('Could not get video segments from [{0}]. '
'Please make sure you give the desired '
'segments in the form: START1:END1,START2:'
'END2, etc.: [{1}]')
class Error(exceptions.Error):
"""Base error class for this module."""
class SegmentError(Error):
"""Error for poorly formatted video segment messages."""
class VideoUriFormatError(Error):
"""Error if the video input URI is invalid."""
class AudioTrackError(Error):
"""Error if the audio tracks setting is invalid."""
def ValidateAndParseSegments(given_segments):
"""Get VideoSegment messages from string of form START1:END1,START2:END2....
Args:
given_segments: [str], the list of strings representing the segments.
Raises:
SegmentError: if the string is malformed.
Returns:
[GoogleCloudVideointelligenceXXXVideoSegment], the messages
representing the segments or None if no segments are specified.
"""
if not given_segments:
return None
messages = apis.GetMessagesModule(VIDEO_API, VIDEO_API_VERSION)
segment_msg = messages.GoogleCloudVideointelligenceV1VideoSegment
segment_messages = []
segments = [s.split(':') for s in given_segments]
for segment in segments:
if len(segment) != 2:
raise SegmentError(SEGMENT_ERROR_MESSAGE.format(
','.join(given_segments), 'Missing start/end segment'))
start, end = segment[0], segment[1]
# v1beta2 requires segments as a duration string representing the
# count of seconds and fractions of seconds to nanosecond resolution
# e.g. offset "42.596413s". To perserve backward compatibility with v1beta1
# we will parse any segment timestamp with out a duration unit as an
# int representing microseconds.
try:
start_duration = _ParseSegmentTimestamp(start)
end_duration = _ParseSegmentTimestamp(end)
except ValueError as ve:
raise SegmentError(SEGMENT_ERROR_MESSAGE.format(
','.join(given_segments), ve))
sec_fmt = '{}s'
segment_messages.append(segment_msg(
endTimeOffset=sec_fmt.format(end_duration.total_seconds),
startTimeOffset=sec_fmt.format(start_duration.total_seconds)))
return segment_messages
def _ParseSegmentTimestamp(timestamp_string):
"""Parse duration formatted segment timestamp into a Duration object.
Assumes string with no duration unit specified (e.g. 's' or 'm' etc.) is
an int representing microseconds.
Args:
timestamp_string: str, string to convert
Raises:
ValueError: timestamp_string is not a properly formatted duration, not a
int or int value is <0
Returns:
Duration object represented by timestamp_string
"""
# Assume timestamp_string passed as int number of microseconds if no unit
# e.g. 4566, 100, etc.
try:
microseconds = int(timestamp_string)
except ValueError:
try:
duration = times.ParseDuration(timestamp_string)
if duration.total_seconds < 0:
raise times.DurationValueError()
return duration
except (times.DurationSyntaxError, times.DurationValueError):
raise ValueError('Could not parse timestamp string [{}]. Timestamp must '
'be a properly formatted duration string with time '
'amount and units (e.g. 1m3.456s, 2m, 14.4353s)'.format(
timestamp_string))
else:
log.warning("Time unit missing ('s', 'm','h') for segment timestamp [{}], "
"parsed as microseconds.".format(timestamp_string))
if microseconds < 0:
raise ValueError('Could not parse duration string [{}]. Timestamp must be'
'greater than >= 0)'.format(timestamp_string))
return iso_duration.Duration(microseconds=microseconds)
def ValidateOutputUri(output_uri):
"""Validates given output URI against validator function.
Args:
output_uri: str, the output URI for the analysis.
Raises:
VideoUriFormatError: if the URI is not valid.
Returns:
str, The same output_uri.
"""
if output_uri and not storage_util.ObjectReference.IsStorageUrl(output_uri):
raise VideoUriFormatError(OUTPUT_ERROR_MESSAGE.format(output_uri))
return output_uri
def UpdateRequestWithInput(unused_ref, args, request):
"""The Python hook for yaml commands to inject content into the request."""
path = args.input_path
if os.path.isfile(path):
request.inputContent = files.ReadBinaryFileContents(path)
elif storage_util.ObjectReference.IsStorageUrl(path):
request.inputUri = path
else:
raise VideoUriFormatError(INPUT_ERROR_MESSAGE.format(path))
return request
# Argument Processors
def AudioTrackProcessor(tracks):
"""Verify at most two tracks, convert to [int, int]."""
if len(tracks) > 2:
raise AudioTrackError('Can not specify more than two audio tracks.')
return tracks

View File

@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*- #
# Copyright 2017 Google LLC. All Rights Reserved.
#
# 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.
"""Flags for gcloud ml vision commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import arg_parsers
def AspectRatioType(value):
"""A type function to be used to parse aspect ratios."""
try:
return float(value)
except ValueError:
parts = value.split(':')
if len(parts) == 2:
try:
return float(parts[0]) / float(parts[1])
except ValueError:
pass
raise arg_parsers.ArgumentTypeError(
'Each aspect ratio must either be specified as a decimal (ex. 1.333) '
'or as a ratio of width to height (ex 4:3)')

View File

@@ -0,0 +1,565 @@
# -*- coding: utf-8 -*- #
# Copyright 2019 Google LLC. All Rights Reserved.
#
# 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.
"""Utilities for ml vision product search surface."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import copy
from googlecloudsdk.api_lib.ml.vision import api_utils
from googlecloudsdk.calliope import arg_parsers
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope.concepts import concepts
from googlecloudsdk.command_lib.util.concepts import concept_parsers
from googlecloudsdk.core import exceptions as core_exceptions
from googlecloudsdk.core.console import console_io
class Error(core_exceptions.Error):
"""Base Error for this Module."""
class LabelsFormatError(Error):
"""Raises when labels are not formatted correctly."""
def ProductLabelsArgumentsForCreate():
return [
base.Argument(
'--product-labels',
metavar='KEY=VALUE',
type=arg_parsers.ArgList(min_length=1, element_type=str),
action='append',
help="""\
Labels that can be attached to the product. Labels are specified as
key-value pairs. Multiple values can be assigned to the same key and
one product may have up to 100 product labels.""")
]
def GetClearLabelsFlag(labels_name='product-labels'):
return base.Argument(
'--clear-{labels}'.format(labels=labels_name),
action='store_true',
help="""\
Remove all product labels. If `--add-{labels}` is also specified, then
`--clear-{labels}` is applied first.
For example, to remove all product labels:
$ {{command}} --clear-{labels}
To set the product labels to exactly "foo" and "baz":
$ {{command}} --clear-{labels} --add-{labels}='foo=bar,baz=qux'
""".format(labels=labels_name))
def GetRemoveLabelsFlag(labels_name='product-labels'):
return base.Argument(
'--remove-{labels}'.format(labels=labels_name),
metavar='KEY=VALUE',
type=arg_parsers.ArgList(),
action='append',
help="""\
List of product labels to remove. If `--add-{labels}` is also
specified, then `--remove-{labels}` is applied first. If a label does
not exist it is silently ignored. Because each key can be associated
with multiple values, both key and value need to be specified to
remove the product label.
For example, to remove the product labels 'foo=baz' and 'baz=qux':
$ {{command}} --remove-{labels}='foo=baz,baz=qux'
""".format(labels=labels_name))
def GetAddLabelsFlag(labels_name='product-labels'):
return base.Argument(
'--add-{}'.format(labels_name),
metavar='KEY=VALUE',
type=arg_parsers.ArgList(),
action='append',
help="""\
List of product labels to add. If a label already exists, it is
silently ignored.
For example, to add the product labels 'foo=baz' and 'baz=qux':
$ {{command}} --add-{labels}='foo=baz,baz=qux'
To change the product label 'foo=baz' to 'foo=qux':
$ {{command}} --remove-{labels}='foo=baz' --add-{labels}='foo-qux'
""".format(labels=labels_name))
def ProductLabelsArgumentsForUpdate():
remove_group = base.ArgumentGroup(mutex=True)
remove_group.AddArgument(GetClearLabelsFlag())
remove_group.AddArgument(GetRemoveLabelsFlag())
return [GetAddLabelsFlag(), remove_group]
def _FormatLabelsArgsToKeyValuePairs(labels):
"""Flattens the labels specified in cli to a list of (k, v) pairs."""
labels = [] if labels is None else labels
labels_flattened = []
for labels_sublist in labels:
labels_flattened.extend([label.strip() for label in labels_sublist])
labels_flattened_unique = list(set(labels_flattened))
return [_ExtractKeyValueFromLabel(label) for label in labels_flattened_unique]
def _FormatKeyValuePairsToLabelsMessage(labels):
"""Converts the list of (k, v) pairs into labels API message."""
sorted_labels = sorted(labels, key=lambda x: x[0] + x[1])
return [
api_utils.GetMessage().KeyValue(key=k, value=v) for k, v in sorted_labels
]
def _ExtractKeyValuePairsFromLabelsMessage(labels):
"""Extracts labels as a list of (k, v) pairs from the labels API message."""
labels = [] if labels is None else labels
return [(label.key, label.value) for label in labels]
def _ExtractKeyValueFromLabel(label):
"""Extracts key and value from label like 'key=value'.
Args:
label: str, the label to extract key and values, i.e. 'foo=buz'.
Returns:
(k, v): k is the substring before '=', v is the substring after '='.
Raises:
LabelsFormatError, raises when label is not formatted as 'key=value', or
key or value is empty.
"""
try:
k, v = label.split('=')
if k and v:
return k, v
raise ValueError('Key or value cannot be empty string.')
except ValueError:
raise LabelsFormatError('Each label must be formatted as "key=value".'
' key and value cannot be empty.')
def PrepareProductLabelsForProductCreationRequest(ref, args, request):
"""Sets labels if user specifies the --product-labels in product creation."""
del ref # Unused
if not args.IsSpecified('product_labels'):
return request
else:
labels = _FormatLabelsArgsToKeyValuePairs(args.product_labels)
request.product.productLabels = _FormatKeyValuePairsToLabelsMessage(labels)
return request
def _ClearLabels(existing_labels):
del existing_labels # Unused
return []
def _RemoveLabels(existing_labels, labels_to_remove):
"""Removes labels in labels_to_remove from existing_labels.
Args:
existing_labels: list of (k,v) pairs, existing labels.
labels_to_remove: list of (k, v) pairs, labels to remove.
Returns:
List of remaining labels after removal.
"""
return [label for label in existing_labels if label not in labels_to_remove]
def _AddLabels(existing_labels, labels_to_add):
"""Adds labels in labels_to_add to existing_labels."""
updated_labels = existing_labels + labels_to_add
return list(set(updated_labels))
def _LabelsUpdated(existing_labels, updated_labels):
return set(existing_labels) != set(updated_labels)
def _AddFieldToUpdateMask(field, patch_request):
update_mask = patch_request.updateMask
if update_mask:
if update_mask.count(field) == 0:
patch_request.updateMask = update_mask + ',' + field
else:
patch_request.updateMask = field
return patch_request
def _GetExistingProductLabels(product_ref):
"""Fetches the existing product labels to update."""
get_request_message = api_utils.GetMessage(
).VisionProjectsLocationsProductsGetRequest(name=product_ref.RelativeName())
product = api_utils.GetClient().projects_locations_products.Get(
get_request_message)
return product.productLabels
def UpdateLabelsAndUpdateMaskForProductUpdateRequest(product_ref, args,
patch_request):
"""Updates product labels field."""
if not args.IsSpecified('add_product_labels') and not args.IsSpecified(
'remove_product_labels') and not args.IsSpecified('clear_product_labels'):
return patch_request
existing_labels = _GetExistingProductLabels(product_ref)
existing_labels = _ExtractKeyValuePairsFromLabelsMessage(existing_labels)
existing_labels_copy = copy.deepcopy(existing_labels)
if args.clear_product_labels:
existing_labels = _ClearLabels(existing_labels)
if args.remove_product_labels:
labels_to_remove = _FormatLabelsArgsToKeyValuePairs(
args.remove_product_labels)
existing_labels = _RemoveLabels(existing_labels, labels_to_remove)
if args.add_product_labels:
labels_to_add = _FormatLabelsArgsToKeyValuePairs(args.add_product_labels)
existing_labels = _AddLabels(existing_labels, labels_to_add)
if _LabelsUpdated(existing_labels, existing_labels_copy):
patch_request = _AddFieldToUpdateMask('productLabels', patch_request)
updated_labels_message = _FormatKeyValuePairsToLabelsMessage(
existing_labels)
if patch_request.product is None:
patch_request.product = api_utils.GetMessage().Product()
patch_request.product.productLabels = updated_labels_message
return patch_request
def AddBoundingPolygonsArg():
return [
base.Argument(
'--bounding-polygon',
type=arg_parsers.ArgDict(
spec={
'vertices': list,
'normalized-vertices': list
},
min_length=1),
action='append',
help="""\
Bounding polygon around the areas of interest in the reference image.
If this field is empty, the system will try to detect regions of interest.
This flag is repeatable to specify multiple bounding polygons. At most 10
bounding polygons will be used.
A bounding polygon can be specified by a list of vertices or normalized
vertices or both. A vertex (x, y) represents a 2D point in the image. x, y
are integers and are in the same scale as the original image.
The normalized vertex coordinates are relative to original image and
range from 0 to 1.
Because of the complexity of this flag, it should be specified
with the `--flags-file`. See $ gcloud topic flags-file for details.
See the examples section for how to use `--bounding-polygon` in
`--flags-file`.""")
]
def AddBoundingPolygonsToReferenceImageCreationRequest(ref, args, request):
"""Populate the boundingPolygon message."""
del ref # Unused
if not args.IsSpecified('bounding_polygon'):
return request
bounding_polygon_message = []
for bounding_polygon in args.bounding_polygon:
bounding_polygon_message.append(
_PrepareBoundingPolygonMessage(bounding_polygon))
request.referenceImage.boundingPolys = bounding_polygon_message
return request
def _PrepareBoundingPolygonMessage(bounding_polygon):
"""Prepares the bounding polygons message given user's input."""
bounding_polygon_message = api_utils.GetMessage().BoundingPoly()
vertices_message = []
normalized_vertices_message = []
if 'vertices' in bounding_polygon:
for vertex in bounding_polygon['vertices']:
vertex_int = Vertex(vertex['x'], vertex['y'])
vertices_message.append(api_utils.GetMessage().Vertex(
x=vertex_int.x, y=vertex_int.y))
if 'normalized-vertices' in bounding_polygon:
for normalized_vertex in bounding_polygon['normalized-vertices']:
normalized_vertex_float = NormalizedVertex(normalized_vertex['x'],
normalized_vertex['y'])
normalized_vertices_message.append(
api_utils.GetMessage().NormalizedVertex(
x=normalized_vertex_float.x, y=normalized_vertex_float.y))
bounding_polygon_message.vertices = vertices_message
bounding_polygon_message.normalizedVertices = normalized_vertices_message
return bounding_polygon_message
class BoundingPolygonFormatError(Error):
"""Raises when the specified polygon is incorrect."""
class VertexFormatError(BoundingPolygonFormatError):
"""Raises when the vertex is not specified correctly."""
class NormalizedVertexFormatError(BoundingPolygonFormatError):
"""Raises when the normalized vertex is not specified correctly."""
class Vertex(object):
"""Vertex to define the polygon.
Attributes:
x: int, x coordinate of a point on a image.
y: int, y coordinate of a point on a image.
"""
def __init__(self, x, y):
self.x = _ValidateAndConvertCoordinateToInteger(x)
self.y = _ValidateAndConvertCoordinateToInteger(y)
def _ValidateAndConvertCoordinateToInteger(coordinate):
try:
coordinate_int = int(coordinate)
if coordinate_int < 0:
raise ValueError
except ValueError:
raise VertexFormatError('Coordinates must be non-negative integers.')
return coordinate_int
class NormalizedVertex(object):
"""Normalized Vertex to define the polygon.
Attributes:
x: float, a float from 0 to 1, inclusive. x coordinate of a point on a
image.
y: float, a float from 0 to 1, inclusive. y coordinate of a point on a
image.
"""
def __init__(self, x, y):
self.x = _ValidateAndConvertCoordinateToFloat(x)
self.y = _ValidateAndConvertCoordinateToFloat(y)
def _ValidateAndConvertCoordinateToFloat(coordinate):
try:
coordinate_float = float(coordinate)
if coordinate_float < 0 or coordinate_float > 1:
raise ValueError
except ValueError:
raise NormalizedVertexFormatError(
'Coordinates must be floats from 0 to 1, inclusive')
return coordinate_float
def FixOperationNameInGetOperationRequest(ref, args, request):
del ref, args # Unused
name = request.name
if name.count('operations') == 2 and name.startswith('operations/'):
name = name[len('operations/'):]
request.name = name
return request
def _GetProductFullName(ref, args):
return 'projects/{}/locations/{}/products/{}'.format(
ref.projectsId, ref.locationsId, args.product)
def FixProductInAddProductToProductSetRequest(ref, args, request):
"""Sets product field to the full name of the product."""
product_name = _GetProductFullName(ref, args)
request.addProductToProductSetRequest.product = product_name
return request
def FixProductInRemoveProductFromProductSetRequest(ref, args, request):
"""Sets product field to the full name of the product."""
product_name = _GetProductFullName(ref, args)
request.removeProductFromProductSetRequest.product = product_name
return request
def FixNameInListProductsInProductSetRequest(ref, args, request):
"""Removes the redundant suffix."""
del ref, args # Unused
name = request.name
if name[-9:] == '/products':
name = name[:-9]
request.name = name
return request
def _LocationAttributeConfig(name='location'):
return concepts.ResourceParameterAttributeConfig(
name=name, help_text='The location of the {resource}.')
def _ProductSetAttributeConfig(name='product-set'):
return concepts.ResourceParameterAttributeConfig(
name=name, help_text='The product set for the {resource}.')
def _GetProductSetResourceSpec(resource_name='product set'):
return concepts.ResourceSpec(
'vision.projects.locations.productSets',
resource_name=resource_name,
productSetsId=_ProductSetAttributeConfig(),
locationsId=_LocationAttributeConfig(),
projectsId=concepts.DEFAULT_PROJECT_ATTRIBUTE_CONFIG,
)
def _GetProductSetConcept():
return concept_parsers.ConceptParser.ForResource(
'--product-set',
_GetProductSetResourceSpec(),
'The product set to be searched for similar images.',
required=True,
prefixes=True)
def ProductSetArgsForDetectProduct():
return [_GetProductSetConcept()]
def AddProductSetToDetectProductRequest(ref, args, request):
"""Adds productSet field to the detect product request."""
del ref # Unused
try:
single_request = request.requests[0]
except IndexError:
return request
product_set_ref = args.CONCEPTS.product_set.Parse()
product_set_name = product_set_ref.RelativeName()
single_request = _InstantiateProductSearchParams(single_request)
single_request.imageContext.productSearchParams.productSet = product_set_name
return request
def AddBoundingPolygonToDetectProductRequest(ref, args, request):
"""Adds the boundingPoly field to detect product request."""
del ref # Unused
try:
single_request = request.requests[0]
except IndexError:
return request
if not args.IsSpecified('bounding_polygon'):
return request
polygon = _ValidateAndExtractFromBoundingPolygonArgs(args.bounding_polygon)
if not polygon:
return request
single_request = _InstantiateProductSearchParams(single_request)
product_search_params = single_request.imageContext.productSearchParams
if not product_search_params.boundingPoly:
product_search_params.boundingPoly = api_utils.GetMessage().BoundingPoly()
bounding_poly = product_search_params.boundingPoly
if isinstance(polygon[0], Vertex):
vertices = [api_utils.GetMessage().Vertex(x=v.x, y=v.y) for v in polygon]
bounding_poly.vertices = vertices
else:
normalized_vertices = [
api_utils.GetMessage().NormalizedVertex(x=v.x, y=v.y) for v in polygon
]
bounding_poly.normalizedVertices = normalized_vertices
return request
def _InstantiateProductSearchParams(request):
if not request.imageContext:
request.imageContext = api_utils.GetMessage().ImageContext()
if not request.imageContext.productSearchParams:
request.imageContext.productSearchParams = api_utils.GetMessage(
).ProductSearchParams()
return request
def _ValidateAndExtractFromBoundingPolygonArgs(bounding_polygon_arg):
"""Extracts coordinates from users' input."""
if not bounding_polygon_arg:
return []
coordinates = bounding_polygon_arg.split(',')
grouped_coordinates = GroupCoordinates(coordinates)
if _IsPolygonSpecifiedAsVertex(coordinates):
return [Vertex(x, y) for x, y in grouped_coordinates]
if _IsPolygonSpecifiedAsNormalizedVertex(coordinates):
return [NormalizedVertex(x, y) for x, y in grouped_coordinates]
raise BoundingPolygonFormatError(
'Coordinates of normalized vertex should have decimal points, '
'Coordinates of vertex should be integers and cannot have decimal points.'
)
def GroupCoordinates(coordinates):
if len(coordinates) % 2 != 0:
raise BoundingPolygonFormatError(
'There must be an even number of values in the list.')
grouped_coordinates = []
for i in range(0, len(coordinates), 2):
grouped_coordinates.append((coordinates[i], coordinates[i + 1]))
return grouped_coordinates
def _IsPolygonSpecifiedAsVertex(bounding_polygon_coordinates):
coordinate_with_decimal_point = [
c for c in bounding_polygon_coordinates if '.' in c
]
return not coordinate_with_decimal_point
def _IsPolygonSpecifiedAsNormalizedVertex(bounding_polygon_coordinates):
coordinate_with_decimal_point = [
c for c in bounding_polygon_coordinates if '.' in c
]
return len(coordinate_with_decimal_point) == len(bounding_polygon_coordinates)
def PromptDeleteAll(ref, args, request):
"""Prompts to confirm deletion. Changes orphan-products to None if False."""
del ref
if not args.force:
console_io.PromptContinue(
message=('You are about to delete products. After deletion, the '
'products cannot be restored.'),
cancel_on_no=True)
request.purgeProductsRequest.force = True
# because deleteOrphanProducts is boolean, if it isn't declared, it will
# have a value of False instead of None
if args.product_set:
request.purgeProductsRequest.deleteOrphanProducts = None
return request

View File

@@ -0,0 +1,84 @@
project:
name: project
collection: vision.projects
attributes:
- &project
parameter_name: projectsId
attribute_name: project
help: The project name of {resource}.
property: core/project
location:
name: location
collection: vision.projects.locations
attributes:
- *project
- &location
parameter_name: locationsId
attribute_name: location
help: The location of {resource}.
# TODO(b/141858337): Delete this and just use entry above
non_primary_location:
name: location
collection: vision.projects.locations
attributes:
- *project
- parameter_name: locationsId
attribute_name: location
help: The location of {resource}.
product_set:
name: product set
collection: vision.projects.locations.productSets
request_id_field: productSetId
attributes:
- *project
- *location
- &product_set
parameter_name: productSetsId
attribute_name: product_set
help: The ID of the product set.
product_set_product:
name: product set
collection: vision.projects.locations.productSets.products
attributes:
- *project
- *location
- parameter_name: productSetsId
attribute_name: product-set
help: The ID of the product set.
product:
name: product
collection: vision.projects.locations.products
request_id_field: productId
attributes:
- *project
- *location
- &product
parameter_name: productsId
attribute_name: product
help: The ID of product.
operation:
name: operation
collection: vision.operations
attributes:
- parameter_name: operationsId
attribute_name: operation
help: The ID of the operation.
reference_image:
name: reference image
collection: vision.projects.locations.products.referenceImages
request_id_field: referenceImageId
attributes:
- *project
- *location
- *product
- &reference_iamge:
parameter_name: referenceImagesId
attribute_name: reference_image
help: The ID of the reference image.

View File

@@ -0,0 +1,137 @@
# -*- coding: utf-8 -*- #
# Copyright 2017 Google LLC. All Rights Reserved.
#
# 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.
"""Utilities for gcloud ml vision commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import os
import re
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.core import exceptions
from googlecloudsdk.core.util import files
VISION_API = 'vision'
VISION_API_VERSION = 'v1'
IMAGE_URI_FORMAT = r'^(https{,1}?|gs)://'
FILE_URI_FORMAT = r'gs://([^/]+)/(.+)'
FILE_PREFIX = r'gs://([^/]+)(/.*)*'
class Error(exceptions.Error):
"""Error for gcloud ml vision commands."""
class ImagePathError(Error):
"""Error if an image path is improperly formatted."""
class GcsSourceError(Error):
"""Error if a gcsSource path is improperly formatted."""
class GcsDestinationError(Error):
"""Error if a gcsDestination path is improperly formatted."""
def GetImageFromPath(path):
"""Builds an Image message from a path.
Args:
path: the path arg given to the command.
Raises:
ImagePathError: if the image path does not exist and does not seem to be
a remote URI.
Returns:
vision_v1_messages.Image: an image message containing information for the
API on the image to analyze.
"""
messages = apis.GetMessagesModule(VISION_API, VISION_API_VERSION)
image = messages.Image()
if os.path.isfile(path):
image.content = files.ReadBinaryFileContents(path)
elif re.match(IMAGE_URI_FORMAT, path):
image.source = messages.ImageSource(imageUri=path)
else:
raise ImagePathError(
'The image path does not exist locally or is not properly formatted. '
'A URI for a remote image must be a Google Cloud Storage image URI, '
'which must be in the form `gs://bucket_name/object_name`, or a '
'publicly accessible image HTTP/HTTPS URL. Please double-check your '
'input and try again.')
return image
def GetGcsSourceFromPath(input_file):
"""Validate a Google Cloud Storage location to read the PDF/TIFF file from.
Args:
input_file: the input file path arg given to the command.
Raises:
GcsSourceError: if the file is not a Google Cloud Storage object.
Returns:
vision_v1_messages.GcsSource: Google Cloud Storage URI for the input file.
This must only be a Google Cloud Storage object.
Wildcards are not currently supported.
"""
messages = apis.GetMessagesModule(VISION_API, VISION_API_VERSION)
gcs_source = messages.GcsSource()
if re.match(FILE_URI_FORMAT, input_file):
gcs_source.uri = input_file
else:
raise GcsSourceError(
'The URI for the input file must be a Google Cloud Storage object, '
'which must be in the form `gs://bucket_name/object_name.'
'Please double-check your input and try again.')
return gcs_source
def GetGcsDestinationFromPath(path):
"""Validate a Google Cloud Storage location to write the output to.
Args:
path: the path arg given to the command.
Raises:
GcsDestinationError: if the file is not a Google Cloud Storage object.
Returns:
vision_v1_messages.GcsDestination:Google Cloud Storage URI prefix
where the results will be stored.
This must only be a Google Cloud Storage object.
Wildcards are not currently supported.
"""
messages = apis.GetMessagesModule(VISION_API, VISION_API_VERSION)
gcs_destination = messages.GcsDestination()
if re.match(FILE_PREFIX, path):
gcs_destination.uri = path
else:
raise GcsDestinationError(
'The URI for the input file must be a Google Cloud Storage object, '
'which must be in the File prefix format `gs://bucket_name/here/file_name_prefix.'
'or directory prefix format `gs://bucket_name/some/location/. '
'Please double-check your input and try again.')
return gcs_destination