Skip to content

Custom text-to-speech synthesizer ​

A speech synthesizer is a text-to-speech service that converts text into human-like audio. It works with the Read Aloud add-on to read page content aloud.

Tip: Depending on the TTS technology, output may sound synthetic or very natural.

This guide shows how to integrate the SDK with:

Speech synthesizer API ​

PDFTextToSpeechSynthesis interface ​

typescript
interface PDFTextToSpeechSynthesis {
    status: PDFTextToSpeechSynthesisStatus;
    supported(): boolean;
    pause(): void;
    resume(): void;
    stop(): void;
    play(utterances: IterableIterator<Promise<PDFTextToSpeechUtterance>>, options?: ReadAloudOptions): Promise<void>;
    updateOptions(options: Partial<ReadAloudOptions>): void;
}

1. status property ​

status is an enum for the current read-aloud state:

typescript
enum PDFTextToSpeechSynthesisStatus {
    playing, paused, stopped,
}

Tip: Initial state is stopped.

2. supported():boolean method ​

Checks whether the client supports PDFTextToSpeechSynthesis. If speech runs on a third-party service, you only need to verify HTML<audio> support on the client.

Tip: The client may be a browser, Electron, Apache Cordova, etc.

Example:

typescript
class CustomPDFTextToSpeechSynthesis {
    supported(): boolean {
        return typeof window.HTMLAudioElement === 'function';
    }
    / .... other methods
}

3. pause(), resume(), and stop() methods ​

Control read-aloud state: pause, resume, stop media, and update status.

4. updateOptions(options: Partial<ReadAloudOptions>) method ​

Updates options while playing, such as volume.

5. play() method ​

typescript
play(utterances: IterableIterator<Promise<PDFTextToSpeechUtterance>>, options?: ReadAloudOptions): Promise<void>

Parameters:

  • utterances: IterableIterator of text to read with page and position info; iterate with for...of
  • options: Optional playback rate, pitch, volume, and external (passed to third-party TTS)

Custom PDFTextToSpeechSynthesis ​

Method 1: Implement the interface ​

Tip: This demo runs in Chrome, Firefox, and Chromium Edge.

html
<html>
</html>
<script>
    const PDFTextToSpeechSynthesisStatus = UIExtension.PDFViewCtrl.readAloud.PDFTextToSpeechSynthesisStatus;

    class CustomPDFTextToSpeechSynthesis {
        constructor() {
            this.playingOptions = {};
            this.status = PDFTextToSpeechSynthesisStatus.stopped;
        }

        supported() {
            return typeof window.speechSynthesis !== 'undefined';
        }

        pause() {
            this.status = PDFTextToSpeechSynthesisStatus.paused;
            window.speechSynthesis.pause();
        }

        resume() {
            this.status = PDFTextToSpeechSynthesisStatus.playing;
            window.speechSynthesis.resume();
        }

        stop() {
            this.status = PDFTextToSpeechSynthesisStatus.stopped;
            window.speechSynthesis.cancel();
        }

        /**
         * @param {IterableIterator<Promise<PDFTextToSpeechUtterance>>} utterances
         * @param {ReadAloudOptions} options
         *
         */
        async play(utterances, options) {
            for await (const utterance of utterances) {
                const nativeSpeechUtterance = new window.SpeechSynthesisUtterance(utterance.text);
                const {pitch, rate, volume} = Object.assign(
                        {}, this.playingOptions, options || {}
                );
                if (typeof pitch === 'number') {
                    nativeSpeechUtterance.pitch = pitch;
                }
                if (typeof rate === 'number') {
                    nativeSpeechUtterance.rate = rate;
                }
                if (typeof volume === 'number') {
                    nativeSpeechUtterance.volume = volume;
                }
                await new Promise((resolve, reject) => {
                    nativeSpeechUtterance.onend = resolve;
                    nativeSpeechUtterance.onabort = resolve;
                    nativeSpeechUtterance.onerror = reject;
                    speechSynthesis.speak(nativeSpeechUtterance);
                });
            }
        }

        updateOptions(options) {
            Object.assign(this.playingOptions, options);
        }
    }

    var libPath = window.top.location.origin + '/lib';
    var pdfui = new UIExtension.PDFUI({
        viewerOptions: {
            libPath: libPath,
            jr: {
                licenseSN: licenseSN,
                licenseKey: licenseKey
            }
        },
        renderTo: document.body,
        appearance: UIExtension.appearances.ribbon,
        addons: [
            libPath + '/uix-addons/read-aloud'
        ]
    });
    pdfui.getReadAloudService().then(function (service) {
        service.setSpeechSynthesis(new CustomPDFTextToSpeechSynthesis());
    });

</script>
json
{
  "iframeOptions": {
    "style": "height: 500px"
  }
}

Method 2: Extend AbstractPDFTextToSpeechSynthesis ​

html
<html>
</html>
<script>
    const PDFTextToSpeechSynthesisStatus = UIExtension.PDFViewCtrl.readAloud.PDFTextToSpeechSynthesisStatus;
    const AbstractPDFTextToSpeechSynthesis = UIExtension.PDFViewCtrl.readAloud.AbstractPDFTextToSpeechSynthesis;
    const CustomPDFTextToSpeechSynthesis = AbstractPDFTextToSpeechSynthesis.extend({
        init() {
        },
        supported() {
            return typeof window.speechSynthesis !== 'undefined';
        },
        doPause() {
            window.speechSynthesis.pause();
        },
        doResume() {
            window.speechSynthesis.resume();
        },
        doStop() {
            window.speechSynthesis.cancel();
        },
        /**
         * @param {string} text
         * @param {ReadAloudOptions | undefined} options
         */
        async speakText(text, options) {
            const nativeSpeechUtterance = new window.SpeechSynthesisUtterance(text);
            const {pitch, rate, volume} = Object.assign(
                    {}, this.playingOptions, options || {}
            );
            if (typeof pitch === 'number') {
                nativeSpeechUtterance.pitch = pitch;
            }
            if (typeof rate === 'number') {
                nativeSpeechUtterance.rate = rate;
            }
            if (typeof volume === 'number') {
                nativeSpeechUtterance.volume = volume;
            }
            await new Promise((resolve, reject) => {
                nativeSpeechUtterance.onend = resolve;
                nativeSpeechUtterance.onabort = resolve;
                nativeSpeechUtterance.onerror = reject;
                speechSynthesis.speak(nativeSpeechUtterance);
            });
        }
    })
    const libPath = window.top.location.origin + '/lib';
    const pdfui = new UIExtension.PDFUI({
        viewerOptions: {
            libPath: libPath,
            jr: {
                licenseSN: licenseSN,
                licenseKey: licenseKey
            }
        },
        renderTo: document.body,
        appearance: UIExtension.appearances.ribbon,
        addons: [
            libPath + '/uix-addons/read-aloud'
        ]
    });
    pdfui.getReadAloudService().then(function (service) {
        service.setSpeechSynthesis(new CustomPDFTextToSpeechSynthesis());
    });

</script>
json
{
  "iframeOptions": {
    "style": "height: 500px"
  }
}

Differences between the two approaches ​

Method 1 implements PDFTextToSpeechSynthesis directly. You manage state and iterate utterances with for await...of. Each item is a text block from PDFPage. Blocks may split words or sentences; merge blocks in play() when you need whole words or sentences for better TTS.

Method 2 extends AbstractPDFTextToSpeechSynthesis. The base class manages state and iteration; you implement speakText() using window.SpeechSynthesisUtterance. The abstract class merges blocks automatically, but merged text may not always be perfect across engines. If you need strict word/sentence boundaries, prefer Method 1.

Third-party TTS services ​

This section uses @google-cloud/text-to-speech as an example.

Server ​

See https://cloud.google.com/text-to-speech/docs/quickstarts for server SDKs.

Client ​

javascript
var readAloud = UIExtension.PDFViewCtrl.readAloud;
var PDFTextToSpeechSynthesisStatus = readAloud.PDFTextToSpeechSynthesisStatus;
var AbstractPDFTextToSpeechSynthesis = readAloud.AbstractPDFTextToSpeechSynthesis;
var SPEECH_SYNTHESIS_URL = '<server url>'; / the server API address

var ThirdpartyPDFTextToSpeechSynthesis = AbstractPDFTextToSpeechSynthesis.extend({
    init: function () {
        this.audioElement = null;
    },
    supported: function () {
        return typeof window.HTMLAudioElement === 'function' && document.createElement('audio') instanceof window.HTMLAudioElement;
    },
    doPause: function () {
        if (this.audioElement) {
            this.audioElement.pause();
        }
    },
    doStop: function () {
        if (this.audioElement) {
            this.audioElement.pause();
            this.audioElement.currentTime = 0;
            this.audioElement = null;
        }
    },
    doResume: function () {
        if (this.audioElement) {
            this.audioElement.play();
        }
    },
    onCurrentPlayingOptionsUpdated: function () {
        if (!this.audioElement) {
            return;
        }
        var options = this.currentPlayingOptions;
        if (this.status === PDFTextToSpeechSynthesisStatus.playing) {
            if (options.volume >= 0 && options.volume <= 1) {
                this.audioElement.volume = options.volume;
            }
        }
    },
    speakText: function (text, options) {
        var audioElement = document.createElement('audio');
        this.audioElement = audioElement;
        if (options.volume >= 0 && options.volume <= 1) {
            audioElement.volume = options.volume;
        }
        return this.speechSynthesis(text, options).then(function (src) {
            return new Promise(function (resolve, reject) {
                audioElement.src = src;
                audioElement.onended = function () {
                    resolve();
                };
                audioElement.onabort = function () {
                    resolve();
                };
                audioElement.onerror = function (e) {
                    reject(e);
                };
                audioElement.play();
            }).finally(function () {
                URL.revokeObjectURL(src);
            });
        });
    },
    / If the server API request method or parameter form is not consistent with the following implementation, it will need to be adjusted accordingly.
    speechSynthesis: function (text, options) {
        var url = SPEECH_SYNTHESIS_URL + '?' + this.buildURIQueries(text, options);
        return fetch(url).then(function (response) {
            if (response.status >= 400) {
                return response.json().then(function (json) {
                    return Promise.reject(JSON.parse(json).error);
                });
            }
            return response.blob();
        }).then(function (blob) {
            return URL.createObjectURL(blob);
        });
    },
    buildURIQueries: function (text, options) {
        var queries = [
            'text=' + encodeURIComponent(text)
        ];
        if (!options) {
            return queries.join('&');
        }
        if (typeof options.rate === 'number') {
            queries.push('rate=' + options.rate);
        }
        if (typeof options.spitch === 'number') {
            queries.push('spitch=' + options.spitch);
        }
        if (typeof options.lang === 'string') {
            queries.push('lang=' + encodeURIComponent(options.lang));
        }
        if (typeof options.voice === 'string') {
            queries.push('voice=' + encodeURIComponent(options.voice));
        }
        if (typeof options.external !== 'undefined') {
            queries.push('external=' + encodeURIComponent(JSON.stringify(options.external)));
        }
        return queries.join('&');
    }
});

Use the custom synthesizer ​

javascript
pdfui.getReadAloudService().then(function (service) {
    serivce.setSpeechSynthesis(new ThirdpartyPDFTextToSpeechSynthesis());
});