- 原文地址:Build a Babel Fish with Nexmo, and the Microsoft Translator Speech API
- 原文做者:Naomi Pentrel
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:Starrier
- 校对者:haoyuez
若是在过去的几个月时间里你关注过互联网上的变化,那你就会注意到 Google 的即时翻译 Pixel Buds。它是一个像给 Galaxy 的 Hitchhiker 指南中 Bable Fish 同样的技术,能够为穿戴者翻译任何可感知的语言并让他们像虚拟人类同样与穿戴者进行交流。但使用 Google 的 Pixel Buds 代价昂贵 —— 那么咱们为什么不本身动手构建呢?这也是 Danielle 和我最近在 hackference 上所构想的。咱们想要去建立一个让电话交流的双方能够根据本身所需,听到彼此说话内容的翻译版本的 Nexmo Babel Fish。html
来自给 Galaxy 的 Hitchhiker 的指南中的 Bable Fish 的图片。前端
在这篇博客中,咱们将介绍搭建 Babel Fish 系统的步骤。首先咱们须要设置和配置环境。而后咱们会设置一个 Nexmo number 来处理调用。在此以后,咱们会搭建一个经过 WebSocket 接收语音,并将其从 Nexmo number 传送到微软语言翻译 API 的 Python 服务器。咱们会使用语音翻译 API 来处理转录和翻译。在此基础上,咱们将实现管理双向对话的逻辑,并指示 Nexmo number 说出翻译内容。为了便于实现,双方必须都调用咱们的 Nexmo number。你能够参考下面的高级图表,它展现了来自任何一方的语音实例是如何处理的。注意在本教程中,我会使用一个德语/英语的示例。python
该图表展现了消息如何在系统中传递。一个德语调用者经过 Nexmo 发送一条德语消息给 Python 服务器。Python 服务器将德语音频发送给微软语音 API。语音 API 将英文翻译做为文本发送到 Python 服务器来响应请求。Python 服务器会向 Nexmo 发送请求,向英语调用者说出英语消息。此时英语调用者便会听到翻译后的英语信息。android
若是你只想看代码,能够在 Github 上找到。ios
你须要同时安装 Python 2.x 或 3.x 和 HTTP 通道软件 ngrok。咱们会列出你在安装过程当中须要的全部命令。git
咱们从使用 Virtualenv 来为此项目配置虚拟环境的 DIY Babel Fish 的解决方案开始。Virtualenv 容许咱们将此项目与其余项目隔离。继续为此项目建立目录,并将如下依赖列表复制到你项目目录中命名为 requirements.txt
的文件中:github
nexmo
tornado>=4.4.2
requests>=2.12.4
复制代码
为了建立并激活你的虚拟环境,请在终端运行如下命令:web
virtualenv venv # sets up the environment
source venv/bin/activate # activates the environment
pip install -r requirement.txt # installs our dependencies
# 若是你使用的是 Python 3,请使用如下命令
pip3 install -r requirement.txt
复制代码
此时,经过在单独打开一个终端的窗口中运行下列命令来启动 ngrok。ngrok 容许咱们将本地 5000 端口暴露给外部请求。为此,你须要让 ngrok 在后台保持正常运行。你能够阅读更多关于用 Nexmo 连接 ngrok 的信息。json
ngrok http 5000
复制代码
一旦你运行上述命令,你的终端就会和下面的截图相似。下一步,你须要在配置你的 Nexmo 应用程序和编号 时转发 URL。后端
在终端运行的 ngrok 以及展现转发来自 “016a0331.ngrok.io” URL 的屏幕截图。
使用咱们的翻译服务须要一个 Nexmo Number。若是你尚未帐号,能够在 dashboard.nexmo.com/sign-up 进行注册,请前往 dashboard.nexmo.com/buy-numbers 购买一个具备语音功能的 Nexmo number。
该截屏展现了用户如何使用 Nexmo 的购买编号菜单来购买 Nexmo number。用户须要选择国家和语音将做为特征,移动设备做为类型,最后点击搜索按钮。而后点击第一个数字边的购买连接,即可确认购买。
进入你的应用程序,而后新增一个。对事件 URL 和应答 URL 使用 Ngrok 转发 URL,添加 /event
做为事件 URL(e.g. http://016a0331.ngrok.io/event
) 的路径,对应答 URL(e.g. http://016a0331.ngrok.io/ncco
) 使用 /ncco
。咱们以后会设置这些端点。在你的电脑上经过用户接口生成并存储一对公钥/私钥对。
使用 Nexmo 应用程序目录建立应用程序的用户屏幕截图。用户点击新增应用程序。在显示的表单中,用户输入 Babelfish 做为应用程序名,http://016a0331.ngrok.io/event
做为事件的 URL,http://016a0331.ngrok.io/ncco
做为应答 URL。而后用户单击 Generate public/private key pair
连接,在提示时保存密钥,最后单击建立应用程序。编号设置的最后一步是将你以前购买的编号连接到你的应用程序。使用应用程序仪表板来连接编号。
咱们须要设置的另外一个服务是微软的语音服务 API。在 azure.com 上注册一个免费的微软 Azure 帐号,而后跳转到 portal.azure.com,建立一个语音翻译 API 资源。你须要它为下一步生成的密钥。
设置微软语音翻译 API 的用户屏幕截图。用户在微软 Azure 上的市场搜索中输入语言翻译。而后,用户单击出现的语音翻译 API 选项,再单击 API 概述屏幕上的建立按钮。以后,用户填写资源的表单,使用 babelfish 做为应用程序名,Pay-as-you-go 做为订阅,F0(10 小时的音频输入)做为订价层,babelfish —— 资源做为资源组组名。选中用户已经“阅读并理解注意事项”的选框,并检查添加到仪表板,用户单击建立并重定向到仪表板。部署完成后,用户单击已部署的资源,会显示一个资源仪表板。在资源仪表板中,抓取用户单击的键并复制键 1。
咱们如今有了本身的 Nexmo number 和语音翻译 API 密钥。咱们如今要作的就是去设置一个包含全部这些重要细节的密码和配置,这样咱们就没必要继续编辑它们,能够对它们进行单独的管理。将下列内容存储在你项目目录的 secrets.py
中,而后用你的值来替换占位符值。
# 用你的值替换下面的值
# 你的 API 密钥和密码能够在这里找到 https://dashboard.nexmo.com/getting-started-guide
NEXMO_API_KEY = "<your-api-key>"
NEXMO_API_SECRET = "<your-api-secret>"
# 你的 nexmo 编号
NEXMO_NUMBER = "+447512345678"
# 这能够在你的 Nexmo 应用程序面板上找到
NEXMO_APPLICATION_ID = "<nexmo-application-id>"
# 这是设置你的应用程序时下载的私钥
NEXMO_PRIVATE_KEY = '''-----BEGIN PRIVATE KEY----- <your-private-key> -----END PRIVATE KEY-----'''
# 你必须注册一个免费的微软帐号才能使用微软语音翻译 API:http://docs.microsofttranslator.com/speech-translate.html
MICROSOFT_TRANSLATION_SPEECH_CLIENT_SECRET = "<your-api-key>"
复制代码
以后,在你的项目目录中,在 config.py
中存储如下内容,而后再用值替换占位符值。注意,你也能够选择如下语言的其余语言。你也能够在以后的任意时间更改这些内容。
HOSTNAME = '<your-value>.ngrok.io'
# 用相同格式的数字替换变量赋值
CALLER = '447812345678'
# 用语言替换变量值
LANGUAGE1 = 'de-DE'
# 将变量赋值替换为你的语言的相应名称。能够在这里找到:
# https://developer.nexmo.com/api/voice/ncco#voice-names
VOICE1 = 'Marlene'
# 其余语言和语音
LANGUAGE2 = 'en-US'
VOICE2 = 'Kimberly'
复制代码
咱们将首先介绍如何使用语言翻译 API 进行身份认证。而后咱们将使用提供的模版来设置咱们的 Tornado Web 服务器。以后,咱们要实现 CallHandler
、EventHandler
以及 WSHandler
。CallHandler
将为咱们处理 Nexmo number 的调用。在此基础上,EventHandler
将被用于处理 Nexmo 发送的事件,例如开始或完成的调用。在每一个事件中,Nexmo 都会发送关于启动或完成调用的执行者的信息。咱们会使用这些信息来存储特定调用中的人。WSHandler
同时被用来打开 WebSocket,Nexmo 和咱们的 Python 服务器经过它进行通讯。Python 服务器将建立音频片断并将其发送到语言翻译 API。处理器将使用 EventHandler
收集信息来正确地路由。下面的部分会进一步解释这些概念,并显示相应的实现。
要使用语音翻译 API,咱们须要一个名为 MICROSOFT_TRANSLATION_SPEECH_CLIENT_SECRET
的 token。幸运的是,微软提供了一个 Python AzureAuthClient,咱们会使用它,不会作任何更改。请将如下内容复制并保存到你的项目目录中名为 azure_auth_client.py
的文件中。
""" 从 Azure 平台获取示例 A 的代码。 访问 http://docs.microsofttranslator.com/oauth-token.html 来查看 微软 Azure 认知服务的身份验证服务 API 参考资料。 """
from datetime import timedelta
from datetime import datetime
import requests
class AzureAuthClient(object):
""" Provides a client for obtaining an OAuth token from the authentication service for Microsoft Translator in Azure Cognitive Services. """
def __init__(self, client_secret):
""" :param client_secret: Client secret. """
self.client_secret = client_secret
# token field is used to store the last token obtained from the token service
# the cached token is re-used until the time specified in reuse_token_until.
self.token = None
self.reuse_token_until = None
def get_access_token(self):
''' Returns an access token for the specified subscription. This method uses a cache to limit the number of requests to the token service. A fresh token can be re-used during its lifetime of 10 minutes. After a successful request to the token service, this method caches the access token. Subsequent invocations of the method return the cached token for the next 5 minutes. After 5 minutes, a new token is fetched from the token service and the cache is updated. '''
if (self.token is None) or (datetime.utcnow() > self.reuse_token_until):
token_service_url = 'https://api.cognitive.microsoft.com/sts/v1.0/issueToken'
request_headers = {'Ocp-Apim-Subscription-Key': self.client_secret}
response = requests.post(token_service_url, headers=request_headers)
response.raise_for_status()
self.token = response.content
self.reuse_token_until = datetime.utcnow() + timedelta(minutes=5)
return self.token
复制代码
计算机通讯协议 WebSocket 容许咱们在一个 TCP 链接中拥有一个双向通讯管道。Nexmo 的 Voice API 容许你将电话调用连接到这样的 WebScoket 端点。咱们会使用 Tornado Web 服务器 web 框架来实现咱们的 WebSocket 协议。
若是你一直按照步骤来,并且全部的文件都如咱们所描述的建立,那么你能够从下面的 Tornado Web 服务器配置开始。这个代码会处理全部的导入,配置 Nexmo 客户端以及 azure auth 客户端,并使用 5000 端口启动服务器。注意这个服务器目前还未执行任何有用操做。它有 3 个端点:ncco
、event
和 socket
,它们会分别调用 CallHandler
、EventHandler
和 WSHandler
。咱们会在下面的部分实现处理器。
在你的项目文件夹中建立一个名为 main.py
的文件,并将如下代码复制进去。
from string import Template
import json
import os
import requests
import struct
import StringIO
from tornado import httpserver, httpclient, ioloop, web, websocket, gen
from xml.etree import ElementTree
import nexmo
from azure_auth_client import AzureAuthClient
from config import HOSTNAME, CALLER, LANGUAGE1, VOICE1, LANGUAGE2, VOICE2
from secrets import NEXMO_APPLICATION_ID, NEXMO_PRIVATE_KEY, MICROSOFT_TRANSLATION_SPEECH_CLIENT_SECRET, NEXMO_NUMBER
nexmo_client = nexmo.Client(application_id=NEXMO_APPLICATION_ID, private_key=NEXMO_PRIVATE_KEY)
azure_auth_client = AzureAuthClient(MICROSOFT_TRANSLATION_SPEECH_CLIENT_SECRET)
conversation_id_by_phone_number = {}
call_id_by_conversation_id = {}
class CallHandler(web.RequestHandler):
@web.asynchronous
def get(self):
self.write("Hello world")
class EventHandler(web.RequestHandler):
@web.asynchronous
def post(self):
self.write("Hello world")
class WSHandler(websocket.WebSocketHandler):
def open(self):
print("WebSocket opened")
def on_message(self, message):
self.write_message(u"You said: " + message)
def on_close(self):
print("WebSocket closed")
def main():
application = web.Application([
(r"/event", EventHandler),
(r"/ncco", CallHandler),
(r"/socket", WSHandler),
])
http_server = httpserver.HTTPServer(application)
port = int(os.environ.get("PORT", 5000))
http_server.listen(port)
print("Running on port: " + str(port))
ioloop.IOLoop.instance().start()
if __name__ == "__main__":
main()
复制代码
为了将电话调用链接到 WebSocket 端点,Nexmo 的 Voice API 使用 Nexmo Call Control Object (NCCO) 或 API 调用。当有人调用你的 Nexmo number 时,Nexmo 就会向你在设置 Nexmo Voice 应用程序时提供的 URL 发起 GET 请求。咱们将应用程序指向服务器,服务器如今须要经过 NCCO
来响应这个请求。这个 NCCO
应该指示 Nexmo 给调用者发生一个简短的欢迎消息,而后将调用者链接到 WebSocket。
显示用户、Nexmo 以及 web 服务器之间的交互图。当用户调用 Nexmo number 时,Nexmo 就会向 web 服务器/ncco 发送一个 GET 请求。web 服务器会指示 Nexmo 打开自身的 socket 来让 NCCO 进行响应。
接着将如下的 NCCO
保存到你项目中名为 ncco.json
的文件中。它包含执行请求动做所需的模版。可是,它也包括一些咱们之后使用时须要替换的占位符变量($hostname
、$whoami
和 $cid
)。
[
{
"action": "talk",
"text": "Please wait while we connect you."
},
{
"action": "connect",
"eventUrl": [
"http://$hostname/event"
],
"from": "12345",
"endpoint": [
{
"type": "websocket",
"uri" : "ws://$hostname/socket",
"content-type": "audio/l16;rate=16000",
"headers": {
"whoami": "$whoami",
"cid": "$cid"
}
}
]
}
]
复制代码
在服务器模版中,如下再现部分设置了 /ncco
端点和 CallHandler
之间的映射。这个映射确保了在 /ncco
接收到 GET 请求时,CallHandler
的 get 方法由服务器执行。
application = web.Application([
(r"/event", EventHandler),
(r"/ncco", CallHandler),
(r"/socket", WSHandler),
])
复制代码
当服务器执行方法时,它会使用如下代码返回一个组装的 NCCO
。首先,咱们从 data
变量中查询(即 GET 请求)收集数据。咱们还存储conversation_uuid
,以便以后的使用。在这种状况下,有一个打印语句,能够在你测试服务器时看见 conversation_uuid
。接下来,代码从咱们建立的 ncco.json
文件中加载 NCCO
。为了完成加载 NCCO
,咱们用从数据变量中手机的值替换占位符变量($hostname
、$cid
和 $whoami
)。替换以后,咱们已经准备好将其返回给 Nexmo 了。
将上述模版中的 CallHandler
替换为如下代码:
class CallHandler(web.RequestHandler):
@web.asynchronous
def get(self):
data={}
data['hostname'] = HOSTNAME
data['whoami'] = self.get_query_argument('from')
data['cid'] = self.get_query_argument('conversation_uuid')
conversation_id_by_phone_number[self.get_query_argument('from')] = self.get_query_argument('conversation_uuid')
print(conversation_id_by_phone_number)
filein = open('ncco.json')
src = Template(filein.read())
filein.close()
ncco = json.loads(src.substitute(data))
self.write(json.dumps(ncco))
self.set_header("Content-Type", 'application/json; charset="utf-8"')
self.finish()
复制代码
不管什么时候,只要有人调用 Nexmo number,Nexmo 就会向咱们的 /ncco
端点发送 GET 请求,CallHandler
将组装并发送 NCCO
。Nexmo 以后会执行 NCCO
中所设计的动做。在这种状况下,这意味着调用者会听到**“请稍侯,咱们正在与你创建链接。”**以后,Nexmo 会尝试将调用链接到提供的 socket
端点。它也会提供了 Nexmo 要使用的 event
端点。若是你如今经过在终端窗口运行 python main.py
来启动服务器,你就会发现你能听到消息,但调用会在消息以后结束。这是由于咱们没有实现 EventHandler
或 WSHandler
。咱们开始实现吧!
EventHandler
处理 Nexmo 发送的时间。咱们对任何调用都感兴趣,所以咱们会检查任何请求,以肯定其主体是否包含 direction
,以及该目录是否为 incoming
。若是是的话,咱们会存储 uuid 并完成上下文请求。call_id_by_conversation_id
字典将用于 WSHandler
中调用方之间的消息路由。
用如下模版代码替换 EventHandler
:
class EventHandler(web.RequestHandler):
@web.asynchronous
def post(self):
body = json.loads(self.request.body)
if 'direction' in body and body['direction'] == 'inbound':
if 'uuid' in body and 'conversation_uuid' in body:
call_id_by_conversation_id[body['conversation_uuid']] = body['uuid']
self.content_type = 'text/plain'
self.write('ok')
self.finish()
复制代码
CallHandler
和 EventHandler
容许咱们的应用程序来设置调用。WSHandler
如今将关注调用的音频流。语音的主调用者将经过语音翻译 API 转录并翻译,结果文本将由另外一端的 Nexmo 语音说出。所以第二我的就能够用他们所明白的语言来倾听调用者的语音了,而后再做出响应。语言翻译 API 将依次翻译响应,以便第一格人能够听到他们的语言。这个工做流就是咱们要实现的部分。
当 Nexmo Voice API 链接 WebSocket 时,Nexmo 会向端点发送一个初始化的 HTTP GET 请求。咱们的服务器响应 HTTP 101 来切换协议,服务器以后会使用 TCP 链接 Nexmo。链接会经过 Tornado 来为咱们处理升级。不管什么时候有人调用 Nexmo number,Nexmo 都会在调用期间打开 WebSocket。当 WebSocket 被打开而且最后被关闭时,Tornado 框架将调用下面的 open
和 close
方法。咱们不须要在这两种状况下作任何事情,但咱们会打印消息,这样咱们就能够在服务器运行时跟踪掌握所发生的一切。
如今咱们打开一个链接,Nexmo 会在 on_message
方法中处理咱们发送的信息。咱们从 Nexmo 收到第一个消息是带有元数据的纯文本。在收到这一消息后,咱们会设置 WSHandler
的 whoami
属性,以便能识别发言人。以后,咱们会建立一个咱们发送到语音翻译 API 的 wave 标题。为了向语音翻译 API 发送消息,咱们将建立一个 translator_future
。根据调用者的不一样,例如,消息来源,咱们将使用相应的语言变量建立 translator_future
,以便 API 了解从哪一种语言翻译成哪一种其余的语言。
translator_future
是链接到语音翻译 API 的另外一个 WebSocket。咱们使用它来传递咱们从 Nexmo Voice API 接收到的消息。在它建立以后,translator_future
被存储在变量 ws
中,被用来发送咱们以前建立的 wave 标题。来自 Nexmo 的每一个后续消息都是二进制消息。这些二进制消息使用 translator_future
传递语音翻译 API,它会处理音频并返回转录的翻译。
当咱们初始化 translator_future
时,咱们声明语言翻译 API 处理咱们时,它应该会调用 speech_to_translation_completed
方法。这个方法在接收到消息后,会检查消息是否为空,而后以消息接收语言语音出消息内容。它只会对其余调用者说出消息,而不是最初说话的人。此外,咱们还会将翻译内容打印到终端。
将模版中的 WSHandler
替换为如下代码:
class WSHandler(websocket.WebSocketHandler):
whoami = None
def open(self):
print("Websocket Call Connected")
def translator_future(self, translate_from, translate_to):
uri = "wss://dev.microsofttranslator.com/speech/translate?from={0}&to={1}&api-version=1.0".format(translate_from[:2], translate_to)
request = httpclient.HTTPRequest(uri, headers={
'Authorization': 'Bearer ' + azure_auth_client.get_access_token(),
})
return websocket.websocket_connect(request, on_message_callback=self.speech_to_translation_completed)
def speech_to_translation_completed(self, new_message):
if new_message == None:
print("Got None Message")
return
msg = json.loads(new_message)
if msg['translation'] != '':
print("Translated: " + "'" + msg['recognition'] + "' -> '" + msg['translation'] + "'")
for key, value in conversation_id_by_phone_number.iteritems():
if key != self.whoami and value != None:
if self.whoami == CALLER:
speak(call_id_by_conversation_id[value], msg['translation'], VOICE2)
else:
speak(call_id_by_conversation_id[value], msg['translation'], VOICE1)
@gen.coroutine
def on_message(self, message):
if type(message) == str:
ws = yield self.ws_future
ws.write_message(message, binary=True)
else:
message = json.loads(message)
self.whoami = message['whoami']
print("Sending wav header")
header = make_wave_header(16000)
if self.whoami == CALLER:
self.ws_future = self.translator_future(LANGUAGE1, LANGUAGE2)
else:
self.ws_future = self.translator_future(LANGUAGE2, LANGUAGE1)
ws = yield self.ws_future
ws.write_message(header, binary=True)
@gen.coroutine
def on_close(self):
print("Websocket Call Disconnected")
复制代码
咱们使用名为 make_wave_header
的函数来建立语言翻译 API 所指望的标题。用于建立 WAV 头的代码复制于 Python-Speech-Translate 项目,以下简介。
将 make_wave_header
函数复制到 main.py
文件末尾:
def make_wave_header(frame_rate):
""" Generate WAV header that precedes actual audio data sent to the speech translation service. :param frame_rate: Sampling frequency (8000 for 8kHz or 16000 for 16kHz). :return: binary string """
if frame_rate not in [8000, 16000]:
raise ValueError("Sampling frequency, frame_rate, should be 8000 or 16000.")
nchannels = 1
bytes_per_sample = 2
output = StringIO.StringIO()
output.write('RIFF')
output.write(struct.pack('<L', 0))
output.write('WAVE')
output.write('fmt ')
output.write(struct.pack('<L', 18))
output.write(struct.pack('<H', 0x0001))
output.write(struct.pack('<H', nchannels))
output.write(struct.pack('<L', frame_rate))
output.write(struct.pack('<L', frame_rate * nchannels * bytes_per_sample))
output.write(struct.pack('<H', nchannels * bytes_per_sample))
output.write(struct.pack('<H', bytes_per_sample * 8))
output.write(struct.pack('<H', 0))
output.write('data')
output.write(struct.pack('<L', 0))
data = output.getvalue()
output.close()
return data
复制代码
最后,上述说起的 speak
函数实际上是在 nexmo_client
方法 send_speech
周围的进行的简单封装。正如你在下面所看到的那样,它会打印打印一些在运行代码时可能对你有用的信息,而后使用 Nexmo API 指示 Nexmo 使用给定的 voice_name
来播放 text
。
将下列 speak
函数复制到你的 main.py
文件末尾。
def speak(uuid, text, vn):
print("speaking to: " + uuid + " " + text)
response = nexmo_client.send_speech(uuid, text=text, voice_name=vn)
print(response)
复制代码
若是你一直是按照步骤作的,那么如今应该已经成功构建了本身的 Babel Fish!若是你没有遵循步骤,也能够在这里找到源代码。
经过在终端中输入 python main.py
来运行。如今和别人合做(或者使用两部手机)。从两条线上拨打你的 Nexmo 号码。你应该能够听到欢迎信息,而后就能够用你选择的两种语音进行交流了。
咱们归纳一下:咱们首先配置了环境, Nexmo 应用程序和微软的语言翻译 API。而后构建了本身的 Tornado WebServer,它容许咱们使用 WebSocket 来处理语音调用,能够将语音调用的语音传递给语音翻译 API。API 为咱们翻译并转录语音。获得结果后,咱们用新语言说出信息。咱们的路由逻辑使得咱们的服务能够处理双向调用,即咱们的服务在链接两个调用者后,会先翻译任何一我的的语音以确保他们彼此能够选择彼此须要的语言来进行沟通。
咱们如今作到了。咱们正在运行的 Babel Fish!恐怕咱们的 DIY Babel Fish 并不会像电影中的那样可爱,但这是一种可行性的选择。
若是你有任何疑问,请联系 @naomi_pen 或在 naomi.codes 上找我。
若是你对此有深刻了解的兴趣,那么为何不实现容许用户在调用开始时能够选择语言的逻辑呢?这种逻辑也可能会消除咱们硬编码主要电话号码的必要性。对于一个有趣的项目来讲,你也能够探索为电话会议工做以及为每一个电话建立记录。最后,我设想你可能想要确保你本身服务的安全性以及不让任何人都有机会调用你的服务。你能够经过只容许某个号码(或多个)来使用你的服务,或者使用第二阶段的内部调用的逻辑来容许你邀请没有给定 Bable Fish 服务权限的用户。我很想知道你在 Twitter 上构建的内容 —— @naomi_pen!
若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。