// 標(biāo)簽朗讀文本

  var tagTextConfig = {

  \\\’a\\\’: \\\’鏈接\\\’,

  \\\’input[text]\\\’: \\\’文本輸入框\\\’,

  \\\’input[password]\\\’: \\\’密碼輸入框\\\’,

  \\\’button\\\’: \\\’按鈕\\\’,

  \\\’img\\\’: \\\’圖片\\\’

  };

  還有需要朗讀的標(biāo)簽,繼續(xù)再添加即可。

  然后根據(jù)標(biāo)簽,返回前綴文本即可。

  /**

  * 獲取標(biāo)簽朗讀文本

  * @param {HTMLElement} el 要處理的HTMLElement

  * @returns {String} 朗讀文本

  */

  function getTagText(el) {

  if (!el) return \\\’\\\’;

  var tagName = el.tagName.toLowerCase();

  // 處理input等多屬性元素

  switch (tagName) {

  case \\\’input\\\’:

  tagName = \\\'[\\\’ el.type \\\’]\\\’;

  break;

  default:

  break;

  }

  // 標(biāo)簽的功能提醒和作用應(yīng)該有間隔,因此在最后加入一個空格

  return (tagTextConfig[tagName] || \\\’\\\’) \\\’ \\\’;

  }

  獲取完整的朗讀文本就更簡單了,先取標(biāo)簽的功能提醒,再取標(biāo)簽的文本即可。

  文本內(nèi)容優(yōu)先取 title 其次 alt 最后 innerText。

  /**

  * 獲取完整朗讀文本

  * @param {HTMLElement} el 要處理的HTMLElement

  * @returns {String} 朗讀文本

  */

  function getText(el) {

  if (!el) return \\\’\\\’;

  return getTagText(el) (el.title || el.alt || el.innerText || \\\’\\\’);

  }

  這樣就可以獲取到一個標(biāo)簽的功能提醒和內(nèi)容的全部帶朗讀文本了。

  正文分隔

  接下來要處理的就是正文分隔了,在這個過程中,踩了不少坑,走了不少彎路,好好記錄一下。

  首先準(zhǔn)備了正文分隔的配置:

  // 正文拆分配置

  var splitConfig = {

  // 內(nèi)容分段標(biāo)簽名稱

  unitTag: \\\’p\\\’,

  // 正文中分隔正則表達(dá)式

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

  // 包裹標(biāo)簽名

  wrapTag: \\\’label\\\’,

  // 包裹標(biāo)簽類名

  wrapCls: \\\’speak-lable\\\’,

  // 高亮樣式名和樣式

  hightlightCls: \\\’speak-help-hightlight\\\’,

  hightStyle: \\\’background: #000!important; color: #fff!important\\\’

  };

  最開始想的就是直接按照正文中的分隔標(biāo)點(diǎn)符號進(jìn)行分隔就好了呀。

  想法如下:

  獲取段落全部文本

  使用 split(分隔正則表達(dá)式) 方法將正文按照標(biāo)點(diǎn)符號分隔成小段

  每個小段用標(biāo)簽包裹放回去即可

  然而理想很豐滿,現(xiàn)實(shí)很骨感。

  兩個大坑如下:

  split 方法進(jìn)行分隔,分隔后分隔字符就丟了,也就是說把原文的一些標(biāo)點(diǎn)符號給弄丟了。

  如果段落內(nèi)還存在其他標(biāo)簽,而這個標(biāo)簽內(nèi)部也正好存在待分隔的標(biāo)點(diǎn)符號,那包裹分段標(biāo)簽時(shí)直接破換了原標(biāo)簽的完整性。

  關(guān)于第一個問題,丟失標(biāo)點(diǎn)的符號,考慮過逐個標(biāo)點(diǎn)來進(jìn)行和替換 split 分隔方法為逐個字符循環(huán)來做。

  前者問題是原本一次完成的工作分成了多次,效率太低。第二種感覺效率更低了,分隔本來是很稀疏的,但是卻要變成逐個字符出判斷處理,更關(guān)鍵的是,分隔標(biāo)點(diǎn)的位置要插入包裹標(biāo)簽,會導(dǎo)致字符串長度變化,還要處理下標(biāo)索引。代碼是機(jī)器跑的,或許不會覺得煩,但是我真的覺得好煩。如果這么干,或許以后哪個AI或者同事看到這樣的代碼,說不定會說“這真是個傻xxxx”。

  第二個問題想過很多辦法來補(bǔ)救,如先使用正則匹配捕獲內(nèi)容中成對的標(biāo)簽,對標(biāo)簽內(nèi)部的分隔先處理一遍,然后再處理整個的。

  想不明白問題二的,可參考一下待分隔的段落:

  這是一段測試文本,這里有個鏈接。您好,可以點(diǎn)擊此處進(jìn)行跳轉(zhuǎn)還有其他內(nèi)容其他內(nèi)容容其他內(nèi)容容其他內(nèi)容,容其他內(nèi)容。

 

  如先使用/<((w ?)>)(. ?)</2(?=>)/g 正則,依次捕獲段落內(nèi)被標(biāo)簽包裹的內(nèi)容,對標(biāo)簽內(nèi)部的內(nèi)容先處理。

  但是問題又來了,這么處理的都是字符串,在js中都是基本類型,這些操作進(jìn)行的時(shí)候都是在復(fù)制的基礎(chǔ)上進(jìn)行的,要修改到原字符串里去,還得記錄下原本的開始結(jié)束位置,再將新的插進(jìn)去。繁,還是繁,但是已經(jīng)比之前逐個字符去遍歷的好,正則捕獲中本來就有了匹配的索引,直接用即可,還能接受。

  但是這只是處理了段落內(nèi)部標(biāo)簽的問題,段落內(nèi)肯定還有很多文本是沒有處理呢,怎么辦?

  正則匹配到了只是段落內(nèi)標(biāo)簽的結(jié)果啊,外面的沒有啊。哦,對,有匹配到的索引,上次匹配到的位置加上上次處理的長度,就是一段直接文本的開始。下一次匹配到的索引-1就是這段直接文本的結(jié)束。這只是匹配過程中的,還有首尾要單獨(dú)處理。又回到煩的老路上去了。。。

  這么煩,一個段落分隔能這么繁瑣,我不信!

  突然想到了,有文本節(jié)點(diǎn)這么個東西,刪繁就簡嘛,正則先到邊上去,直接處理段落的所有節(jié)點(diǎn)不就行了。

  文本節(jié)點(diǎn)則分隔直接包裹,標(biāo)簽節(jié)點(diǎn)則對內(nèi)容進(jìn)行包裹,這種情況下處理的直接是dom,更省事。

  文本節(jié)點(diǎn)里放標(biāo)簽?這是在開玩笑么,是也不是。文本節(jié)點(diǎn)里確實(shí)只能放文本,但是我把標(biāo)簽直接放進(jìn)去,它會自動轉(zhuǎn)義,那最后再替換出來不就行了。

  好了,方案終于有了,而且這個方案邏輯多簡單,代碼邏輯自然也不會煩。

  /**

  * 正文內(nèi)容分段處理

  * @param {jQueryObject/HTMLElement/String} $content 要處理的正文jQ對象或HTMLElement或其對應(yīng)選擇器

  */

  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 節(jié)點(diǎn)

  // 由于是文本節(jié)點(diǎn),標(biāo)簽被轉(zhuǎn)義了,后續(xù)再轉(zhuǎn)回來

  node.data = \\\'<\\\’ splitConfig.wrapTag \\\’>\\\’

  node.data.replace(splitConfig.splitReg, \\\’$&<\\\’ splitConfig.wrapTag \\\’>\\\’)

  \\\’\\\’;

  break;

  case 1:

  // 元素節(jié)點(diǎn)

  var innerHtml = node.innerHTML,

  start = \\\’\\\’,

  end = \\\’\\\’;

  // 如果內(nèi)部還有直接標(biāo)簽,先去掉

  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);

  }

  // 更新內(nèi)部內(nèi)容

  node.innerHTML = start

  \\\'<\\\’ splitConfig.wrapTag \\\’>\\\’

  innerHtml.replace(splitConfig.splitReg, \\\’$&<\\\’ splitConfig.wrapTag \\\’>\\\’)

  \\\’\\\’

  end;

  break;

  default:

  break;

  }

  });

  // 處理文本節(jié)點(diǎn)中被轉(zhuǎn)義的html標(biāo)簽

  $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);

  });

  }

  上面代碼中最后對文本節(jié)點(diǎn)中被轉(zhuǎn)義的包裹標(biāo)簽替換似乎有點(diǎn)麻煩,但是沒辦法,ES5之前JavaScript并不支持正則的后行斷言(也就是正則表達(dá)式中“后顧”)。所以沒辦法對包裹標(biāo)簽前后的 < 和 > 進(jìn)行精準(zhǔn)替換,只能連同標(biāo)簽名一起替換。

  事件處理

  在上面完成了文本獲取和段落分隔,下面要做的就是鼠標(biāo)移動上去時(shí)獲取文本觸發(fā)朗讀即可,移開時(shí)停止朗讀即可。

  鼠標(biāo)移動,只讀一次,基于這兩點(diǎn)原因,使用 mouseenter 和 mouseleave 事件來完成。

  原因:

  不冒泡,不會觸發(fā)父元素的再次朗讀

  不重復(fù)觸發(fā),一個元素內(nèi)移動時(shí)不會重復(fù)觸發(fā)。

  /**

  * 在頁面上寫入高亮樣式

  */

  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);

  }

  // 非正文需要朗讀的標(biāo)簽 逗號分隔

  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);

  // 排除段落內(nèi)的

  if ($target.parents(\\\’.\\\’ splitConfig.wrapCls).length || $target.find(\\\’.\\\’ splitConfig.wrapCls).length) {

  return;

  }

  // 圖片樣式單獨(dú)處理 其他樣式統(tǒng)一處理

  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();

  });

  // 段落內(nèi)文本朗讀

  $(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();

  });

  注意要把針對段落的語音處理和其他地方的分開。為什么? 因?yàn)槎温涫莻€塊級元素,鼠標(biāo)移入段落中的空白時(shí),如:段落前后空白、首行縮進(jìn)、末行剩余空白等,是不應(yīng)該觸發(fā)朗讀的,如果不阻止掉,進(jìn)行這些區(qū)域?qū)⒅苯佑|發(fā)整段文字的朗讀,失去了我們對段落文本內(nèi)分隔的意義,而且,無論什么方式轉(zhuǎn)化語音都是要時(shí)間的,大段內(nèi)容可能需要較長時(shí)間,影響語音輸出的體驗(yàn)。

  文本合成語音

  上面我們是直接使用了 speakText(text) 和 stopSpeak() 兩個方法來觸發(fā)語音的朗讀和停止。

  我們來看下如何實(shí)現(xiàn)這個兩個功能。

  其實(shí)現(xiàn)代瀏覽器默認(rèn)已經(jīng)提供了上面功能:

  var speechSU = new window.SpeechSynthesisUtterance();

  speechSU.text = \\\’你好,世界!\\\’;

  window.speechSynthesis.speak(speechSU);

  復(fù)制到瀏覽器控制臺看看能不能聽到聲音呢?(需要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 : 單詞或句子邊界觸發(fā),即分隔處觸發(fā) Fired when the spoken utterance reaches a word or sentence boundary.

  onend : 結(jié)束時(shí)觸發(fā) Fired when the utterance has finished being spoken.

  onerror : 錯誤時(shí)觸發(fā) 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 : 暫停時(shí)觸發(fā) Fired when the utterance is paused part way through.

  onresume : 重新播放時(shí)觸發(fā) Fired when a paused utterance is resumed.

  onstart : 開始時(shí)觸發(fā) 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 : 聲音變化時(shí)觸發(fā)

  cancel() : 情況待朗讀隊(duì)列 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() : 讀合成的語音,參數(shù)必須為SpeechSynthesisUtterance的實(shí)例 Adds an utterance to the utterance queue; it will be spoken when any other utterances queued before it have been spoken.

  詳細(xì)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);

  }

  因?yàn)檎Z音合成本來是個異步的操作,因此在過程中進(jìn)行以上處理。

  現(xiàn)代瀏覽器已經(jīng)內(nèi)置了這個功能,兩個API接口兼容性如下:

  Feature

  Chrome

  Edge

  Firefox (Gecko)

  Internet Explorer

  Opera

  Safari

  (WebKit) Basic

  support 33

  (Yes)

  49 (49)

  No support

  ?

  7

  如果要兼容其他瀏覽器或者需要一種完美兼容的解決方案,可能就需要服務(wù)端完成了,根據(jù)給定文本,返回相應(yīng)語音即可。

更多關(guān)于云服務(wù)器域名注冊,虛擬主機(jī)的問題,請?jiān)L問三五互聯(lián)官網(wǎng):m.shinetop.cn

贊(0)
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享網(wǎng)絡(luò)內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。郵箱:3140448839@qq.com。本站原創(chuàng)內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明出處:三五互聯(lián)知識庫 » 網(wǎng)頁中文本朗讀功能開發(fā)和實(shí)現(xiàn)

登錄

找回密碼

注冊

主站蜘蛛池模板: 小嫩批日出水无码视频免费| 亚洲狼人久久伊人久久伊| 亚洲高清最新AV网站| 南木林县| 日韩熟妇| 久久精品国产亚洲AV麻豆长发| 欧美大胆老熟妇乱子伦视频| 国日韩精品一区二区三区| 高清在线一区二区三区视频| 国产999久久高清免费观看| 国产精品美女久久久久久麻豆| 老熟妇国产一区二区三区 | 人妻熟女一二三区夜夜爱| 中文字幕日韩人妻一区| 精品无码一区二区三区水蜜桃| 久久国产自偷自偷免费一区| 国产精品免费中文字幕| 国产成人一区二区三区| 亚洲成在人天堂一区二区| 国产很色很黄很大爽的视频| 国产成人午夜福利院| 免费国产一级 片内射老| 老色鬼在线精品视频| 大城县| 久久精品国产99精品亚洲| 国产在线观看免费人成视频| 国产不卡一区二区在线| 国产成人精品午夜二三区| 国产精品人成视频免费播放| 国外av片免费看一区二区三区| 一区二区三区国产不卡| 日韩 高清 无码 人妻| 久草热大美女黄色片免费看| 国产精品一区二区久久精品无码| 欧美大bbbb流白水| 国产av亚洲一区二区| 亚洲精品自拍在线视频| 亚洲无码在线免费观看| 亚洲avav天堂av在线网爱情| 宜都市| 国产一区二区三区美女|