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:
- The browser
Web Speech API - Google Cloud
text-to-speech API
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 withfor...ofoptions: Optional playback rate, pitch, volume, andexternal(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());
});