UNPKG

react-say

Version:

A React component that synthesis text into speech using Web Speech API

538 lines (518 loc) 18.1 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.mjs var src_exports = {}; __export(src_exports, { Composer: () => Composer_default, Context: () => Context_default, SayButton: () => SayButton_default, SayUtterance: () => SayUtterance_default, default: () => src_default }); module.exports = __toCommonJS(src_exports); // src/Composer.jsx var import_prop_types4 = __toESM(require("prop-types")); var import_react7 = __toESM(require("react")); // src/Context.mjs var import_react = __toESM(require("react"), 1); var Context = import_react.default.createContext(); var Context_default = Context; // src/createCustomEvent.mjs function createCustomEvent(name, eventInitDict) { if (name === "error") { if (typeof ErrorEvent === "function") { return new ErrorEvent(name, eventInitDict); } } else if (typeof CustomEvent === "function") { return new CustomEvent(name, eventInitDict); } const event = document.createEvent("Event"); event.initEvent(name, true, true); Object.entries(eventInitDict || {}).forEach(([key, value]) => { event[key] = value; }); return event; } // src/createDeferred.mjs function createDeferred() { let reject, resolve; const promise = new Promise((promiseResolve, promiseReject) => { reject = promiseReject; resolve = promiseResolve; }); if (!reject || !resolve) { throw new Error("Promise is not a ES-compliant and do not run exector immediately"); } return { promise, reject, resolve }; } // src/createErrorEvent.mjs function createErrorEvent(error) { return createCustomEvent("error", { error }); } // src/QueuedUtterance.mjs async function speakUtterance(ponyfill, utterance, startCallback) { const { speechSynthesis } = ponyfill; const startDeferred = createDeferred(); const errorDeferred = createDeferred(); const endDeferred = createDeferred(); utterance.addEventListener("end", endDeferred.resolve); utterance.addEventListener("error", errorDeferred.resolve); utterance.addEventListener("start", startDeferred.resolve); speechSynthesis.speak(utterance); const startEvent = await Promise.race([errorDeferred.promise, startDeferred.promise]); if (startEvent.type === "error") { throw startEvent.error; } let finishedSpeaking; const endPromise = Promise.race([errorDeferred.promise, endDeferred.promise]); startCallback && startCallback(async () => { if (!finishedSpeaking) { speechSynthesis.cancel(); await endPromise; } }); const endEvent = await endPromise; finishedSpeaking = true; if (endEvent.type === "error") { throw endEvent.error; } } var QueuedUtterance = class { constructor(ponyfill, utterance, { onEnd, onError, onStart }) { this._cancelled = false; this._deferred = createDeferred(); this._onEnd = onEnd; this._onError = onError; this._onStart = onStart; this._ponyfill = ponyfill; this._speaking = false; this._utterance = utterance; this.promise = this._deferred.promise; } async cancel() { this._cancelled = true; this._cancel && await this._cancel(); } speak() { if (this._speaking) { console.warn(`ASSERTION: QueuedUtterance is already speaking or has spoken.`); } this._speaking = true; (async () => { if (this._cancelled) { throw new Error("cancelled"); } await speakUtterance(this._ponyfill, this._utterance, (cancel) => { if (this._cancelled) { cancel(); throw new Error("cancelled"); } else { this._cancel = cancel; this._onStart && this._onStart(createCustomEvent("start")); } }); if (this._cancelled) { throw new Error("cancelled"); } })().then( () => { this._onEnd && this._onEnd(createCustomEvent("end")); this._deferred.resolve(); }, (error) => { this._onError && this._onError(createErrorEvent(error)); this._deferred.reject(error); } ); return this.promise; } }; // src/createSynthesize.mjs function createSynthesize() { let queueWithCurrent = []; let running; const run = async () => { if (running) { return; } running = true; try { let queuedUtterance; while (queuedUtterance = queueWithCurrent[0]) { try { await queuedUtterance.speak(); } catch (err) { err.message !== "cancelled" && console.error(err); } queueWithCurrent = queueWithCurrent.filter((target) => target !== queuedUtterance); } } finally { running = false; } }; return (ponyfill, utterance, { onEnd, onError, onStart } = {}) => { if (!(utterance instanceof ponyfill.SpeechSynthesisUtterance)) { throw new Error("utterance must be instance of the ponyfill"); } const queuedUtterance = new QueuedUtterance(ponyfill, utterance, { onEnd, onError, onStart }); queueWithCurrent = [...queueWithCurrent, queuedUtterance]; run(); return { // The cancel() function returns a Promise cancel: () => queuedUtterance.cancel(), promise: queuedUtterance.promise }; }; } // src/Say.jsx var import_prop_types2 = __toESM(require("prop-types")); var import_react4 = __toESM(require("react")); // src/createNativeUtterance.mjs function createNativeUtterance({ speechSynthesis, SpeechSynthesisUtterance }, { lang, onBoundary, pitch, rate, text, voice, volume }) { const utterance = new SpeechSynthesisUtterance(text); let targetVoice; if (typeof voice === "function") { targetVoice = voice.call(speechSynthesis, speechSynthesis.getVoices()); } else { const { voiceURI } = voice || {}; targetVoice = voiceURI && [].find.call([].slice.call(speechSynthesis.getVoices()), (v) => v.voiceURI === voiceURI); } utterance.lang = lang || ""; if (pitch || pitch === 0) { utterance.pitch = pitch; } if (rate || rate === 0) { utterance.rate = rate; } if (targetVoice) { utterance.voice = targetVoice; } if (volume || volume === 0) { utterance.volume = volume; } onBoundary && utterance.addEventListener("boundary", onBoundary); return utterance; } // src/SayUtterance.jsx var import_prop_types = __toESM(require("prop-types")); var import_react3 = __toESM(require("react")); // src/useSynthesize.mjs var import_react2 = require("react"); function useSynthesize() { const { ponyfill, synthesize } = (0, import_react2.useContext)(Context_default); return (utteranceOrText, progressFn) => { if (typeof utteranceOrText === "string") { utteranceOrText = createNativeUtterance(ponyfill, { text: utteranceOrText }); } return synthesize(ponyfill, utteranceOrText, { onStart: progressFn && (() => progressFn()) }); }; } // src/SayUtterance.jsx var SayUtterance = (props) => { const { onEnd, onError, onStart, utterance } = migrateDeprecatedProps(props); const started = (0, import_react3.useRef)(false); const synthesize = useSynthesize(); (0, import_react3.useEffect)(() => { if (started.current) { return console.warn("react-say: Should not change utterance after synthesis started."); } let cancelled; const { cancel, promise } = synthesize(utterance, () => { started.current = true; !cancelled && onStart && onStart(createCustomEvent("start")); }); promise.then( () => !cancelled && onEnd && onEnd(createCustomEvent("end")), (error) => !cancelled && onError && onError(createErrorEvent(error)) ); return () => { cancelled = true; cancel(); }; }, []); return false; }; SayUtterance.defaultProps = { onEnd: void 0, onError: void 0, onStart: void 0 }; SayUtterance.propTypes = { onEnd: import_prop_types.default.func, onError: import_prop_types.default.func, onStart: import_prop_types.default.func }; var SayUtteranceWithContext = ({ ponyfill, ...props }) => /* @__PURE__ */ import_react3.default.createElement(Composer_default, { ponyfill }, /* @__PURE__ */ import_react3.default.createElement(SayUtterance, { ...props })); SayUtteranceWithContext.defaultProps = { ...SayUtterance.defaultProps, ponyfill: void 0 }; SayUtteranceWithContext.propTypes = { ...SayUtterance.propTypes, ponyfill: import_prop_types.default.shape({ speechSynthesis: import_prop_types.default.any.isRequired, SpeechSynthesisUtterance: import_prop_types.default.any.isRequired }) }; var SayUtterance_default = SayUtteranceWithContext; // src/Say.jsx var Say = (props) => { let { lang, onBoundary, onEnd, onError, onStart, pitch, rate, speak, text, voice, volume } = migrateDeprecatedProps( props, Say ); const { ponyfill } = (0, import_react4.useContext)(Context_default); if (speak && !text) { console.warn('react-say: "speak" prop is being deprecated and renamed to "text".'); text = speak; } const utterance = (0, import_react4.useMemo)( () => createNativeUtterance(ponyfill, { lang, onBoundary, pitch, rate, text, voice, volume }), [lang, onBoundary, pitch, ponyfill, rate, text, voice, volume] ); return /* @__PURE__ */ import_react4.default.createElement(SayUtterance_default, { onEnd, onError, onStart, ponyfill, utterance }); }; Say.defaultProps = { children: void 0, lang: void 0, onBoundary: void 0, onEnd: void 0, onError: void 0, onStart: void 0, pitch: void 0, rate: void 0, speak: void 0, voice: void 0, volume: void 0 }; Say.propTypes = { children: import_prop_types2.default.any, lang: import_prop_types2.default.string, onBoundary: import_prop_types2.default.func, onEnd: import_prop_types2.default.func, onError: import_prop_types2.default.func, onStart: import_prop_types2.default.func, pitch: import_prop_types2.default.number, rate: import_prop_types2.default.number, speak: import_prop_types2.default.string, text: import_prop_types2.default.string.isRequired, voice: import_prop_types2.default.oneOfType([import_prop_types2.default.any, import_prop_types2.default.func]), volume: import_prop_types2.default.number }; var SayWithContext = ({ ponyfill, ...props }) => /* @__PURE__ */ import_react4.default.createElement(Composer_default, { ponyfill }, /* @__PURE__ */ import_react4.default.createElement(Say, { ...props })); SayWithContext.defaultProps = { ...SayUtterance_default.defaultProps, ponyfill: void 0 }; SayWithContext.propTypes = { ...SayUtterance_default.propTypes, ponyfill: import_prop_types2.default.shape({ speechSynthesis: import_prop_types2.default.any.isRequired, SpeechSynthesisUtterance: import_prop_types2.default.any.isRequired }) }; var Say_default = SayWithContext; // src/SayButton.jsx var import_prop_types3 = __toESM(require("prop-types")); var import_react5 = __toESM(require("react")); var SayButton = (props) => { const { children, disabled, lang, onBoundary, onEnd, onError, onStart, pitch, ponyfill, rate, text, voice, volume } = migrateDeprecatedProps(props, SayButton); const [busy, setBusy] = (0, import_react5.useState)(false); const handleClick = (0, import_react5.useCallback)(() => setBusy(true)); const sayProps = { lang, onBoundary, onEnd: (event) => { setBusy(false); onEnd && onEnd(event); }, onError, onStart, pitch, ponyfill, rate, text, voice, volume }; return /* @__PURE__ */ import_react5.default.createElement(import_react5.default.Fragment, null, /* @__PURE__ */ import_react5.default.createElement("button", { disabled: typeof disabled === "boolean" ? disabled : busy, onClick: handleClick }, children), busy && /* @__PURE__ */ import_react5.default.createElement(Say_default, { ...sayProps })); }; SayButton.defaultProps = { children: void 0, disabled: void 0, lang: void 0, onBoundary: void 0, onEnd: void 0, onError: void 0, onStart: void 0, pitch: void 0, ponyfill: void 0, rate: void 0, text: void 0, voice: void 0, volume: void 0 }; SayButton.propTypes = { children: import_prop_types3.default.any, disabled: import_prop_types3.default.bool, lang: import_prop_types3.default.string, onBoundary: import_prop_types3.default.func, onEnd: import_prop_types3.default.func, onError: import_prop_types3.default.func, onStart: import_prop_types3.default.func, pitch: import_prop_types3.default.number, ponyfill: import_prop_types3.default.shape({ speechSynthesis: import_prop_types3.default.any.isRequired, SpeechSynthesisUtterance: import_prop_types3.default.any.isRequired }), rate: import_prop_types3.default.number, text: import_prop_types3.default.string, voice: import_prop_types3.default.oneOfType([import_prop_types3.default.any, import_prop_types3.default.func]), volume: import_prop_types3.default.number }; var SayButton_default = SayButton; // src/migrateDeprecatedProps.mjs var warnings = { ponyfill: true, saySpeak: true }; function migrateDeprecatedProps({ ponyfill, speak, speechSynthesis, speechSynthesisUtterance, text, ...otherProps }, componentType) { if (!ponyfill && (speechSynthesis || speechSynthesisUtterance)) { if (warnings.ponyfill) { console.warn( 'react-say: "speechSynthesis" and "speechSynthesisUtterance" props has been renamed to "ponyfill". Please update your code. The deprecated props will be removed in version >= 3.0.0.' ); warnings.ponyfill = false; } ponyfill = { speechSynthesis, SpeechSynthesisUtterance: speechSynthesisUtterance }; } if (componentType === Say_default || componentType === SayButton_default) { if (speak && !text) { if (warnings.saySpeak) { console.warn( 'react-say: "speak" prop has been renamed to "text". Please update your code. The deprecated props will be removed in version >= 3.0.0.' ); warnings.saySpeak = false; } text = speak; } } return { ponyfill, text, ...otherProps }; } // src/useImmediateEffect.mjs var import_react6 = require("react"); function useImmediateEffect(fn, deps) { const unsubscribeRef = (0, import_react6.useRef)({ first: true, id: Math.random().toString(36).substr(2, 5), unsubscribe: fn() }); (0, import_react6.useEffect)(() => { const { current } = unsubscribeRef; if (!current.first) { current.unsubscribe = fn(); } else { current.first = false; } return () => { current.unsubscribe && current.unsubscribe(); current.unsubscribe = null; }; }, deps); } // src/useEvent.mjs function useEvent(target, name, listener, options) { useImmediateEffect(() => { const handler = (event) => listener && listener(event); target.addEventListener(name, handler, options); return () => { listener = null; target.removeEventListener(name, handler, options); }; }, [listener, name, options, target]); } // src/Composer.jsx var Composer = (props) => { const { children, ponyfill: ponyfillFromProps } = migrateDeprecatedProps(props, Composer); const { ponyfill: parentPonyfill, synthesize: parentSynthesize } = (0, import_react7.useContext)(Context_default) || {}; const ponyfill = ponyfillFromProps || parentPonyfill || { speechSynthesis: window.speechSynthesis || window.webkitSpeechSynthesis, SpeechSynthesisUtterance: window.SpeechSynthesisUtterance || window.webkitSpeechSynthesisUtterance }; const synthesize = (0, import_react7.useMemo)(() => parentSynthesize || createSynthesize(), [parentSynthesize]); const { speechSynthesis } = ponyfill; const [voices, setVoices] = (0, import_react7.useState)(speechSynthesis.getVoices()); useEvent(speechSynthesis, "voiceschanged", () => setVoices(speechSynthesis.getVoices())); const context = (0, import_react7.useMemo)( () => ({ ponyfill, synthesize, voices }), [ponyfill, synthesize, voices] ); return /* @__PURE__ */ import_react7.default.createElement(Context_default.Provider, { value: context }, typeof children === "function" ? /* @__PURE__ */ import_react7.default.createElement(Context_default.Consumer, null, (context2) => children(context2)) : children); }; Composer.defaultProps = { children: void 0, ponyfill: void 0 }; Composer.propTypes = { children: import_prop_types4.default.any, ponyfill: import_prop_types4.default.shape({ speechSynthesis: import_prop_types4.default.any, SpeechSynthesisUtterance: import_prop_types4.default.any }) }; var Composer_default = Composer; // src/index.mjs var src_default = Say_default; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Composer, Context, SayButton, SayUtterance }); //# sourceMappingURL=react-say.js.map