分享好友 资讯文章首页 资讯文章分类 切换频道

网页中文本朗读功能开发实现分享

2023-03-13 16:58IP属地 广东佛山420网络整理
网页中文本朗读功能开发实现分享:

前几天完成了一个需求,在网页中完成鼠标指向哪里,就用语音读出所指的文本。如果是按钮、链接、文本输入框,则还还要给出是什么的提醒。同时针对大段的文本,不能整段的去读,要按照标点符号进行断句处理。

重点当然就是先获取到当前标签上的文本,再把文本转化成语音即可。

标签朗读

这个很简单了,只用根据当前是什么标签,给出提示即可。

// 标签朗读文本

var tagTextConfig = {

'a': '链接',

'input[text]': '文本输入框',

'input[password]': '密码输入框',

'button': '按钮',

'img': '图片'

};

还有需要朗读的标签,继续再添加即可。

然后根据标签,返回前缀文本即可。

function getTagText(el) {

if (!el) return '';

var tagName = el.tagName.toLowerCase();

// 处理input等多属性元素

switch (tagName) {

case 'input':

tagName += '[' + el.type + ']';

break;

default:

break;

}

// 标签的功能提醒和作用应该有间隔,因此在最后加入一个空格

return (tagTextConfig[tagName] || '') + ' ';

}

获取完整的朗读文本就更简单了,先取标签的功能提醒,再取标签的文本即可。

文本内容优先取 title 其次 alt 最后 innerText。

function getText(el) {

if (!el) return '';

return getTagText(el) + (el.title || el.alt || el.innerText || '');

}

这样就可以获取到一个标签的功能提醒和内容的全部带朗读文本了。

正文分隔

接下来要处理的就是正文分隔了,在这个过程中,踩了不少坑,走了不少弯路,好好记录一下。

首先准备了正文分隔的配置:

// 正文拆分配置

var splitConfig = {

// 内容分段标签名称

unitTag: 'p',

// 正文中分隔正则表达式

splitReg: /[,;,;。]/g,

// 包裹标签名

wrapTag: 'label',

// 包裹标签类名

wrapCls: 'speak-lable',

// 高亮样式名和样式

hightlightCls: 'speak-help-hightlight',

hightStyle: 'background: #000!important; color: #fff!important'

};

最开始想的就是直接按照正文中的分隔标点符号进行分隔就好了呀。

想法如下:

获取段落全部文本

使用 split(分隔正则表达式) 方法将正文按照标点符号分隔成小段

每个小段用标签包裹放回去即可

然而理想很丰满,现实很骨感。

两个大坑如下:

split 方法进行分隔,分隔后分隔字符就丢了,也就是说把原文的一些标点符号给弄丢了。

如果段落内还存在其他标签,而这个标签内部也正好存在待分隔的标点符号,那包裹分段标签时直接破换了原标签的完整性。

关于第一个问题,丢失标点的符号,考虑过逐个标点来进行和替换 split 分隔方法为逐个字符循环来做。

前者问题是原本一次完成的工作分成了多次,效率太低。第二种感觉效率更低了,分隔本来是很稀疏的,但是却要变成逐个字符出判断处理,更关键的是,分隔标点的位置要插入包裹标签,会导致字符串长度变化,还要处理下标索引。代码是机器跑的,或许不会觉得烦,但是我真的觉得好烦。如果这么干,或许以后哪个AI或者同事看到这样的代码,说不定会说“这真是个傻xxxx”。

第二个问题想过很多办法来补救,如先使用正则匹配捕获内容中成对的标签,对标签内部的分隔先处理一遍,然后再处理整个的。

想不明白问题二的,可参考一下待分隔的段落:

这是一段测试文本,这里有个链接。您好,可以点击此处进行跳转还有其他内容其他内容容其他内容容其他内容,容其他内容。

如先使用/<((/w+?)>)(.+?)<///2(?=>)/g 正则,依次捕获段落内被标签包裹的内容,对标签内部的内容先处理。

但是问题又来了,这么处理的都是字符串,在js中都是基本类型,这些操作进行的时候都是在复制的基础上进行的,要修改到原字符串里去,还得记录下原本的开始结束位置,再将新的插进去。繁,还是繁,但是已经比之前逐个字符去遍历的好,正则捕获中本来就有了匹配的索引,直接用即可,还能接受。

但是这只是处理了段落内部标签的问题,段落内肯定还有很多文本是没有处理呢,怎么办?

正则匹配到了只是段落内标签的结果啊,外面的没有啊。哦,对,有匹配到的索引,上次匹配到的位置加上上次处理的长度,就是一段直接文本的开始。下一次匹配到的索引-1就是这段直接文本的结束。这只是匹配过程中的,还有首尾要单独处理。又回到烦的老路上去了。。。

这么烦,一个段落分隔能这么繁琐,我不信!

突然想到了,有文本节点这么个东西,删繁就简嘛,正则先到边上去,直接处理段落的所有节点不就行了。

文本节点则分隔直接包裹,标签节点则对内容进行包裹,这种情况下处理的直接是dom,更省事。

文本节点里放标签?这是在开玩笑么,是也不是。文本节点里确实只能放文本,但是我把标签直接放进去,它会自动转义,那最后再替换出来不就行了。

好了,方案终于有了,而且这个方案逻辑多简单,代码逻辑自然也不会烦。

function splitConent($content) {

$content = $($content);

$content.find(splitConfig.unitTag).each(function (index, item) {

var $item = $(item),

text = $.trim($item.text());

if (!text) return;

var nodes = $item[0].childNodes;

$.each(nodes, function (i, node) {

switch (node.nodeType) {

case 3:

// text 节点

// 由于是文本节点,标签被转义了,后续再转回来

node.data = '<' + splitConfig.wrapTag + '>' +

node.data.replace(splitConfig.splitReg, '$&<' + splitConfig.wrapTag + '>') +

'';

break;

case 1:

// 元素节点

var innerHtml = node.innerHTML,

start = '',

end = '';

// 如果内部还有直接标签,先去掉

var startResult = /^</w+?>/.exec(innerHtml);

if (startResult) {

start = startResult[0];

innerHtml = innerHtml.substr(start.length);

}

var endResult = /<///w+?>$/.exec(innerHtml);

if (endResult) {

end = endResult[0];

innerHtml = innerHtml.substring(0, endResult.index);

}

// 更新内部内容

node.innerHTML = start +

'<' + splitConfig.wrapTag + '>' +

innerHtml.replace(splitConfig.splitReg, '$&<' + splitConfig.wrapTag + '>') +

'' +

end;

break;

default:

break;

}

});

// 处理文本节点中被转义的html标签

$item[0].innerHTML = $item[0].innerHTML

.replace(new RegExp('<' + splitConfig.wrapTag + '>', 'g'), '<' + splitConfig.wrapTag + '>')

.replace(new RegExp('</' + splitConfig.wrapTag + '>', 'g'), '');

$item.find(splitConfig.wrapTag).addClass(splitConfig.wrapCls);

});

}

上面代码中最后对文本节点中被转义的包裹标签替换似乎有点麻烦,但是没办法,ES5之前Javascript并不支持正则的后行断言(也就是正则表达式中“后顾”)。所以没办法对包裹标签前后的 < 和 > 进行精准替换,只能连同标签名一起替换。

事件处理

在上面完成了文本获取和段落分隔,下面要做的就是鼠标移动上去时获取文本触发朗读即可,移开时停止朗读即可。

鼠标移动,只读一次,基于这两点原因,使用 mouseenter 和 mouseleave 事件来完成。

原因:

不冒泡,不会触发父元素的再次朗读

不重复触发,一个元素内移动时不会重复触发。

function createStyle() {

if (document.getElementById('speak-light-style')) return;

var style = document.createElement('style');

style.id = 'speak-light-style';

style.innerText = '.' + splitConfig.hightlightCls + '{' + splitConfig.hightStyle + '}';

document.getElementsByTagName('head')[0].appendChild(style);

}

// 非正文需要朗读的标签 逗号分隔

var speakTags = 'a, p, span, h1, h2, h3, h4, h5, h6, img, input, button';

$(document).on('mouseenter.speak-help', speakTags, function (e) {

var $target = $(e.target);

// 排除段落内的

if ($target.parents('.' + splitConfig.wrapCls).length || $target.find('.' + splitConfig.wrapCls).length) {

return;

}

// 图片样式单独处理 其他样式统一处理

if (e.target.nodeName.toLowerCase() === 'img') {

$target.css({

border: '2px solid #000'

});

} else {

$target.addClass(splitConfig.hightlightCls);

}

// 开始朗读

speakText(getText(e.target));

}).on('mouseleave.speak-help', speakTags, function (e) {

var $target = $(e.target);

if ($target.find('.' + splitConfig.wrapCls).length) {

return;

}

// 图片样式

if (e.target.nodeName.toLowerCase() === 'img') {

$target.css({

border: 'none'

});

} else {

$target.removeClass(splitConfig.hightlightCls);

}

// 停止语音

stopSpeak();

});

// 段落内文本朗读

$(document).on('mouseenter.speak-help', '.' + splitConfig.wrapCls, function (e) {

$(this).addClass(splitConfig.hightlightCls);

// 开始朗读

speakText(getText(this));

}).on('mouseleave.speak-help', '.' + splitConfig.wrapCls, function (e) {

$(this).removeClass(splitConfig.hightlightCls);

// 停止语音

stopSpeak();

});

注意要把针对段落的语音处理和其他地方的分开。为什么? 因为段落是个块级元素,鼠标移入段落中的空白时,如:段落前后空白、首行缩进、末行剩余空白等,是不应该触发朗读的,如果不阻止掉,进行这些区域将直接触发整段文字的朗读,失去了我们对段落文本内分隔的意义,而且,无论什么方式转化语音都是要时间的,大段内容可能需要较长时间,影响语音输出的体验。

文本合成语音

上面我们是直接使用了 speakText(text) 和 stopSpeak() 两个方法来触发语音的朗读和停止。

我们来看下如何实现这个两个功能。

其实现代浏览器默认已经提供了上面功能:

var speechSU = new window.SpeechSynthesisUtterance();

speechSU.text = '你好,世界!';

window.speechSynthesis.speak(speechSU);

复制到浏览器控制台看看能不能听到声音呢?(需要Chrome 33+、Firefox 49+ 或 IE-Edge)

利用一下两个API即可:

SpeechSynthesisUtterance 用于语音合成

lang : 语言 Gets and sets the language of the utterance.

pitch : 音高 Gets and sets the pitch at which the utterance will be spoken at.

rate : 语速 Gets and sets the speed at which the utterance will be spoken at.

text : 文本 Gets and sets the text that will be synthesised when the utterance is spoken.

voice : 声音 Gets and sets the voice that will be used to speak the utterance.

volume : 音量 Gets and sets the volume that the utterance will be spoken at.

onboundary : 单词或句子边界触发,即分隔处触发 Fired when the spoken utterance reaches a word or sentence boundary.

onend : 结束时触发 Fired when the utterance has finished being spoken.

onerror : 错误时触发 Fired when an error occurs that prevents the utterance from being succesfully spoken.

onmark : Fired when the spoken utterance reaches a named SSML "mark" tag.

onpause : 暂停时触发 Fired when the utterance is paused part way through.

onresume : 重新播放时触发 Fired when a paused utterance is resumed.

onstart : 开始时触发 Fired when the utterance has begun to be spoken.

SpeechSynthesis : 用于朗读

paused : Read only 是否暂停 A Boolean that returns true if the SpeechSynthesis object is in a paused state.

pending : Read only 是否处理中 A Boolean that returns true if the utterance queue contains as-yet-unspoken utterances.

speaking : Read only 是否朗读中 A Boolean that returns true if an utterance is currently in the process of being spoken — even if SpeechSynthesis is in a paused state.

onvoiceschanged : 声音变化时触发

cancel() : 情况待朗读队列 Removes all utterances from the utterance queue.

getVoices() : 获取浏览器支持的语音包列表 Returns a list of SpeechSynthesisVoice objects representing all the available voices on the current device.

pause() : 暂停 Puts the SpeechSynthesis object into a paused state.

resume() : 重新开始 Puts the SpeechSynthesis object into a non-paused state: resumes it if it was already paused.

speak() : 读合成的语音,参数必须为SpeechSynthesisUtterance的实例 Adds an utterance to the utterance queue; it will be spoken when any other utterances queued before it have been spoken.

详细api和说明可参考:

MDN - SpeechSynthesisUtterance

MDN - SpeechSynthesis

那么上面的两个方法可以写为:

var speaker = new window.SpeechSynthesisUtterance();

var speakTimer,

stopTimer;

// 开始朗读

function speakText(text) {

clearTimeout(speakTimer);

window.speechSynthesis.cancel();

speakTimer = setTimeout(function () {

speaker.text = text;

window.speechSynthesis.speak(speaker);

}, 200);

}

// 停止朗读

function stopSpeak() {

clearTimeout(stopTimer);

clearTimeout(speakTimer);

stopTimer = setTimeout(function () {

window.speechSynthesis.cancel();

}, 20);

}

因为语音合成本来是个异步的操作,因此在过程中进行以上处理。

现代浏览器已经内置了这个功能,两个API接口兼容性如下:

Feature

Chrome

Edge

Firefox (Gecko)

Internet Explorer

Opera

Safari

(WebKit) Basic

support 33

(Yes)

49 (49)

No support

?

7

如果要兼容其他浏览器或者需要一种完美兼容的解决方案,可能就需要服务端完成了,根据给定文本,返回相应语音即可,百度语音 就提供这样的服务。

举报
收藏 0
打赏 0
评论 0
摘要(Abstract)指的是一篇英国留学毕业论文主要内容的提要或者概要
摘要(Abstract)指的是一篇英国留学毕业论文主要内容的提要或者概要。它的目的是对作者研究论文内容、观点以及结论的简明扼要总结。Abstract一般包括研究目的(Purpose)、研究方法(Methods)、研究结果(Results)和研究结论(Conclusion)。

2023-08-3145

16岁女孩骑摩托带人出事故双双身亡 同乡称刚学会不到1年:曾劝别暴力骑车
4月13日,都市快报报道称,2个JK小姐姐驾驶一辆黄龙600,由于速度过快发生事故。两人均已当场死亡。各位车友骑车注意安全。现场的视频显示:两个女孩倒在街道边的绿化带里,已经没生命特征,周围有不少围观者。一辆摩托车栽倒在路坎上。其中一个女孩的视频帐号,发了十四篇内容,全是骑摩托车的内容。两天前,她发了一条视频动态,写下一句话,“我什么时候才能长高,骑上比我

2023-04-1444

16岁女孩骑摩托带人出事故双双身亡 多位网红飚车都出事:专家痛批这不是流量密码
不知道大家发现了没,现在骑摩托车飚车成了不少网红提升流量的密码,因此造成的悲剧事故也是多了起来。1月5日晚,网传重庆江北区聚贤岩隧道内发生两辆摩托车对撞事故。一名男子和一名女子面部朝下趴在隧道一侧,另一名绿衣女子仰躺在隧道另一侧。隧道外,其中一辆涉事摩托车被撞后发生燃烧,不远处,又有一名佩戴护具的男子仰躺在地上,随后证实四人均已死亡。随后证实,其中一位车手是21岁网红

2023-04-1457

余承东、张朝阳首发体验AITO问界M5智驾版:补足最后一块短板
快科技4月13日消息,真心没想到,AITO问界M5智驾版就这样被余承东给突然曝光了出来。就在今天,余承东本人邀请搜狐创始人张朝阳体验了即将于4月17日发布的AITO问界M5智驾版,此次公开算是预热,但可以看到该车的一些核心信息。外观内饰来看,智驾版更换了全新的LED大灯,造型更为犀利且年轻,同时在车头顶部加入了激光雷达,挡风玻璃顶端还用上了多颗摄像头,可以提供融合感知

2023-04-1350

5月1日起 湖北部分高速公路限速调整:80km/h路段上调至100km/h
近日,湖北省交通运输厅、湖北省公安厅联合发布通告,对部分高速公路限速值予以适当调整,于2023年5月1日起施行。调整内容为:严格遵守高速公路有关法律、法规、标准规定,限速值设置不超过设计时速20km以上,同路段小型汽车与其他机动车限速差一般不大于20km/h;小型汽车限速值不超过120km/h,其他机动车限速值不超过100km/h,摩托车限速值不超过80km/h。20

2023-04-1148

立省4万!商家上线理想L7、L8“激光雷达”改装件:自己都能装
理想L7和L8均分为Max、Pro和Air共三种版本,其中Max版配有激光雷达,可以实现更为高阶的辅助驾驶能力,但Pro和Air版则取消了激光雷达,其他的则和Max版在外观方面保持一致。近日,一博主发现有商家针对这一区别上架了“车顶激光雷达外观件”,并称“帮你省下4万元”。从内容看,该套件针对L7、L8的Pro和Air车

2023-04-0550

一汽丰田官微删除张继科宣传物料 曾任bZ电动车自由大使
4月3日,有网友发现,一汽丰田官方微博同样已删除关于张继科的全部动态,并且下架了与其有关的全部宣传物料。而张继科个人微博,则还保留着相关内容。对此,有媒体报道,一汽丰田相关人士回应称,“正在和销售公司核实。”据悉,张继科曾为一汽丰田纯电动车bZ系列自由大使。张继科事件最新进展3月30日,针对网传“张继科涉嫌欠赌债,将自己和景甜的私密

2023-04-0445

Html5移动端div固定到底部实现底部导航条的几种方式
Html5移动端div固定到底部实现底部导航条的几种方式:这篇文章主要介绍了Html5移动端div固定到底部实现底部导航条的几种方式,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧需求:需要把导航固定在底部?只能滑动内容,导航菜单固定不动的。效果如下:这篇文章主要讲解三种实现方案,包括:fixed,

2023-03-23247

SEO之页面具体做法总结篇
SEO之页面具体做法总结篇:我觉得作为站长大家都或多或少对SEO有些了解,现在seo文章也很多,但是对具体的SEO的方法介绍的文章却不多,本文主要对页面怎么具体做SEO做些介绍,站长尤其是新站长一定要看一下怎么具体的作页面的SEO,强调一下,这些seo的做法不可同时用到你的网站,否则一定会被K的,本人能力也有限,主要目的在于交流,有问题大家可以发信给我好了,好了,现在

2023-03-23224