成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

網頁中文本朗讀功能開發實現分享

開發 前端
前幾天完成了一個需求,在網頁中完成鼠標指向哪里,就用語音讀出所指的文本。如果是按鈕、鏈接、文本輸入框,則還還要給出是什么的提醒。同時針對大段的文本,不能整段的去讀,要按照標點符號進行斷句處理。

[[212817]]

前幾天完成了一個需求,在網頁中完成鼠標指向哪里,就用語音讀出所指的文本。如果是按鈕、鏈接、文本輸入框,則還還要給出是什么的提醒。同時針對大段的文本,不能整段的去讀,要按照標點符號進行斷句處理。

重點當然就是先獲取到當前標簽上的文本,再把文本轉化成語音即可。

標簽朗讀

這個很簡單了,只用根據當前是什么標簽,給出提示即可。

  1. // 標簽朗讀文本 
  2. var tagTextConfig = { 
  3.     'a''鏈接'
  4.     'input[text]''文本輸入框'
  5.     'input[password]''密碼輸入框'
  6.     'button''按鈕'
  7.     'img''圖片' 
  8. }; 

還有需要朗讀的標簽,繼續再添加即可。

然后根據標簽,返回前綴文本即可。

  1. /** 
  2. * 獲取標簽朗讀文本 
  3. * @param {HTMLElement} el 要處理的HTMLElement 
  4. * @returns {String}   朗讀文本 
  5. */ 
  6. function getTagText(el) { 
  7.     if (!el) return ''
  8.  
  9.     var tagName = el.tagName.toLowerCase(); 
  10.  
  11.     // 處理input等多屬性元素 
  12.     switch (tagName) { 
  13.         case 'input'
  14.             tagName += '[' + el.type + ']'
  15.             break; 
  16.         default
  17.             break; 
  18.     } 
  19.  
  20.     // 標簽的功能提醒和作用應該有間隔,因此在***加入一個空格 
  21.     return (tagTextConfig[tagName] || '') + ' '

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

文本內容優先取 title 其次 alt *** innerText

  1. /** 
  2. * 獲取完整朗讀文本 
  3. * @param {HTMLElement} el 要處理的HTMLElement 
  4. * @returns {String}   朗讀文本 
  5. */ 
  6. function getText(el) { 
  7.     if (!el) return ''
  8.  
  9.     return getTagText(el) + (el.title || el.alt || el.innerText || ''); 

這樣就可以獲取到一個標簽的功能提醒和內容的全部帶朗讀文本了。

正文分隔

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

首先準備了正文分隔的配置:

  1. // 正文拆分配置 
  2. var splitConfig = { 
  3.     // 內容分段標簽名稱 
  4.     unitTag: 'p'
  5.     // 正文中分隔正則表達式 
  6.     splitReg: /[,;,;。]/g, 
  7.     // 包裹標簽名 
  8.     wrapTag: 'label'
  9.     // 包裹標簽類名 
  10.     wrapCls: 'speak-lable'
  11.     // 高亮樣式名和樣式 
  12.     hightlightCls: 'speak-help-hightlight'
  13.     hightStyle: 'background: #000!important; color: #fff!important' 
  14. }; 

最開始想的就是直接按照正文中的分隔標點符號進行分隔就好了呀。

想法如下:

  1. 獲取段落全部文本

  2. 使用 split(分隔正則表達式) 方法將正文按照標點符號分隔成小段

  3. 每個小段用標簽包裹放回去即可

然而理想很豐滿,現實很骨感。

兩個大坑如下:

  1. split 方法進行分隔,分隔后分隔字符就丟了,也就是說把原文的一些標點符號給弄丟了。

  2. 如果段落內還存在其他標簽,而這個標簽內部也正好存在待分隔的標點符號,那包裹分段標簽時直接破換了原標簽的完整性。

關于***個問題,丟失標點的符號,考慮過逐個標點來進行和替換 split 分隔方法為逐個字符循環來做。

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

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

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

<p>這是一段測試文本,這里有個鏈接。<a>您好,可以點擊此處進行跳轉</a>還有其他內容其他內容容其他內容容其他內容,容其他內容。</p>

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

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

但是這只是處理了段落內部標簽的問題,段落內肯定還有很多文本是沒有處理呢,怎么辦?

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

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

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

文本節點則分隔直接包裹,標簽節點則對內容進行包裹,這種情況下處理的直接是dom,更省事。

文本節點里放標簽?這是在開玩笑么,是也不是。文本節點里確實只能放文本,但是我把標簽直接放進去,它會自動轉義,那***再替換出來不就行了。

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

 

  1. /** 
  2. * 正文內容分段處理 
  3. * @param {jQueryObject/HTMLElement/String}  $content 要處理的正文jQ對象或HTMLElement或其對應選擇器 
  4. */ 
  5. function splitConent($content) { 
  6.     $content = $($content); 
  7.  
  8.     $content.find(splitConfig.unitTag).each(function (index, item) { 
  9.         var $item = $(item), 
  10.             text = $.trim($item.text()); 
  11.         if (!text) return
  12.  
  13.         var nodes = $item[0].childNodes; 
  14.  
  15.         $.each(nodes, function (i, node) { 
  16.             switch (node.nodeType) { 
  17.                 case 3: 
  18.                     // text 節點 
  19.                     // 由于是文本節點,標簽被轉義了,后續再轉回來 
  20.                     node.data = '<' + splitConfig.wrapTag + '>' + 
  21.                         node.data.replace(splitConfig.splitReg, '</' + splitConfig.wrapTag + '>$&<' + splitConfig.wrapTag + '>') + 
  22.                         '</' + splitConfig.wrapTag + '>'
  23.                     break; 
  24.                 case 1: 
  25.                     // 元素節點 
  26.                     var innerHtml = node.innerHTML, 
  27.                         start = ''
  28.                         end = ''
  29.                     // 如果內部還有直接標簽,先去掉 
  30.                     var startResult = /^<\w+?>/.exec(innerHtml); 
  31.                     if (startResult) { 
  32.                         start = startResult[0]; 
  33.                         innerHtml = innerHtml.substr(start.length); 
  34.                     } 
  35.                     var endResult = /<\/\w+?>$/.exec(innerHtml); 
  36.                     if (endResult) { 
  37.                         end = endResult[0]; 
  38.                         innerHtml = innerHtml.substring(0, endResult.index); 
  39.                     } 
  40.                     // 更新內部內容 
  41.                     node.innerHTML = start + 
  42.                         '<' + splitConfig.wrapTag + '>' + 
  43.                         innerHtml.replace(splitConfig.splitReg, '</' + splitConfig.wrapTag + '>$&<' + splitConfig.wrapTag + '>') + 
  44.                         '</' + splitConfig.wrapTag + '>' + 
  45.                         end
  46.                     break; 
  47.                 default
  48.                     break; 
  49.             } 
  50.         }); 
  51.  
  52.         // 處理文本節點中被轉義的html標簽 
  53.         $item[0].innerHTML = $item[0].innerHTML 
  54.             .replace(new RegExp('&lt;' + splitConfig.wrapTag + '&gt;''g'), '<' + splitConfig.wrapTag + '>'
  55.             .replace(new RegExp('&lt;/' + splitConfig.wrapTag + '&gt;''g'), '</' + splitConfig.wrapTag + '>'); 
  56.         $item.find(splitConfig.wrapTag).addClass(splitConfig.wrapCls); 
  57.     }); 

上面代碼中***對文本節點中被轉義的包裹標簽替換似乎有點麻煩,但是沒辦法,ES5之前JavaScript并不支持正則的后行斷言(也就是正則表達式中“后顧”)。所以沒辦法對包裹標簽前后的 &lt;&gt; 進行精準替換,只能連同標簽名一起替換。

事件處理

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

鼠標移動,只讀一次,基于這兩點原因,使用 mouseentermouseleave 事件來完成。

原因:

  1. 不冒泡,不會觸發父元素的再次朗讀

  2. 不重復觸發,一個元素內移動時不會重復觸發。

  1. /** 
  2. * 在頁面上寫入高亮樣式 
  3. */ 
  4. function createStyle() { 
  5.     if (document.getElementById('speak-light-style')) return
  6.  
  7.     var style = document.createElement('style'); 
  8.     style.id = 'speak-light-style'
  9.     style.innerText = '.' + splitConfig.hightlightCls + '{' + splitConfig.hightStyle + '}'
  10.     document.getElementsByTagName('head')[0].appendChild(style); 
  11. // 非正文需要朗讀的標簽 逗號分隔 
  12. var speakTags = 'a, p, span, h1, h2, h3, h4, h5, h6, img, input, button'
  13.  
  14. $(document).on('mouseenter.speak-help', speakTags, function (e) { 
  15.     var $target = $(e.target); 
  16.  
  17.     // 排除段落內的 
  18.     if ($target.parents('.' + splitConfig.wrapCls).length || $target.find('.' + splitConfig.wrapCls).length) { 
  19.         return
  20.     } 
  21.  
  22.     // 圖片樣式單獨處理 其他樣式統一處理 
  23.     if (e.target.nodeName.toLowerCase() === 'img') { 
  24.         $target.css({ 
  25.             border: '2px solid #000' 
  26.         }); 
  27.     } else { 
  28.         $target.addClass(splitConfig.hightlightCls); 
  29.     } 
  30.  
  31.     // 開始朗讀 
  32.     speakText(getText(e.target)); 
  33.  
  34. }).on('mouseleave.speak-help', speakTags, function (e) { 
  35.     var $target = $(e.target); 
  36.     if ($target.find('.' + splitConfig.wrapCls).length) { 
  37.         return
  38.     } 
  39.  
  40.     // 圖片樣式 
  41.     if (e.target.nodeName.toLowerCase() === 'img') { 
  42.         $target.css({ 
  43.             border: 'none' 
  44.         }); 
  45.     } else { 
  46.         $target.removeClass(splitConfig.hightlightCls); 
  47.     } 
  48.  
  49.     // 停止語音 
  50.     stopSpeak(); 
  51. }); 
  52.  
  53. // 段落內文本朗讀 
  54. $(document).on('mouseenter.speak-help''.' + splitConfig.wrapCls, function (e) { 
  55.     $(this).addClass(splitConfig.hightlightCls); 
  56.  
  57.     // 開始朗讀 
  58.     speakText(getText(this)); 
  59. }).on('mouseleave.speak-help''.' + splitConfig.wrapCls, function (e) { 
  60.     $(this).removeClass(splitConfig.hightlightCls); 
  61.  
  62.     // 停止語音 
  63.     stopSpeak(); 
  64. }); 

注意要把針對段落的語音處理和其他地方的分開。為什么? 因為段落是個塊級元素,鼠標移入段落中的空白時,如:段落前后空白、首行縮進、末行剩余空白等,是不應該觸發朗讀的,如果不阻止掉,進行這些區域將直接觸發整段文字的朗讀,失去了我們對段落文本內分隔的意義,而且,無論什么方式轉化語音都是要時間的,大段內容可能需要較長時間,影響語音輸出的體驗。

文本合成語音

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

我們來看下如何實現這個兩個功能。

其實現代瀏覽器默認已經提供了上面功能:

  1. var speechSU = new window.SpeechSynthesisUtterance(); 
  2. speechSU.text = '你好,世界!'
  3. 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和說明可參考:

那么上面的兩個方法可以寫為:

 

  1. var speaker = new window.SpeechSynthesisUtterance(); 
  2. var speakTimer, 
  3.     stopTimer; 
  4.  
  5. // 開始朗讀 
  6. function speakText(text) { 
  7.     clearTimeout(speakTimer); 
  8.     window.speechSynthesis.cancel(); 
  9.     speakTimer = setTimeout(function () { 
  10.         speaker.text = text; 
  11.         window.speechSynthesis.speak(speaker); 
  12.     }, 200); 
  13.  
  14. // 停止朗讀 
  15. function stopSpeak() { 
  16.     clearTimeout(stopTimer); 
  17.     clearTimeout(speakTimer); 
  18.     stopTimer = setTimeout(function () { 
  19.         window.speechSynthesis.cancel(); 
  20.     }, 20); 

因為語音合成本來是個異步的操作,因此在過程中進行以上處理。

現代瀏覽器已經內置了這個功能,兩個API接口兼容性如下:

Feature

Chrome

Edge

Firefox (Gecko)

Internet Explorer

Opera

Safari

(WebKit) Basic

support 33

(Yes)

49 (49)

No support

?

7

如果要兼容其他瀏覽器或者需要一種***兼容的解決方案,可能就需要服務端完成了,根據給定文本,返回相應語音即可,百度語音 http://yuyin.baidu.com/docs就提供這樣的服務。

責任編輯:龐桂玉 來源: segmentfault
相關推薦

2012-03-07 14:37:03

JavaJavaMail

2012-11-05 10:36:40

IBMdw

2023-04-26 09:37:25

智駕開發

2021-11-02 10:10:49

鴻蒙HarmonyOS應用

2024-08-05 09:51:00

2024-08-08 08:31:32

SpringNeo4j優化

2023-02-28 15:49:09

鴻蒙應用開發

2022-08-09 16:01:24

應用開發鴻蒙

2024-07-26 10:50:51

SpringScrew數據庫

2010-02-05 18:09:28

C++ Doxygen

2022-12-01 17:46:53

網頁變灰功能前端

2010-01-27 18:06:03

Android短信發送

2016-08-11 08:24:39

AndroidIntentShareTestDe

2012-02-08 17:01:36

2012-03-08 21:51:45

Siri

2010-06-10 17:47:48

Zabbix中文

2010-08-30 14:03:59

CSS

2021-12-09 11:58:35

Edge操作系統微軟

2020-12-30 09:35:20

EuiAdmin后端vue框架

2025-03-11 14:45:31

點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 国产亚洲精品成人av久久ww | 国产精品美女在线观看 | 人人干视频在线 | 精品国产一区二区三区性色av | 99热视| 亚洲视频 欧美视频 | 国产成人免费视频网站视频社区 | 日韩中文字幕在线免费 | 亚洲va在线va天堂va狼色在线 | 国产成人精品一区二区三区在线 | 四虎影院美女 | 91免费版在线观看 | 成人一区av | 免费国产视频 | 美女黄网 | 亚洲精品一区二三区不卡 | 亚洲一区二区三区国产 | 一区二区三区在线免费 | 国产激情一区二区三区 | 91免费小视频 | 精品一区二区三区在线观看国产 | 日日碰狠狠躁久久躁96avv | av中文字幕在线观看 | 亚洲精品视频导航 | 亚洲一区视频在线 | 成人3d动漫一区二区三区91 | 久久亚洲一区 | 国产一区视频在线 | 欧美精品福利 | 欧美亚州综合 | 欧美精品一区二区三区在线播放 | 欧美嘿咻 | 国产成人小视频 | 日韩在线播放一区 | 成人一区二区在线 | 伊人伊人伊人 | 视频在线一区二区 | 一区精品视频在线观看 | 农夫在线精品视频免费观看 | 欧美日韩高清在线观看 | 欧美专区在线 |