Amazon Transcribe Streaming API без SDK

Я пытаюсь использовать Amazon новый API потоковой транскрипции из Go 1.11. В настоящее время Amazon предоставляет только Java SDK, поэтому я пробую низкоуровневый способ.

Единственная соответствующая часть документации здесь, но она не показывает конечную точку. Я нашел это в примере Java, что это 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)) client.Do(req) на пять минут, а затем заканчивается ошибкой "неожиданный EOF".

Есть идеи, что я делаю не так? Кто-нибудь успешно использовал новый API потоковой транскрипции без Java SDK?

РЕДАКТИРОВАТЬ (11 марта 2019 г.):

Я проверил это снова, и теперь он не истекает, но сразу возвращает ответ 200 OK. В теле ответа есть "исключение": {"Output":{"__type":"com.amazon.coral.service#SerializationException"},"Version":"1.0"}

Я попытался открыть поток HTTP2 с помощью io.Pipe (как в приведенном выше коде), а также с помощью тела JSON, описанного в документации:

{
    "AudioStream": { 
        "AudioEvent": { 
            "AudioChunk": ""
        }
    }
}

Результат был таким же.

РЕДАКТИРОВАТЬ (13 марта 2019 г.):

Как уже упоминалось @gpeng, удаление content-type из заголовков исправит исключение SerializationException. Но затем возникает исключение IAM, и необходимо добавить разрешение transcription:StartStreamTranscription вашему пользователю IAM. Это, однако, нигде в консоли AWS IAM и должно быть добавлено вручную как пользовательское разрешение JSON:/

Существует также новый/другая документация документ здесь, который показывает неправильный host и новый content-type (не использовать этот content-type, запрос будет возвращать 404 с ней).

После удаления content-type и добавления нового разрешения теперь я получаю исключение {"Message":"A complete signal was sent without the preceding empty frame."}. Также пишу в трубу навсегда, поэтому я снова застрял. Сообщения, описанные в новой документации, отличаются от старых, теперь, наконец, двоичные, но я их не понимаю. Есть идеи, как отправить такие сообщения HTTP2 в Go?

РЕДАКТИРОВАТЬ (Матч 15, 2019): *

Если вы получили ошибку HTTP 403 о несоответствии сигнатур, не устанавливайте HTTP-заголовки x-amz-content-sha256 transfer-encoding и x-amz-content-sha256. Когда я их устанавливаю, подписываю запрос с помощью AWS SDK V4, а затем получаю HTTP 403. The request signature we calculated does not match the signature you provided.

Ответы

Ответ 1

Попробуйте не устанавливать заголовок типа контента и посмотрите, какой ответ вы получите. Я пытаюсь сделать то же самое (но в Ruby) и это "исправило" SerializationException. Я все еще не могу заставить его работать, но у меня теперь есть новая ошибка, чтобы думать о :)

ОБНОВЛЕНИЕ: у меня это работает сейчас. Моя проблема была с подписью. Если оба host и authority передаются заголовки, они соединяются с , и рассматриваются как host на стороне сервера, когда подпись проверяются так подписи не совпадают. Это не похоже на правильное поведение на стороне AWS, но не похоже, что это будет проблемой для вас в Go.

Ответ 2

Я все еще сражаюсь с Node.js. Что не ясно в документах, так это то, что в одном месте написано, что Content-Type не должен быть application/json, но в каком-то другом месте application/vnd.amazon.eventstream впечатление, что полезная нагрузка должна быть закодирована как application/vnd.amazon.eventstream, Похоже, что полезная нагрузка должна быть тщательно отформатирована в двоичном формате вместо объекта JSON следующим образом:

Amazon Transcribe использует формат потокового кодирования событий для потоковой транскрипции. Этот формат кодирует двоичные данные с информацией заголовка, которая описывает содержимое каждого события. Вы можете использовать эту информацию для приложений, которые вызывают конечную точку Amazon Transcribe без использования Amazon Transcribe SDK. Amazon Transcribe использует протокол HTTP/2 для потоковой транскрипции. Ключевые компоненты для запроса потоковой передачи:

  • Фрейм заголовка. Он содержит заголовки HTTP для запроса и подпись в заголовке авторизации, которую Amazon Transcribe использует в качестве исходной подписи для подписи следующих фреймов данных.

  • Один или фреймы сообщения в кодировке потока событий. Кадр содержит метаданные и необработанные аудио байты.

  • Конечная рамка. Это подписанное сообщение в кодировке потока событий с пустым телом.

Существует пример функции, которая показывает, как реализовать все это с помощью Java, которая может пролить некоторый свет на то, как должна выполняться эта кодировка.

Ответ 3

У нас есть премиальная поддержка с 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')
  console.log('####################################################')
  console.log(buf)
  console.log('####################################################')
  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.