speech-to-element
Version:
Add real-time speech to text functionality into your website with no effort
229 lines (228 loc) • 11.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Speech = void 0;
const eventListeners_1 = require("./utils/eventListeners");
const preResultUtils_1 = require("./utils/preResultUtils");
const commandUtils_1 = require("./utils/commandUtils");
const autoScroll_1 = require("./utils/autoScroll");
const highlight_1 = require("./utils/highlight");
const elements_1 = require("./utils/elements");
const padding_1 = require("./utils/padding");
const browser_1 = require("./utils/browser");
const cursor_1 = require("./utils/cursor");
const text_1 = require("./utils/text");
class Speech {
constructor() {
this.finalTranscript = '';
// used for editable element
this.interimSpan = elements_1.Elements.createInterimSpan();
this.finalSpan = elements_1.Elements.createGenericSpan();
// used for allowing autoScroll() to scroll to it when interimSpan enters another line and doesn't scroll
this.scrollingSpan = elements_1.Elements.createGenericSpan();
this.isCursorAtEnd = false;
this.spansPopulated = false;
this.startPadding = '';
// primitive elements use this as the right hand side text of cursor
this.endPadding = '';
this.numberOfSpacesBeforeNewText = 0; // primarily used for setting cursor for primitive elements
this.numberOfSpacesAfterNewText = 0; // primarily used for setting cursor for primitive elements
this.isHighlighted = false;
this.primitiveTextRecorded = false;
this.recognizing = false;
this._displayInterimResults = true;
this.insertInCursorLocation = true;
this.autoScroll = true;
this.isRestarting = false;
this.isPaused = false;
this.isWaitingForCommand = false;
this.isTargetInShadow = false;
this.cannotBeStopped = false; // this is mostly used for Azure to prevent user from stopping when it is connecting
this.resetState();
}
prepareBeforeStart(options) {
var _a, _b;
if (options === null || options === void 0 ? void 0 : options.element) {
eventListeners_1.EventListeners.add(this, options);
if (Array.isArray(options.element)) {
// checks if any of available elements are currently focused, else proceeds to the first
const focusedElement = options.element.find((element) => element === document.activeElement);
const targetElement = focusedElement || options.element[0];
if (!targetElement)
return;
this.prepare(targetElement);
}
else {
this.prepare(options.element);
}
}
if ((options === null || options === void 0 ? void 0 : options.displayInterimResults) !== undefined)
this._displayInterimResults = options.displayInterimResults;
if (options === null || options === void 0 ? void 0 : options.textColor) {
this._finalTextColor = (_a = options === null || options === void 0 ? void 0 : options.textColor) === null || _a === void 0 ? void 0 : _a.final;
elements_1.Elements.applyCustomColors(this, options.textColor);
}
if ((options === null || options === void 0 ? void 0 : options.insertInCursorLocation) !== undefined)
this.insertInCursorLocation = options.insertInCursorLocation;
if ((options === null || options === void 0 ? void 0 : options.autoScroll) !== undefined)
this.autoScroll = options.autoScroll;
this._onResult = options === null || options === void 0 ? void 0 : options.onResult;
this._onPreResult = options === null || options === void 0 ? void 0 : options.onPreResult;
this._onStart = options === null || options === void 0 ? void 0 : options.onStart;
this._onStop = options === null || options === void 0 ? void 0 : options.onStop;
this._onError = options === null || options === void 0 ? void 0 : options.onError;
this.onCommandModeTrigger = options === null || options === void 0 ? void 0 : options.onCommandModeTrigger;
this.onPauseTrigger = options === null || options === void 0 ? void 0 : options.onPauseTrigger;
this._options = options;
if ((_b = this._options) === null || _b === void 0 ? void 0 : _b.commands)
this.commands = commandUtils_1.CommandUtils.process(this._options.commands);
}
prepare(targetElement) {
padding_1.Padding.setState(this, targetElement);
highlight_1.Highlight.setState(this, targetElement);
this.isTargetInShadow = elements_1.Elements.isInsideShadowDOM(targetElement);
if (elements_1.Elements.isPrimitiveElement(targetElement)) {
this._primitiveElement = targetElement;
this._originalText = this._primitiveElement.value;
}
else {
this._genericElement = targetElement;
this._originalText = this._genericElement.textContent;
}
}
// there was an attempt to optimize this by not having to restart the service and just reset state:
// unfortunately it did not work because the service would still continue firing the intermediate and final results
// into the new position
resetRecording(options) {
this.isRestarting = true;
this.stop(true);
this.resetState(true);
this.start(options, true);
}
// prettier-ignore
updateElements(interimTranscript, finalTranscript, newText) {
var _a;
const newFinalText = text_1.Text.capitalize(finalTranscript);
if (this.finalTranscript === newFinalText && interimTranscript === '')
return;
if (preResultUtils_1.PreResultUtils.process(this, newText, interimTranscript === '', this._onPreResult, this._options)) {
interimTranscript = '', newText = '';
}
const commandResult = this.commands && commandUtils_1.CommandUtils.execCommand(this, newText, this._options, this._primitiveElement || this._genericElement, this._originalText);
if (commandResult) {
if (commandResult.doNotProcessTranscription)
return;
interimTranscript = '', newText = '';
}
if (this.isPaused || this.isWaitingForCommand)
return;
(_a = this._onResult) === null || _a === void 0 ? void 0 : _a.call(this, newText, interimTranscript === '');
this.finalTranscript = newFinalText;
if (!this._displayInterimResults)
interimTranscript = '';
// this is primarily used to remove padding when interim/final text is removed on command
const isNoText = this.finalTranscript === '' && interimTranscript === '';
if (this._primitiveElement) {
this.updatePrimitiveElement(this._primitiveElement, interimTranscript, isNoText);
}
else if (this._genericElement) {
this.updateGenericElement(this._genericElement, interimTranscript, isNoText);
}
}
// prettier-ignore
// remember that padding values here contain actual text left and right
updatePrimitiveElement(element, interimTranscript, isNoText) {
if (this.isHighlighted)
highlight_1.Highlight.removeForPrimitive(this, element);
if (!this.primitiveTextRecorded)
padding_1.Padding.adjustStateAfterRecodingPrimitiveElement(this, element);
if (isNoText)
padding_1.Padding.adjustSateForNoTextPrimitiveElement(this);
const cursorLeftSideText = this.startPadding + this.finalTranscript + interimTranscript;
element.value = cursorLeftSideText + this.endPadding;
if (!this.isTargetInShadow) {
const newCusrorPos = cursorLeftSideText.length + this.numberOfSpacesAfterNewText;
cursor_1.Cursor.setOffsetForPrimitive(element, newCusrorPos, this.autoScroll);
}
if (this.autoScroll && browser_1.Browser.IS_SAFARI() && this.isCursorAtEnd)
autoScroll_1.AutoScroll.scrollSafariPrimitiveToEnd(element);
}
updateGenericElement(element, interimTranscript, isNoText) {
if (this.isHighlighted)
highlight_1.Highlight.removeForGeneric(this, element);
if (!this.spansPopulated)
elements_1.Elements.appendSpans(this, element);
// for web speech api - safari only returns final text - no interim
const finalText = (isNoText ? '' : this.startPadding) + text_1.Text.lineBreak(this.finalTranscript);
this.finalSpan.innerHTML = finalText;
const isAutoScrollingRequired = autoScroll_1.AutoScroll.isRequired(this.autoScroll, element);
autoScroll_1.AutoScroll.changeStateIfNeeded(this, isAutoScrollingRequired);
const interimText = text_1.Text.lineBreak(interimTranscript) + (isNoText ? '' : this.endPadding);
this.interimSpan.innerHTML = interimText;
if (browser_1.Browser.IS_SAFARI() && this.insertInCursorLocation) {
cursor_1.Cursor.setOffsetForSafariGeneric(element, finalText.length + interimText.length);
}
if (isAutoScrollingRequired)
autoScroll_1.AutoScroll.scrollGeneric(this, element);
if (isNoText)
this.scrollingSpan.innerHTML = '';
}
finalise(isDuringReset) {
if (this._genericElement) {
if (isDuringReset) {
this.finalSpan = elements_1.Elements.createGenericSpan();
this.setInterimColorToFinal();
this.interimSpan = elements_1.Elements.createInterimSpan();
this.scrollingSpan = elements_1.Elements.createGenericSpan();
}
else {
this._genericElement.textContent = this._genericElement.textContent;
}
this.spansPopulated = false;
}
eventListeners_1.EventListeners.remove(this);
}
setInterimColorToFinal() {
this.interimSpan.style.color = this._finalTextColor || 'black';
}
resetState(isDuringReset) {
this._primitiveElement = undefined;
this._genericElement = undefined;
this.finalTranscript = '';
this.finalSpan.innerHTML = '';
this.interimSpan.innerHTML = '';
this.scrollingSpan.innerHTML = '';
this.startPadding = '';
this.endPadding = '';
this.isHighlighted = false;
this.primitiveTextRecorded = false;
this.numberOfSpacesBeforeNewText = 0;
this.numberOfSpacesAfterNewText = 0;
if (!isDuringReset)
this.stopTimeout = undefined;
}
setStateOnStart() {
var _a;
this.recognizing = true;
if (this.isRestarting) {
// this is the only place where this.isRestarting needs to be set to false
// as whn something goes wrong or the user is manually restarting - a new speech service will be initialized
this.isRestarting = false;
}
else {
(_a = this._onStart) === null || _a === void 0 ? void 0 : _a.call(this);
}
}
setStateOnStop() {
var _a;
this.recognizing = false;
if (!this.isRestarting) {
(_a = this._onStop) === null || _a === void 0 ? void 0 : _a.call(this);
}
}
setStateOnError(details) {
var _a;
(_a = this._onError) === null || _a === void 0 ? void 0 : _a.call(this, details);
this.recognizing = false;
}
}
exports.Speech = Speech;