ibm-watson
Version:
Client library to use the IBM Watson Services
214 lines (213 loc) • 10.4 kB
JavaScript
/**
* (C) Copyright IBM Corp. 2018, 2023.
*
* 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
*/
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var ibm_cloud_sdk_core_1 = require("ibm-cloud-sdk-core");
var stream_1 = require("stream");
var websocket_1 = require("websocket");
var websocket_utils_1 = require("./websocket-utils");
/**
* pipe()-able Node.js Readable stream - accepts text in the constructor and emits binary audio data in its 'message' events
*
* Cannot be instantiated directly, instead created by calling #synthesizeUsingWebSocket()
*
* Uses WebSockets under the hood.
* @param {Object} options
* @constructor
*/
var SynthesizeStream = /** @class */ (function (_super) {
__extends(SynthesizeStream, _super);
/**
* pipe()-able Node.js Readable stream - accepts text and emits binary audio data in its 'message' events
*
* Uses WebSockets under the hood.
*
*
* Note that the WebSocket connection is not established until the first chunk of data is recieved. This allows for IAM token request management by the SDK.
*
* @param {Options} options
* @param {Authenticator} options.authenticator - Authenticator to add Authorization header
* @param {string} [options.serviceUrl] - Base url for service (default='wss://api.us-south.speech-to-text.watson.cloud.ibm.com')
* @param {OutgoingHttpHeaders} [options.headers] - Only works in Node.js, not in browsers. Allows for custom headers to be set, including an Authorization header (preventing the need for auth tokens)
* @param {boolean} [options.disableSslVerification] - If true, disable SSL verification for the WebSocket connection (default=false)
* @param {Agent} [options.agent] - custom http(s) agent, useful for using the sdk behind a proxy (Node only)
* @param {string} options.text - The text that us to be synthesized
* @param {string} options.accept - The requested format (MIME type) of the audio
* @param {string[]} [options.timings] - An array that specifies whether the service is to return word timing information for all strings of the input text
* @param {string} [options.accessToken] - Bearer token to put in query string
* @param {string} [options.watsonToken] - Valid Watson authentication token (for Cloud Foundry)
* @param {string} [options.voice] - The voice to use for the synthesis (default='en-US_MichaelVoice')
* @param {string} [options.customizationId] - The customization ID (GUID) of a custom voice model that is to be used for the synthesis
* @param {number} [options.ratePercentage] - The percentage change from the default speaking rate of the voice that is used for speech synthesis.
* @param {number} [options.pitchPercentage] - The percentage change from the default speaking pitch of the voice that is used for speech synthesis.
* @param {boolean} [options.xWatsonLearningOptOut] - Indicates whether IBM can use data that is sent over the connection to improve the service for future users (default=false)
* @param {string} [options.xWatsonMetadata] - Associates a customer ID with all data that is passed over the connection. The parameter accepts the argument customer_id={id}, where {id} is a random or generic string that is to be associated with the data
* @constructor
*/
function SynthesizeStream(options) {
var _this = _super.call(this, options) || this;
_this.options = options;
_this.initialized = false;
_this.authenticator = options.authenticator;
return _this;
}
SynthesizeStream.prototype.initialize = function () {
var options = this.options;
// process query params
var queryParamsAllowed = [
'access_token',
'watson-token',
'voice',
'customization_id',
'rate_percentage',
'pitch_percentage',
'x-watson-learning-opt-out',
'x-watson-metadata',
];
var queryParams = (0, websocket_utils_1.processUserParameters)(options, queryParamsAllowed);
var queryString = ibm_cloud_sdk_core_1.qs.stringify(queryParams);
// synthesize the url
var url = (options.serviceUrl || 'wss://api.us-south.text-to-speech.watson.cloud.ibm.com')
.replace(/^http/, 'ws') +
'/v1/synthesize?' +
queryString;
// add custom agent in the request options if given by user
// default request options to null
var agent = options.agent;
var requestOptions = agent ? { agent: agent } : null;
var socket = (this.socket = new websocket_1.w3cwebsocket(url, null, null, options.headers, requestOptions, { tlsOptions: { rejectUnauthorized: !options.disableSslVerification } }));
// use class context within arrow functions
var self = this;
socket.onopen = function () {
// process the payload params
var payloadParamsAllowed = [
'text',
'accept',
'timings',
];
var payload = (0, websocket_utils_1.processUserParameters)(options, payloadParamsAllowed);
socket.send(JSON.stringify(payload));
/**
* emitted once the WebSocket connection has been established
* @event SynthesizeStream#open
*/
self.emit('open');
};
socket.onmessage = function (message) {
var chunk = message.data;
// some info messages are sent as strings, telling the content_type and
// timings. Emit them as separate events, but do not send them along the
// pipe.
if (typeof chunk === 'string') {
try {
var json = JSON.parse(chunk);
if (json['binary_streams']) {
self.emit('binary_streams', message, json);
}
else if (json['marks']) {
self.emit('marks', message, json);
}
else if (json['words']) {
self.emit('words', message, json);
}
else if (json['error']) {
// this should have same structure as onerror emit
var err = new Error(json['error']);
err.name = SynthesizeStream.WEBSOCKET_ERROR;
err['event'] = message;
self.emit('error', err);
}
else if (json['warnings']) {
self.emit('warnings', message, json);
}
}
finally {
self.emit('message', message, chunk);
}
return;
}
/**
* Emit any messages received over the wire, mainly used for debugging.
*
* @event SynthesizeStream#message
* @param {Object} message - frame object received from service
* @param {Object} data - a data attribute of the frame that's a Buffer/TypedArray
*/
var data = Buffer.from(chunk);
self.emit('message', message, data);
self.push(data);
};
socket.onerror = function (event) {
var err = new Error('WebSocket connection error');
err.name = SynthesizeStream.WEBSOCKET_CONNECTION_ERROR;
err['event'] = event;
self.emit('error', err);
self.push(null);
};
socket.onclose = function (event) {
self.push(null);
/**
* @event SynthesizeStream#close
* @param {Number} reasonCode
* @param {String} description
*/
self.emit('close', event.code, event.reason);
};
this.initialized = true;
};
SynthesizeStream.prototype._read = function () {
var _this = this;
// even though we aren't controlling the read from websocket,
// we can take advantage of the fact that _read is async and hack
// this funtion to retrieve a token if the service is using IAM auth
this.authenticator.authenticate(this.options).then(function () {
if (!_this.initialized) {
_this.initialize();
}
}, function (err) {
_this.emit('error', err);
_this.push(null);
});
};
/**
* Returns a Promise that resolves with Watson Transaction ID from the X-Transaction-ID header
*
* Works in Node.js but not in browsers (the W3C WebSocket API does not expose headers)
*
* @return Promise<String>
*/
SynthesizeStream.prototype.getTransactionId = function () {
return (0, websocket_utils_1.extractTransactionId)(this);
};
SynthesizeStream.WEBSOCKET_ERROR = 'WebSocket error';
SynthesizeStream.WEBSOCKET_CONNECTION_ERROR = 'WebSocket connection error';
return SynthesizeStream;
}(stream_1.Readable));
module.exports = SynthesizeStream;
;