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;