我正在尝试使用Go 1.11中亚马逊的新流式转录API。目前亚马逊只提供Java SDK,所以我尝试的是低级方式。
唯一相关的文档是here,但它没有显示端点。我在Java example发现它是https://transcribestreaming.<region>.amazonaws.com
,我正在尝试爱尔兰地区,即https://transcribestreaming.eu-west-1.amazonaws.com
。这是我打开HTTP / 2双向流的代码:
import (
"crypto/tls"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/aws/external"
"github.com/aws/aws-sdk-go-v2/aws/signer/v4"
"golang.org/x/net/http2"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"time"
)
const (
HeaderKeyLanguageCode = "x-amzn-transcribe-language-code" // en-US
HeaderKeyMediaEncoding = "x-amzn-transcribe-media-encoding" // pcm only
HeaderKeySampleRate = "x-amzn-transcribe-sample-rate" // 8000, 16000 ... 48000
HeaderKeySessionId = "x-amzn-transcribe-session-id" // For retrying a session. Pattern: [a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}
HeaderKeyVocabularyName = "x-amzn-transcribe-vocabulary-name"
HeaderKeyRequestId = "x-amzn-request-id"
)
...
region := "eu-west-1"
cfg, err := external.LoadDefaultAWSConfig(aws.Config{
Region: region,
})
if err != nil {
log.Printf("could not load default AWS config: %v", err)
return
}
signer := v4.NewSigner(cfg.Credentials)
transport := &http2.Transport{
TLSClientConfig: &tls.Config{
// allow insecure just for debugging
InsecureSkipVerify: true,
},
}
client := &http.Client{
Transport: transport,
}
signTime := time.Now()
header := http.Header{}
header.Set(HeaderKeyLanguageCode, "en-US")
header.Set(HeaderKeyMediaEncoding, "pcm")
header.Set(HeaderKeySampleRate, "16000")
header.Set("Content-type", "application/json")
// Bi-directional streaming via a pipe.
pr, pw := io.Pipe()
req, err := http.NewRequest(http.MethodPost, "https://transcribestreaming.eu-west-1.amazonaws.com/stream-transcription", ioutil.NopCloser(pr))
if err != nil {
log.Printf("err: %+v", err)
return
}
req.Header = header
_, err = signer.Sign(req, nil, "transcribe", region, signTime)
if err != nil {
log.Printf("problem signing headers: %+v", err)
return
}
// This freezes and ends after 5 minutes with "unexpected EOF".
res, err := client.Do(req)
...
问题是执行请求(client.Do(req)
)冻结五分钟然后以“意外的EOF”错误结束。
我有什么想法我做错了吗?有人在没有Java SDK的情况下成功使用了新的流式转录API吗?
编辑(2019年3月11日):
我再次测试了这个,现在它没有超时,但立即返回200 OK
响应。但是响应主体中有一个“例外”:{"Output":{"__type":"com.amazon.coral.service#SerializationException"},"Version":"1.0"}
我尝试使用io.Pipe
(如上面的代码)打开HTTP2流,并使用文档中描述的JSON主体:
{
"AudioStream": {
"AudioEvent": {
"AudioChunk": ""
}
}
}
结果是一样的。
编辑(2019年3月13日):
正如@gpeng所提到的,从标题中删除content-type
将修复SerializationException
。但是有一个IAM异常,需要将transcription:StartStreamTranscription
权限添加到您的IAM用户。这虽然在AWS IAM控制台中没有任何地方,但必须手动添加为自定义JSON权限:/
还有一个新的/另一个文档文档here显示不正确的host
和一个新的content-type
(不要使用content-type
,请求将返回404)。
删除content-type
,并添加新的权限后,现在我得到一个例外{"Message":"A complete signal was sent without the preceding empty frame."}
。还要永远写入管道块,所以我又被卡住了。新文档中描述的消息与旧文档中的消息不同,现在最终是二进制的,但我不理解它们。有关如何在Go中发送此类HTTP2消息的任何想法?
编辑(第15场,2019年):*
如果您收到有关签名不匹配的HTTP 403错误,请不要设置transfer-encoding
和x-amz-content-sha256
HTTP标头。当我设置它们时,使用AWS SDK的V4签名者签署请求,然后我收到HTTP 403 The request signature we calculated does not match the signature you provided.
尝试不设置内容类型标题,看看你得到了什么回应。我正在尝试做同样的事情(但在Ruby中)并且'修复'了SerializationException
。仍然无法让它工作,但我现在有一个新的错误想想:)
更新:我现在已经开始工作了。我的问题在于签名。如果传递了host
和authority
标题,则在检查签名时,它们将与,
连接并在服务器端被视为host
,因此签名永远不会匹配。这似乎不是AWS方面的正确行为,但它看起来不像是Go中的问题。
我还在和Node.js打架。不明确的文件是in one place它说Content-Type
不应该是application/json
,但in some other place,它使它看起来像有效载荷应编码为application/vnd.amazon.eventstream
。看起来有效负载应该以二进制格式而不是JSON对象仔细格式化,如下所示:
Amazon Transcribe使用称为事件流编码的格式进行流式转录。此格式编码二进制数据,其头信息描述每个事件的内容。您可以将此信息用于不使用Amazon Transcribe SDK调用Amazon Transcribe终结点的应用程序。 Amazon Transcribe使用HTTP / 2协议进行流式转录。流式传输请求的关键组件是:
- 标题框架。这包含请求的HTTP标头,以及Amazon Transcribe用作签名以跟踪数据帧的种子签名的授权标头中的签名。
- 事件流编码中的一个或消息帧。该帧包含元数据和原始音频字节。
- 结束框架。这是具有空主体的事件流编码中的签名消息。
有一个示例函数,显示如何implement all of that using Java可能会如何完成这种编码。
我们已经获得了AWS的高级支持,所以我找到了一个例子并得到了这个
var access_key = process.env.AWS_ACCESS_KEY_ID;
var secret_key = process.env.AWS_SECRET_ACCESS_KEY;
// I changed the region according my account, you can ignore it.
var region = 'eu-west-1';
var url = 'https://transcribestreaming.' + region + '.amazonaws.com';
var myService = 'transcribe';
var myMethod = 'POST';
var myPath = '/stream-transcription';
var query = ''
var crypto = require('crypto-js');
var http2 = require('http2');
var fs = require('fs')
const trimAll = (val) => {
return val.trim().replace(/\s+/g, ' ');
}
// this function gets the Signature Key, see AWS documentation(https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html) for more details
const getSignatureKey = (Crypto, key, dateStamp, regionName, serviceName) => {
var kDate = Crypto.HmacSHA256(dateStamp, "AWS4" + key);
var kRegion = Crypto.HmacSHA256(regionName, kDate);
var kService = Crypto.HmacSHA256(serviceName, kRegion);
var kSigning = Crypto.HmacSHA256("aws4_request", kService);
return kSigning;
}
// this function converts the generic JS ISO8601 date format to the specific format the AWS API wants
const getAmzDate = (dateStr) => {
var chars = [":", "-"];
for (var i = 0; i < chars.length; i++) {
while (dateStr.indexOf(chars[i]) != -1) {
dateStr = dateStr.replace(chars[i], "");
}
}
dateStr = dateStr.split(".")[0] + "Z";
return dateStr;
}
const getAuthHeaders = () => {
// get the various date formats needed to form our request
var amzDate = getAmzDate(new Date().toISOString());
var authDate = amzDate.split("T")[0];
const buf = fs.readFileSync('./test.mp3')
let arraybuffer = Uint8Array.from(buf).buffer;
// Payload needs to be managed on client side
var payload = `{
"AudioStream": {
"AudioEvent": {
"AudioChunk": ${arraybuffer}
}
}
}`;
// get the SHA256 hash value for payload
//var payload = '';
//var hashedPayload = crypto.SHA256(payload).toString();
var cannonicalHeaders = '';
var signedHeaders = [];
var headers = {
':authority': url,
':method': myMethod,
':path': myPath,
'content-type': 'application/vnd.amazon.eventstream',
//'transfer-encoding': 'chunked',
'x-amz-content-sha256': 'STREAMING-AWS4-HMAC-SHA256-EVENTS',
'x-amz-date': amzDate,
//'content-type':'application/json',
//'x-amz-security-token': session_token,
'x-amzn-target': 'com.amazonaws.transcribe.Transcribe.StartStreamTranscription',
'x-amzn-transcribe-language-code': 'en-US',
'x-amzn-transcribe-media-encoding': 'pcm',
'x-amzn-transcribe-sample-rate': '8000'
}
Object.keys(headers).sort().forEach((key) => {
header_key = key.toLowerCase();
if (header_key == ':authority') {
header_key = 'host';
}
if (header_key == 'x-amz-content-sha256' || header_key == 'x-amz-date' || header_key == 'host') {
cannonicalHeaders += header_key + ':' + trimAll(headers[key]) + '\n';
signedHeaders.push(header_key);
}
});
signedHeaders = signedHeaders.join(';');
var cannonicalReq = myMethod + '\n'
+ myPath + '\n'
+ query + '\n'
+ cannonicalHeaders + '\n'
+ signedHeaders + '\n'
+ 'STREAMING-AWS4-HMAC-SHA256-EVENTS';
console.log('\n=== cannonicalReq ===');
console.log(cannonicalReq);
// This is what the Canonical request should look like, you can get it from the 403 error message
// cannonicalReq = `POST
///stream-transcription
//
//host:https://transcribestreaming.eu-west-1.amazonaws.com
//x-amz-content-sha256:STREAMING-AWS4-HMAC-SHA256-EVENTS
//x-amz-date:${amzDate}
//
//host;x-amz-content-sha256;x-amz-date
//STREAMING-AWS4-HMAC-SHA256-EVENTS`
// hash the canonical request
var canonicalReqHash = crypto.SHA256(cannonicalReq).toString();
var stringToSign = 'AWS4-HMAC-SHA256\n'
+ amzDate + '\n'
+ authDate + '/' + region + '/transcribe/aws4_request\n'
+ canonicalReqHash;
console.log('\n=== StringToSign ===');
console.log(stringToSign);
// get our Signing Key
var signingKey = getSignatureKey(crypto, secret_key, authDate, region, myService);
// Sign our String-to-Sign with our Signing Key
var authKey = crypto.HmacSHA256(stringToSign, signingKey);
// Form our authorization header
var authString = 'AWS4-HMAC-SHA256 ' +
'Credential=' +
access_key + '/' +
authDate + '/' +
region + '/' +
myService + '/aws4_request,' +
'SignedHeaders=host;x-amz-content-sha256;x-amz-date,' +
'Signature=' + authKey;
console.log('\n=== authorization in headers ===');
console.log(authString);
headers['authorization'] = authString;
console.log('\n=== headers ===');
console.log('HEADERS:::::::');
console.log(headers);
return {
headers,
payload
};
}
try {
const client = http2.connect(url);
const { headers, payload } = getAuthHeaders();
var data;
client.on('error', (err) => console.error(err))
const req = client.request(headers);
console.log('Main Requestt');
console.log(req);
req.on('response', (headers, flags) => {
// may check and play with the http/2 response headers, and flags
console.dir(headers);
});a
req.setEncoding('utf8');
console.log(req)
req.on('data', (chunk) => {
// do something with the data
data += new Buffer.from(chunk).toString('ascii');
console.log(new Buffer.from(chunk).toString('ascii'));
});
req.on('end', () => {
client.close();
});
req.write(payload);
req.end();
} catch (error) {
console.error(error);
}
我知道这是NodeJS,但我发布这个作为答案(直到更好的一个出现)为了可见性,因为片段得到很好的评论,并且可能是最佳实践,因为它是由AWS编写的