# 自定义语音合成器

为方便演示, WEBSDK 使用浏览器自带的语音合成接口(Web Speech API (opens new window)),由于该特性还是一个实验中的功能,不推荐在面向终端用户的生产环境中使用, 因此,WEBSDK 提供自定义语音合成器 API 以方便开发者自由选择和集成第三方语音合成服务。

# 语音合成器API

# PDFTextToSpeechSynthesis 接口规范

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 属性

status 是表示当前朗读状态的枚举,定义如下:

enum PDFTextToSpeechSynthesisStatus {
    playing, paused, stopped,
}

状态初始值应设置为 stopped

# 2. supported():boolean 方法

这个方法用来检测当前环境是否支持改语音合成器。一般集成的第三方服务在后台运行的话,只需要检查支持音频播放功能即可。

class CustomPDFTextToSpeechSynthesis {
    supported(): boolean {
        return typeof window.HTMLAudioElement === 'function';
    }
    // .... 其它方法
}

# 3. pause(),resume 以及 stop() 方法

这三个方法都是用来控制朗读的状态。语音合成器在朗读的时候可以通过这三个方法控制暂停、恢复和停止。并且设置 status 属性值。

# 4. updateOptions(options: Partial<ReadAloudOptions>) 方法

此方法用于更新正在朗读状态的语音合成器。例如:控制音量。

# 5. play(utterances: IterableIterator<Promise<PDFTextToSpeechUtterance>>, options?: ReadAloudOptions): Promise<void> 方法

参数说明: 1. utterances: 是一个 IterableIterator, 包含需要阅读的文本内容以及所在的页码和坐标信息, 可以使用 for...of 语法遍历。 2. options:这时一个可选参数,包含了播放的语速、音调、音量以及external参数,其中external是传给第三方语音合成器服务的参数对象。

# 自定义 PDFTextToSpeechSynthesis

# 方法1:实现 PDFTextToSpeechSynthesis interface

注意: 该示例仅支持在 Chrome, Firefox, Chromium Edge 浏览器中运行。

# 方法2:基于 AbstractPDFTextToSpeechSynthesis 自定义语音合成器

# 两种自定义方法区别

方法1通过实现 PDFTextToSpeechSynthesis 接口自定义语音合成器,它需要手动管理状态的变化以及通过for await...of语法遍历 utterances 列表。utterance 列表的每一项均是从PDFPage获取的文本块,由于文本块可能出现单词或句子不完整的问题,通常需要进行合并等操作才能组成完整的单词或句子。 方法2通过继承AbstractPDFTextToSpeechSynthesis抽象类来实现自定义,他不需要手动管理状态以及遍历utterances列表,只需要根据接收到的文本和参数正确转换语音和播放即可,内部会根据接收到的文本自动进行合并, 但目前无法保证在不同的语言环境下合并的句子或单词一定是完整的,所以对此有特殊要求的用户建议使用方法1

# 集成第三方语音服务(@google-cloud/text-to-speech 为例)

# 服务端

可参考官方各个语言版本SDK: https://github.com/googleapis?q=text-to-speech&type=&language=&sort= (opens new window)

# 客户端

var readAloud = UIExtension.PDFViewCtrl.readAloud;
var PDFTextToSpeechSynthesisStatus = readAloud.PDFTextToSpeechSynthesisStatus;
var AbstractPDFTextToSpeechSynthesis = readAloud.AbstractPDFTextToSpeechSynthesis;
var SPEECH_SYNTHESIS_URL = '<server url>'; // 这里要替换成服务端的接口地址

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);
            });
        });
    },
    // 如果服务端接口请求方法或参数形式和下面的实现不一致,则需要进行相应的调整。
    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('&');
    }
});

使用自定义语音合成器:

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