presidium 0.15.12 → 0.15.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +1 -1
- package/.github/workflows/nodejs.yml +3 -0
- package/DynamoStream.js +30 -12
- package/DynamoStream.test.js +3 -46
- package/DynamoTable.js +29 -7
- package/DynamoTable.test.js +68 -2
- package/TranscribeStreaming.js +276 -0
- package/TranscribeStreaming.test.js +122 -0
- package/internal/AmzCanonicalHeaders.js +18 -0
- package/internal/AmzDate.js +14 -0
- package/internal/AmzSignature.js +35 -0
- package/internal/AmzSignedHeaders.js +13 -0
- package/internal/AwsCredentials.js +32 -0
- package/internal/AwsPresignedUrlV4.js +111 -0
- package/internal/Crc32.js +93 -0
- package/internal/sha256.js +1 -1
- package/media-stream-fixture-aws-keynote.txt +366 -0
- package/package.json +3 -2
package/.eslintrc.js
CHANGED
|
@@ -38,3 +38,6 @@ jobs:
|
|
|
38
38
|
with:
|
|
39
39
|
node-version: ${{ matrix.node-version }}
|
|
40
40
|
- run: npm i && npx nyc --reporter=lcovonly npm test && npx codecov --token=3586c688-30cd-46be-a6c4-79a6e0f7fe80 --file=coverage/lcov.info && npm run lint
|
|
41
|
+
env:
|
|
42
|
+
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
|
43
|
+
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
package/DynamoStream.js
CHANGED
|
@@ -60,6 +60,7 @@ const DynamoStream = function (options) {
|
|
|
60
60
|
this.shardIteratorType = options.shardIteratorType ?? 'LATEST'
|
|
61
61
|
this.shardUpdatePeriod = options.shardUpdatePeriod ?? 30000
|
|
62
62
|
this.listStreamsLimit = options.listStreamsLimit ?? 100
|
|
63
|
+
this.debug = options.debug ?? false
|
|
63
64
|
this.client = new DynamoDBStreams({
|
|
64
65
|
apiVersion: '2012-08-10',
|
|
65
66
|
accessKeyId: 'id',
|
|
@@ -129,12 +130,14 @@ DynamoStream.prototype.getRecords = async function* getRecords(
|
|
|
129
130
|
ShardId: Shard.ShardId,
|
|
130
131
|
StreamArn: Shard.Stream.StreamArn,
|
|
131
132
|
ShardIteratorType: Shard.ShardIteratorType,
|
|
133
|
+
|
|
134
|
+
/*
|
|
132
135
|
...(
|
|
133
|
-
|
|
134
|
-
||
|
|
135
|
-
) ? {
|
|
136
|
-
|
|
137
|
-
|
|
136
|
+
Shard.ShardIteratorType == 'AFTER_SEQUENCE_NUMBER'
|
|
137
|
+
|| Shard.ShardIteratorType == 'AT_SEQUENCE_NUMBER'
|
|
138
|
+
) ? { SequenceNumber: Shard.SequenceNumber } : {},
|
|
139
|
+
*/
|
|
140
|
+
|
|
138
141
|
}).promise().then(get('ShardIterator'))
|
|
139
142
|
let records = await this.client.getRecords({
|
|
140
143
|
ShardIterator: startingShardIterator,
|
|
@@ -156,15 +159,22 @@ const SymbolUpdateShards = Symbol('UpdateShards')
|
|
|
156
159
|
|
|
157
160
|
DynamoStream.prototype[Symbol.asyncIterator] = async function* () {
|
|
158
161
|
let shards = await pipe([
|
|
159
|
-
|
|
162
|
+
always(this.getStreams()),
|
|
160
163
|
flatMap(Stream => this.getShards(Stream)),
|
|
161
|
-
map(assign({
|
|
162
|
-
|
|
164
|
+
map(assign({
|
|
165
|
+
ShardIteratorType: always(this.shardIteratorType),
|
|
166
|
+
})),
|
|
167
|
+
transform(map(identity), []),
|
|
168
|
+
])()
|
|
163
169
|
let muxAsyncIterator = Mux.race(shards.map(Shard => this.getRecords(Shard)))
|
|
164
170
|
let iterationPromise = muxAsyncIterator.next()
|
|
165
171
|
let shardUpdatePromise = new Promise(resolve => setTimeout(
|
|
166
172
|
thunkify(resolve, SymbolUpdateShards), this.shardUpdatePeriod))
|
|
167
173
|
|
|
174
|
+
if (this.debug) {
|
|
175
|
+
console.log('Starting shards:', shards.map(get('ShardId')))
|
|
176
|
+
}
|
|
177
|
+
|
|
168
178
|
while (!this.closed) {
|
|
169
179
|
const iteration = await Promise.race([
|
|
170
180
|
shardUpdatePromise,
|
|
@@ -172,16 +182,24 @@ DynamoStream.prototype[Symbol.asyncIterator] = async function* () {
|
|
|
172
182
|
])
|
|
173
183
|
if (iteration == SymbolUpdateShards) {
|
|
174
184
|
const latestShards = await pipe([
|
|
175
|
-
|
|
185
|
+
always(this.getStreams()),
|
|
176
186
|
flatMap(Stream => this.getShards(Stream)),
|
|
177
|
-
|
|
187
|
+
transform(map(identity), []),
|
|
188
|
+
])()
|
|
178
189
|
const newShards = pipe([
|
|
190
|
+
always(shards),
|
|
179
191
|
differenceWith(
|
|
180
192
|
(ShardA, ShardB) => ShardA.ShardId == ShardB.ShardId,
|
|
181
193
|
latestShards,
|
|
182
194
|
),
|
|
183
|
-
map(assign({
|
|
184
|
-
|
|
195
|
+
map(assign({
|
|
196
|
+
ShardIteratorType: always('TRIM_HORIZON'),
|
|
197
|
+
})),
|
|
198
|
+
])()
|
|
199
|
+
|
|
200
|
+
if (this.debug) {
|
|
201
|
+
console.log('Latest shards:', latestShards.map(get('ShardId')))
|
|
202
|
+
}
|
|
185
203
|
|
|
186
204
|
shards = latestShards
|
|
187
205
|
muxAsyncIterator = newShards.length == 0 ? muxAsyncIterator : Mux.race([
|
package/DynamoStream.test.js
CHANGED
|
@@ -226,52 +226,7 @@ const test = Test('DynamoStream', DynamoStream)
|
|
|
226
226
|
.case({
|
|
227
227
|
table: 'my-table',
|
|
228
228
|
endpoint: 'http://localhost:8000',
|
|
229
|
-
|
|
230
|
-
shardIteratorType: 'AFTER_SEQUENCE_NUMBER',
|
|
231
|
-
debug: true,
|
|
232
|
-
}, async function (myStream) {
|
|
233
|
-
await myStream.ready
|
|
234
|
-
|
|
235
|
-
const table = this.table
|
|
236
|
-
await table.putItem({
|
|
237
|
-
id: '1',
|
|
238
|
-
status: 'waitlist',
|
|
239
|
-
createTime: 1000,
|
|
240
|
-
name: 'George',
|
|
241
|
-
})
|
|
242
|
-
await table.putItem({
|
|
243
|
-
id: '2',
|
|
244
|
-
status: 'waitlist',
|
|
245
|
-
createTime: 1001,
|
|
246
|
-
name: 'geo',
|
|
247
|
-
})
|
|
248
|
-
await table.putItem({
|
|
249
|
-
id: '3',
|
|
250
|
-
status: 'waitlist',
|
|
251
|
-
createTime: 1002,
|
|
252
|
-
name: 'john',
|
|
253
|
-
})
|
|
254
|
-
await table.putItem({
|
|
255
|
-
id: '4',
|
|
256
|
-
status: 'approved',
|
|
257
|
-
createTime: 1003,
|
|
258
|
-
name: 'sally',
|
|
259
|
-
})
|
|
260
|
-
await table.putItem({
|
|
261
|
-
id: '5',
|
|
262
|
-
status: 'approved',
|
|
263
|
-
createTime: 1004,
|
|
264
|
-
name: 'sally',
|
|
265
|
-
})
|
|
266
|
-
|
|
267
|
-
const first5 = await asyncIterableTake(5)(myStream)
|
|
268
|
-
assert.strictEqual(first5.length, 5)
|
|
269
|
-
myStream.close()
|
|
270
|
-
})
|
|
271
|
-
|
|
272
|
-
.case({
|
|
273
|
-
table: 'my-table',
|
|
274
|
-
endpoint: 'http://localhost:8000',
|
|
229
|
+
shardUpdatePeriod: 500,
|
|
275
230
|
}, async function (myStream) {
|
|
276
231
|
await myStream.ready
|
|
277
232
|
|
|
@@ -282,6 +237,8 @@ const test = Test('DynamoStream', DynamoStream)
|
|
|
282
237
|
new Promise(resolve => setTimeout(thunkify(resolve, 'hey'), 3000))
|
|
283
238
|
])
|
|
284
239
|
assert.equal(raceResult, 'hey')
|
|
240
|
+
// wait a second for shard update
|
|
241
|
+
await new Promise(resolve => setTimeout(thunkify(resolve, 'hey'), 1000))
|
|
285
242
|
myStream.close()
|
|
286
243
|
})
|
|
287
244
|
|
package/DynamoTable.js
CHANGED
|
@@ -144,7 +144,10 @@ DynamoTable.prototype.putItem = async function dynamoTablePutItem(item, options)
|
|
|
144
144
|
TableName: this.name,
|
|
145
145
|
Item: map(Dynamo.AttributeValue)(item),
|
|
146
146
|
...options,
|
|
147
|
-
}).promise()
|
|
147
|
+
}).promise().catch(error => {
|
|
148
|
+
error.tableName = this.name
|
|
149
|
+
throw error
|
|
150
|
+
})
|
|
148
151
|
}
|
|
149
152
|
|
|
150
153
|
/**
|
|
@@ -162,9 +165,14 @@ DynamoTable.prototype.getItem = async function dynamoTableGetItem(key) {
|
|
|
162
165
|
Key: map(Dynamo.AttributeValue)(key),
|
|
163
166
|
}).promise().then(result => {
|
|
164
167
|
if (result.Item == null) {
|
|
165
|
-
|
|
168
|
+
const error = new Error(`Item not found for ${stringifyJSON(key)}`)
|
|
169
|
+
error.tableName = this.name
|
|
170
|
+
throw error
|
|
166
171
|
}
|
|
167
172
|
return result
|
|
173
|
+
}).catch(error => {
|
|
174
|
+
error.tableName = this.name
|
|
175
|
+
throw error
|
|
168
176
|
})
|
|
169
177
|
}
|
|
170
178
|
|
|
@@ -223,7 +231,10 @@ DynamoTable.prototype.updateItem = async function dynamoTableUpdateItem(
|
|
|
223
231
|
([key, value]) => [`:${hashJSON(value)}`, Dynamo.AttributeValue(value)],
|
|
224
232
|
)(updates),
|
|
225
233
|
...options,
|
|
226
|
-
}).promise()
|
|
234
|
+
}).promise().catch(error => {
|
|
235
|
+
error.tableName = this.name
|
|
236
|
+
throw error
|
|
237
|
+
})
|
|
227
238
|
}
|
|
228
239
|
|
|
229
240
|
/**
|
|
@@ -263,7 +274,10 @@ DynamoTable.prototype.incrementItem = async function incrementItem(
|
|
|
263
274
|
([key, value]) => [`:${hashJSON(value)}`, Dynamo.AttributeValue(value)],
|
|
264
275
|
)(incrementUpdates),
|
|
265
276
|
...options,
|
|
266
|
-
}).promise()
|
|
277
|
+
}).promise().catch(error => {
|
|
278
|
+
error.tableName = this.name
|
|
279
|
+
throw error
|
|
280
|
+
})
|
|
267
281
|
}
|
|
268
282
|
|
|
269
283
|
/**
|
|
@@ -287,7 +301,10 @@ DynamoTable.prototype.deleteItem = async function dynamoTableDeleteItem(key, opt
|
|
|
287
301
|
TableName: this.name,
|
|
288
302
|
Key: map(Dynamo.AttributeValue)(key),
|
|
289
303
|
...options,
|
|
290
|
-
}).promise()
|
|
304
|
+
}).promise().catch(error => {
|
|
305
|
+
error.tableName = this.name
|
|
306
|
+
throw error
|
|
307
|
+
})
|
|
291
308
|
}
|
|
292
309
|
|
|
293
310
|
/**
|
|
@@ -298,6 +315,7 @@ DynamoTable.prototype.deleteItem = async function dynamoTableDeleteItem(key, opt
|
|
|
298
315
|
* DynamoTable(options).scan(options {
|
|
299
316
|
* limit: number,
|
|
300
317
|
* exclusiveStartKey: Object<string=>DynamoAttributeValue>
|
|
318
|
+
* forceTableName?: string, // a test parameter
|
|
301
319
|
* }) -> Promise<{
|
|
302
320
|
* Items: Array<Object<string=>DynamoAttributeValue>>
|
|
303
321
|
* Count: number, // number of Items
|
|
@@ -307,14 +325,18 @@ DynamoTable.prototype.deleteItem = async function dynamoTableDeleteItem(key, opt
|
|
|
307
325
|
* ```
|
|
308
326
|
*/
|
|
309
327
|
DynamoTable.prototype.scan = async function scan(options = {}) {
|
|
328
|
+
const { forceTableName } = options
|
|
310
329
|
await this.ready
|
|
311
330
|
return this.client.scan({
|
|
312
|
-
TableName: this.name,
|
|
331
|
+
TableName: forceTableName ?? this.name,
|
|
313
332
|
Limit: options.limit ?? 100,
|
|
314
333
|
...options.exclusiveStartKey && {
|
|
315
334
|
ExclusiveStartKey: options.exclusiveStartKey,
|
|
316
335
|
},
|
|
317
|
-
}).promise()
|
|
336
|
+
}).promise().catch(error => {
|
|
337
|
+
error.tableName = this.name
|
|
338
|
+
throw error
|
|
339
|
+
})
|
|
318
340
|
}
|
|
319
341
|
|
|
320
342
|
module.exports = DynamoTable
|
package/DynamoTable.test.js
CHANGED
|
@@ -16,13 +16,37 @@ const test = new Test('DynamoTable', DynamoTable)
|
|
|
16
16
|
endpoint: 'http://localhost:8000/',
|
|
17
17
|
key: [{ id: 'string' }],
|
|
18
18
|
}, async function (testTable) {
|
|
19
|
+
await testTable.ready
|
|
20
|
+
// if we created another instance of testTable it shouldn't have to create now
|
|
21
|
+
await new DynamoTable({
|
|
22
|
+
name: 'test-tablename',
|
|
23
|
+
endpoint: 'http://localhost:8000/',
|
|
24
|
+
key: [{ id: 'string' }],
|
|
25
|
+
}).ready
|
|
26
|
+
|
|
19
27
|
// .case('http://localhost:8000/', 'test-tablename', async function (testTable) {
|
|
20
28
|
await testTable.putItem({ id: '1', name: 'george' })
|
|
21
29
|
await testTable.putItem({ id: '2', name: 'henry' })
|
|
22
30
|
await testTable.putItem({ id: '3', name: 'jude' })
|
|
31
|
+
assert.rejects(
|
|
32
|
+
testTable.putItem({ somekey: 'hey' }),
|
|
33
|
+
{
|
|
34
|
+
name: 'ValidationException',
|
|
35
|
+
message: 'One of the required keys was not given a value',
|
|
36
|
+
tableName: 'test-tablename',
|
|
37
|
+
},
|
|
38
|
+
)
|
|
23
39
|
assert.deepEqual(
|
|
24
40
|
await testTable.getItem({ id: '1' }),
|
|
25
41
|
{ Item: map(Dynamo.AttributeValue)({ id: '1', name: 'george' }) })
|
|
42
|
+
assert.rejects(
|
|
43
|
+
testTable.getItem({ id: 'not-exists' }),
|
|
44
|
+
{
|
|
45
|
+
name: 'Error',
|
|
46
|
+
message: 'Item not found for {"id":"not-exists"}',
|
|
47
|
+
tableName: 'test-tablename',
|
|
48
|
+
},
|
|
49
|
+
)
|
|
26
50
|
assert.deepEqual(
|
|
27
51
|
await testTable.putItem({ id: '1', name: 'george' }, {
|
|
28
52
|
ReturnValues: 'ALL_OLD',
|
|
@@ -51,6 +75,17 @@ const test = new Test('DynamoTable', DynamoTable)
|
|
|
51
75
|
},
|
|
52
76
|
)
|
|
53
77
|
|
|
78
|
+
assert.rejects(
|
|
79
|
+
testTable.updateItem({ id: 'not-exists' }, { a: 1 }, {
|
|
80
|
+
ConditionExpression: 'attribute_exists(id)',
|
|
81
|
+
}),
|
|
82
|
+
{
|
|
83
|
+
name: 'ConditionalCheckFailedException',
|
|
84
|
+
message: 'The conditional request failed',
|
|
85
|
+
tableName: 'test-tablename',
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
|
|
54
89
|
assert.deepEqual(
|
|
55
90
|
await testTable.getItem({ id: '1' }),
|
|
56
91
|
{
|
|
@@ -99,20 +134,51 @@ const test = new Test('DynamoTable', DynamoTable)
|
|
|
99
134
|
),
|
|
100
135
|
{
|
|
101
136
|
name: 'ValidationException',
|
|
102
|
-
message: 'An operand in the update expression has an incorrect data type'
|
|
137
|
+
message: 'An operand in the update expression has an incorrect data type',
|
|
138
|
+
tableName: 'test-tablename',
|
|
103
139
|
},
|
|
104
140
|
)
|
|
105
141
|
|
|
142
|
+
assert.rejects(
|
|
143
|
+
testTable.incrementItem({ id: 'not-exists' }, { a: 1 }, {
|
|
144
|
+
ConditionExpression: 'attribute_exists(id)',
|
|
145
|
+
}),
|
|
146
|
+
{
|
|
147
|
+
name: 'ConditionalCheckFailedException',
|
|
148
|
+
message: 'The conditional request failed',
|
|
149
|
+
tableName: 'test-tablename',
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
|
|
106
153
|
{
|
|
107
154
|
const scanResult1 = await testTable.scan({ limit: 1 })
|
|
108
155
|
const scanResult2 = await testTable.scan({ limit: 2, exclusiveStartKey: scanResult1.LastEvaluatedKey })
|
|
109
|
-
const scanResult3 = await testTable.scan({
|
|
156
|
+
const scanResult3 = await testTable.scan({ exclusiveStartKey: scanResult2.LastEvaluatedKey })
|
|
157
|
+
const bareScanResult = await testTable.scan()
|
|
110
158
|
assert.strictEqual(scanResult1.Items.length, 1)
|
|
111
159
|
assert.strictEqual(scanResult2.Items.length, 2)
|
|
112
160
|
assert.strictEqual(scanResult3.Items.length, 0)
|
|
161
|
+
assert.strictEqual(bareScanResult.Items.length, 3)
|
|
162
|
+
|
|
163
|
+
assert.rejects(
|
|
164
|
+
testTable.scan({ forceTableName: 'nonexistent-table-name' }),
|
|
165
|
+
{
|
|
166
|
+
name: 'ResourceNotFoundException',
|
|
167
|
+
message: 'Cannot do operations on a non-existent table',
|
|
168
|
+
tableName: 'test-tablename',
|
|
169
|
+
},
|
|
170
|
+
)
|
|
113
171
|
}
|
|
114
172
|
|
|
115
173
|
await testTable.deleteItem({ id: '1' })
|
|
174
|
+
assert.rejects(
|
|
175
|
+
testTable.deleteItem({ somekey: 'a' }),
|
|
176
|
+
{
|
|
177
|
+
name: 'ValidationException',
|
|
178
|
+
message: 'One of the required keys was not given a value',
|
|
179
|
+
tableName: 'test-tablename',
|
|
180
|
+
}
|
|
181
|
+
)
|
|
116
182
|
const shouldReject = testTable.getItem({ id: '1' })
|
|
117
183
|
assert.rejects(
|
|
118
184
|
() => shouldReject,
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
const EventEmitter = require('events')
|
|
2
|
+
const rubico = require('rubico')
|
|
3
|
+
const WebSocket = require('./WebSocket')
|
|
4
|
+
const sha256 = require('./internal/sha256')
|
|
5
|
+
const AwsPresignedUrlV4 = require('./internal/AwsPresignedUrlV4')
|
|
6
|
+
const Crc32 = require('./internal/Crc32')
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
pipe, tap,
|
|
10
|
+
switchCase, tryCatch,
|
|
11
|
+
fork, assign, get, pick, omit,
|
|
12
|
+
map, filter, reduce, transform, flatMap,
|
|
13
|
+
and, or, not, any, all,
|
|
14
|
+
eq, gt, lt, gte, lte,
|
|
15
|
+
thunkify, always,
|
|
16
|
+
curry, __,
|
|
17
|
+
} = rubico
|
|
18
|
+
|
|
19
|
+
// All prelude components are unsigned, 32-bit integers
|
|
20
|
+
const PRELUDE_MEMBER_LENGTH = 4
|
|
21
|
+
|
|
22
|
+
// The prelude consists of two components
|
|
23
|
+
const PRELUDE_LENGTH = PRELUDE_MEMBER_LENGTH * 2
|
|
24
|
+
|
|
25
|
+
// Checksums are always CRC32 hashes.
|
|
26
|
+
const CHECKSUM_LENGTH = 4
|
|
27
|
+
|
|
28
|
+
// Messages must include a full prelude, a prelude checksum, and a message checksum
|
|
29
|
+
const MINIMUM_MESSAGE_LENGTH = PRELUDE_LENGTH + CHECKSUM_LENGTH * 2
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @name TranscribeStreaming
|
|
33
|
+
*
|
|
34
|
+
* @synopsis
|
|
35
|
+
* ```coffeescript [specscript]
|
|
36
|
+
* const myTranscribeStream = new TranscribeStreaming(options {
|
|
37
|
+
* accessKeyId: string,
|
|
38
|
+
* secretAccessKey: string,
|
|
39
|
+
* region: string,
|
|
40
|
+
* endpoint: string,
|
|
41
|
+
* languageCode: string,
|
|
42
|
+
* mediaEncoding: string,
|
|
43
|
+
* sampleRate: number,
|
|
44
|
+
* sessionId?: string,
|
|
45
|
+
* vocabularyName?: string,
|
|
46
|
+
* })
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* @description
|
|
50
|
+
* https://docs.aws.amazon.com/TranscribeStreaming/latest/dg/websocket.html
|
|
51
|
+
*
|
|
52
|
+
* `languageCode` - `en-AU`, `en-GB`, `en-US`, `es-US`, `fr-CA`, `fr-FR`, `de-DE`, `ja-JP`, `ko-KR`, `pt-BR`, `zh-CN` or `it-IT`.
|
|
53
|
+
*
|
|
54
|
+
* `mediaEncoding` - `pcm`, `ogg-opus`, or `flac`.
|
|
55
|
+
*
|
|
56
|
+
* `sampleRate` - The sample rate of the input audio in Hertz. We suggest that you use 8,000 Hz for low-quality audio and 16,000 Hz (or higher) for high-quality audio. The sample rate must match the sample rate in the audio file.
|
|
57
|
+
*
|
|
58
|
+
* `sessionId` - id for the transcription session. If you don't provide a session ID, Amazon Transcribe generates one for you and returns it in the response.
|
|
59
|
+
*
|
|
60
|
+
* `vocabularyName` - The name of the vocabulary to use when processing the transcription job, if any.
|
|
61
|
+
*/
|
|
62
|
+
const TranscribeStreaming = function (options) {
|
|
63
|
+
const {
|
|
64
|
+
accessKeyId,
|
|
65
|
+
secretAccessKey,
|
|
66
|
+
region,
|
|
67
|
+
languageCode,
|
|
68
|
+
mediaEncoding,
|
|
69
|
+
sampleRate,
|
|
70
|
+
sessionId,
|
|
71
|
+
vocabularyName,
|
|
72
|
+
} = options
|
|
73
|
+
|
|
74
|
+
const url = AwsPresignedUrlV4({
|
|
75
|
+
accessKeyId,
|
|
76
|
+
secretAccessKey,
|
|
77
|
+
region,
|
|
78
|
+
method: 'GET',
|
|
79
|
+
endpoint: `transcribestreaming.${region}.amazonaws.com:8443`,
|
|
80
|
+
protocol: 'wss',
|
|
81
|
+
canonicalUri: '/stream-transcription-websocket',
|
|
82
|
+
serviceName: 'transcribe',
|
|
83
|
+
payloadHash: sha256(''),
|
|
84
|
+
expires: 300,
|
|
85
|
+
queryParams: {
|
|
86
|
+
'language-code': languageCode,
|
|
87
|
+
'media-encoding': mediaEncoding,
|
|
88
|
+
'sample-rate': sampleRate,
|
|
89
|
+
...sessionId == null ? {} : { 'session-id': sessionId },
|
|
90
|
+
...vocabularyName == null ? {} : { 'vocabulary-name': vocabularyName },
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
this.websocket = new WebSocket(url)
|
|
95
|
+
this.ready = new Promise(resolve => {
|
|
96
|
+
this.websocket.on('open', resolve)
|
|
97
|
+
})
|
|
98
|
+
this.websocket.on('message', chunk => {
|
|
99
|
+
const { headers, body } = unmarshalMessage(chunk)
|
|
100
|
+
if (body.Transcript.Results.length > 0) {
|
|
101
|
+
if (body.Transcript.Results[0].IsPartial) {
|
|
102
|
+
this.emit('partialTranscription', body.Transcript.Results[0])
|
|
103
|
+
} else {
|
|
104
|
+
this.emit('transcription', body.Transcript.Results[0])
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
return this
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
TranscribeStreaming.prototype = EventEmitter.prototype
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @name TranscribeStreaming.prototype.sendAudioChunk
|
|
116
|
+
*
|
|
117
|
+
* @synopsis
|
|
118
|
+
* ```coffeescript [specscript]
|
|
119
|
+
* myTranscribeStream.sendAudioChunk(
|
|
120
|
+
* chunk Buffer, // chunk is binary and assumed to be properly encoded in the specified mediaEncoding
|
|
121
|
+
* ) -> undefined
|
|
122
|
+
* ```
|
|
123
|
+
*
|
|
124
|
+
* @description
|
|
125
|
+
* https://docs.aws.amazon.com/transcribe/latest/dg/event-stream.html
|
|
126
|
+
* https://github.com/aws-samples/amazon-transcribe-comprehend-medical-twilio/blob/main/lib/transcribe-service.js
|
|
127
|
+
*/
|
|
128
|
+
TranscribeStreaming.prototype.sendAudioChunk = function (chunk) {
|
|
129
|
+
const headersBytes = marshalHeaders({
|
|
130
|
+
':message-type': {
|
|
131
|
+
type: 'string',
|
|
132
|
+
value: 'event',
|
|
133
|
+
},
|
|
134
|
+
':event-type': {
|
|
135
|
+
type: 'string',
|
|
136
|
+
value: 'AudioEvent',
|
|
137
|
+
},
|
|
138
|
+
':content-type': {
|
|
139
|
+
type: 'string',
|
|
140
|
+
value: 'application/octet-stream',
|
|
141
|
+
},
|
|
142
|
+
// hey: { type: 'string', value: 'yo' },
|
|
143
|
+
})
|
|
144
|
+
const length = headersBytes.byteLength + chunk.byteLength + 16
|
|
145
|
+
const bytes = new Uint8Array(length)
|
|
146
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
|
147
|
+
const checksum = new Crc32()
|
|
148
|
+
|
|
149
|
+
view.setUint32(0, length, false)
|
|
150
|
+
view.setUint32(4, headersBytes.byteLength, false)
|
|
151
|
+
view.setUint32(8, checksum.update(bytes.subarray(0, 8)).digest(), false)
|
|
152
|
+
bytes.set(headersBytes, 12)
|
|
153
|
+
bytes.set(chunk, headersBytes.byteLength + 12)
|
|
154
|
+
view.setUint32(
|
|
155
|
+
length - 4,
|
|
156
|
+
checksum.update(bytes.subarray(8, length - 4)).digest(),
|
|
157
|
+
false
|
|
158
|
+
)
|
|
159
|
+
this.websocket.send(bytes)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @name marshalHeaders
|
|
164
|
+
*
|
|
165
|
+
* @synopsis
|
|
166
|
+
* ```coffeescript [specscript]
|
|
167
|
+
* marshalHeaders(headers Object<
|
|
168
|
+
* [headerName string]: {
|
|
169
|
+
* type: 'string',
|
|
170
|
+
* value: string,
|
|
171
|
+
* }
|
|
172
|
+
* >) -> headersBytes Uint8Array
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
const marshalHeaders = function (headers) {
|
|
176
|
+
const chunks = []
|
|
177
|
+
for (const headerName in headers) {
|
|
178
|
+
const nameBytes = Buffer.from(headerName, 'utf8')
|
|
179
|
+
const header = headers[headerName]
|
|
180
|
+
chunks.push(Buffer.from([nameBytes.byteLength]))
|
|
181
|
+
chunks.push(nameBytes)
|
|
182
|
+
if (header.type == 'string') {
|
|
183
|
+
chunks.push(marshalStringHeaderValue(header.value))
|
|
184
|
+
} else {
|
|
185
|
+
throw new Error(`Unrecognized header type ${header.type}`)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const headersBytes = new Uint8Array(
|
|
189
|
+
chunks.reduce((total, bytes) => total + bytes.byteLength, 0),
|
|
190
|
+
)
|
|
191
|
+
let index = 0
|
|
192
|
+
for (const chunk of chunks) {
|
|
193
|
+
headersBytes.set(chunk, index)
|
|
194
|
+
index += chunk.byteLength
|
|
195
|
+
}
|
|
196
|
+
return headersBytes
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* @synopsis
|
|
201
|
+
* ```coffeescript [specscript]
|
|
202
|
+
* marshalStringHeaderValue(value string) -> bytes Uint8Array
|
|
203
|
+
* ```
|
|
204
|
+
*/
|
|
205
|
+
const marshalStringHeaderValue = function (value) {
|
|
206
|
+
const buffer = Buffer.from(value, 'utf8')
|
|
207
|
+
const view = new DataView(new ArrayBuffer(3 + buffer.byteLength))
|
|
208
|
+
view.setUint8(0, 7) // string value type
|
|
209
|
+
view.setUint16(1, buffer.byteLength, false)
|
|
210
|
+
const result = new Uint8Array(view.buffer)
|
|
211
|
+
result.set(buffer, 3)
|
|
212
|
+
return result
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* @synopsis
|
|
217
|
+
* ```coffeescript [specscript]
|
|
218
|
+
* unmarshalMessage(chunk ArrayBuffer) -> message {
|
|
219
|
+
* headers: DataView,
|
|
220
|
+
* body: Uint8Array,
|
|
221
|
+
* }
|
|
222
|
+
* ```
|
|
223
|
+
*/
|
|
224
|
+
const unmarshalMessage = function (chunk) {
|
|
225
|
+
const { buffer, byteOffset, byteLength } = chunk
|
|
226
|
+
const view = new DataView(buffer, byteOffset, byteLength)
|
|
227
|
+
const messageLength = view.getUint32(0, false)
|
|
228
|
+
const headerLength = view.getUint32(PRELUDE_MEMBER_LENGTH, false)
|
|
229
|
+
return {
|
|
230
|
+
headers: unmarshalHeaders(new DataView(
|
|
231
|
+
buffer,
|
|
232
|
+
byteOffset + PRELUDE_LENGTH + CHECKSUM_LENGTH,
|
|
233
|
+
headerLength
|
|
234
|
+
)),
|
|
235
|
+
body: JSON.parse(String.fromCharCode.apply(null, new Uint8Array(
|
|
236
|
+
buffer,
|
|
237
|
+
byteOffset + PRELUDE_LENGTH + CHECKSUM_LENGTH + headerLength,
|
|
238
|
+
messageLength - headerLength - (
|
|
239
|
+
PRELUDE_LENGTH + CHECKSUM_LENGTH + CHECKSUM_LENGTH
|
|
240
|
+
)
|
|
241
|
+
))),
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* @synopsis
|
|
247
|
+
* ```coffeescript [specscript]
|
|
248
|
+
* unmarshalHeaders(headersView DataView) -> headers Object
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
const unmarshalHeaders = function (headersView) {
|
|
252
|
+
const headers = {}
|
|
253
|
+
let index = 0
|
|
254
|
+
while (index < headersView.byteLength) {
|
|
255
|
+
const nameLength = headersView.getUint8(index)
|
|
256
|
+
index += 1
|
|
257
|
+
const name = String.fromCharCode.apply(null, new Uint8Array(
|
|
258
|
+
headersView.buffer,
|
|
259
|
+
headersView.byteOffset + index,
|
|
260
|
+
nameLength,
|
|
261
|
+
))
|
|
262
|
+
index += nameLength
|
|
263
|
+
index += 1 // byte for header type, assumed to be all strings for now
|
|
264
|
+
const stringLength = headersView.getUint16(index, false)
|
|
265
|
+
index += 2
|
|
266
|
+
headers[name] = String.fromCharCode.apply(null, new Uint8Array(
|
|
267
|
+
headersView.buffer,
|
|
268
|
+
headersView.byteOffset + index,
|
|
269
|
+
stringLength,
|
|
270
|
+
))
|
|
271
|
+
index += stringLength
|
|
272
|
+
}
|
|
273
|
+
return headers
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
module.exports = TranscribeStreaming
|