Created
July 23, 2017 23:33
-
-
Save shunito/f68ea64c9ac870091beb97e4eff5a321 to your computer and use it in GitHub Desktop.
BiB/i Extension: TTS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*! | |
* | |
* # BiB/i Extension: OverReflow | |
* | |
* - "Overlays Reflowable Content Layers on Pre-Paginated Book" | |
* - Copyright (c) Satoru MATSUSHIMA - http://bibi.epub.link/ or https://github.com/satorumurmur/bibi | |
* - Licensed under the MIT license. - http://www.opensource.org/licenses/mit-license.php | |
*/ | |
Bibi.x({ | |
name: "Text To Speech", | |
description: "text to speech for BiB/i", | |
version: "0.1.0", | |
build: 20170611.0001 | |
})(function() { | |
var tts = false; | |
var MAX_TEXT = 40; | |
var speakList = []; | |
var highlightColor = '#FFD700'; | |
var msg = new SpeechSynthesisUtterance(); | |
X.TTS = {}; | |
X.TTS.isSpeeking = false; | |
X.TTS.Type = "TEXT"; | |
X.TTS.Rate = 1.5; | |
msg.volume = 1; | |
msg.rate = 1.5; | |
msg.pitch = 1; | |
msg.lang = 'ja-JP'; // default | |
O.appendStyleSheetLink({ | |
className: "bibi-extension-stylesheet", | |
id: "bibi-extension-stylesheet_TTS", | |
href: O.RootPath + "extensions/tts/tts.css" | |
}); | |
if (!'SpeechSynthesisUtterance' in window) { | |
O.log(2, "plugin Error - TTS Plugin:" + 'Web Speech API is disabled' ); | |
return; | |
} | |
if(S["use-cookie"]) { | |
var BibiCookie = O.Cookie.remember(O.RootPath); | |
if(BibiCookie && BibiCookie.TTS && BibiCookie.TTS.Rate != undefined){ | |
msg.rate = X.TTS.Rate = BibiCookie.TTS.Rate * 1; | |
} | |
} | |
// defaulr Rate | |
if(typeof X.TTS.Rate != "number" || X.TTS.Rate < 0.5 || X.TTS.Rate > 10 ) { | |
msg.rate = X.TTS.Rate = 1.5; | |
} | |
I.Menu.TTS = {}; | |
var TTSButtonGroup = I.createButtonGroup({ Area: I.Menu.R, Sticky: true }); | |
// Menu Button | |
var TTSButton = TTSButtonGroup.addButton({ | |
Type: "toggle", | |
Labels: { | |
default: { default: 'TTS', ja: '読み上げ' }, | |
active: { default: 'Close Share-Menu', ja: '読み上げメニューを閉じる' } | |
}, | |
Help: true, | |
Icon: '<span class="bibi-icon bibi-icon-speech"></span>' | |
}); | |
var TTSSubPanel = I.createSubPanel({ | |
Opener: TTSButton, | |
id: "bibi-subpanel_tts", | |
open: function() { | |
console.log("open tts sub panel"); | |
} | |
}); | |
I.Menu.TTS.Speech = TTSSubPanel.addSection({ | |
Labels: { default: { default: 'Text To Speech', ja: '読み上げ' } }, | |
ButtonGroup: { | |
Tiled: false, | |
Buttons: [ | |
{ | |
Type: "toggle", | |
Labels: { | |
default: { | |
default: 'Start Speech', | |
ja: '読み上げを開始' | |
}, | |
active: { | |
default: 'Stop Speech', | |
ja: '読み上げを停止' | |
} | |
}, | |
Icon: '<span class="bibi-icon bibi-icon-speech-on"></span>', | |
action: function() { | |
X.TTS.isSpeeking = !X.TTS.isSpeeking; | |
TTSSubPanel.close(); | |
if( X.TTS.isSpeeking ){ | |
startTTS(); | |
} | |
else{ | |
speechSynthesis.cancel(msg); | |
} | |
}, | |
on: { click: function() { return false; } } | |
}, | |
{ | |
Type: "toggle", | |
Labels: { | |
default: { | |
default: 'Speech Ruby text', | |
ja: 'ルビも読み上げする' | |
}, | |
active: { | |
default: 'Speech non-ruby text only', | |
ja: 'ルビは読み上げない' | |
} | |
}, | |
Icon: '<span class="bibi-icon bibi-icon-speech-on"></span>', | |
action: function() { | |
speechSynthesis.cancel(msg); | |
if( X.TTS.Type === "TEXT" ){ | |
X.TTS.Type = "RUBY"; | |
} | |
else{ | |
X.TTS.Type = "TEXT" | |
} | |
if( X.TTS.isSpeeking ){ | |
X.TTS.isSpeeking = false; | |
setTimeout(function(){ | |
X.TTS.isSpeeking = true; | |
startTTS(); | |
}, 1000); | |
} | |
}, | |
on: { click: function() { return false; } } | |
}, | |
] | |
} | |
}); | |
I.Menu.TTS.Rate = TTSSubPanel.addSection({ | |
Labels: { default: { default: 'Speech Rate', ja: '読み上げ速度' } }, | |
ButtonGroup: { | |
Tiled: true, | |
Buttons: [ | |
{ | |
Type: "radio", | |
Labels: { | |
default: { | |
default: '<span class="non-visual-in-label">Speech Rate:</span> Normal', | |
ja: '<span class="non-visual-in-label">読み上げ速度:</span>標準' | |
} | |
}, | |
Icon: '', | |
Rate: 1, | |
action: changeSpeechRate | |
}, | |
{ | |
Type: "radio", | |
Labels: { | |
default: { | |
default: '<span class="non-visual-in-label">Speech Rate:</span> 1.5 speed', | |
ja: '<span class="non-visual-in-label">読み上げ速度:</span>1.5倍速' | |
} | |
}, | |
Icon: '', | |
Rate: 1.5, | |
action: changeSpeechRate | |
}, | |
{ | |
Type: "radio", | |
Labels: { | |
default: { | |
default: '<span class="non-visual-in-label">Speech Rate:</span> 2 speed', | |
ja: '<span class="non-visual-in-label">読み上げ速度:</span>2倍速' | |
} | |
}, | |
Icon: '', | |
Rate: 2, | |
action: changeSpeechRate | |
}, | |
{ | |
Type: "radio", | |
Labels: { | |
default: { | |
default: '<span class="non-visual-in-label">Speech Rate:</span> 3 speed', | |
ja: '<span class="non-visual-in-label">読み上げ速度:</span>3倍速' | |
} | |
}, | |
Icon: '', | |
Rate: 3.0, | |
action: changeSpeechRate | |
} | |
] | |
} | |
}); | |
I.Menu.TTS.Rate.ButtonGroup.Buttons.forEach(function(Button) { | |
if(Button.Rate == X.TTS.Rate) I.setUIState(Button, "active"); | |
}); | |
function changeSpeechRate(){ | |
var Button = this; | |
var Rate = Button.Rate; | |
msg.rate = X.TTS.Rate = Rate; | |
if(S["use-cookie"]) { | |
O.Cookie.eat(O.RootPath, { TTS: { Rate: Rate } }); | |
} | |
} | |
function getDisplayType (element) { | |
var cStyle = element.currentStyle || window.getComputedStyle(element, ""); | |
return cStyle.display; | |
} | |
function stopSpeech(){ | |
var buttons = I.Menu.TTS.Speech.ButtonGroup.Buttons; | |
speechSynthesis.cancel(msg); | |
X.TTS.isSpeeking = false; | |
I.setUIState( buttons[0], "default"); | |
} | |
function nextPage(){ | |
var hasNext = R.Current.Pages.EndPage != R.Pages[R.Pages.length - 1] || R.Current.Pages.EndPageRatio != 100; | |
var endPage = R.Current.Pages.EndPage; | |
var startPage = R.Current.Pages.StartPage; | |
var page = R.getCurrent(); | |
var nextIndex = page.Page.PageIndex ; | |
var currentItemNo; | |
speakList = []; | |
speakList.length = 0; | |
//console.log("-- Next Page --" ); | |
// console.log(" pageIndex ", page.Page.PageIndex ); | |
// console.log(" start Page", startPage ); | |
// console.log(" start Page Index", startPage.PageIndex ); | |
// console.log(" end Page", endPage ); | |
// console.log(" end Page Index", endPage.PageIndex ); | |
// console.log("--Item--"); | |
// console.log("currentPage:Item:" , page.Page.Item); | |
//console.log("currentPage:Item Index:" , page.Page.Item.ItemIndex); | |
//console.log("StartPage :Item Index:" , startPage.Item.ItemIndex); | |
currentItemNo = startPage.Item.ItemIndex +1; | |
//console.log("currentItemNo :" , currentItemNo); | |
nextIndex = currentItemNo +1; | |
//console.log( "Next item index :: ", nextIndex, hasNext ); | |
if( hasNext ){ | |
R.focusOn(nextIndex); | |
setTimeout( startTTS, 1000); | |
} | |
else{ | |
stopSpeech(); | |
} | |
} | |
function speak( num, page ){ | |
var i =0, l = speakList.length; | |
var text , node , parent, span; | |
var st , displayType; | |
var pages; | |
if( num >= l ) { | |
nextPage(); | |
return true; | |
} | |
if( !X.TTS.isSpeeking ){ return; } | |
node = speakList[num][0]; | |
text = speakList[num][1]; | |
parent = node.parentElement; | |
st = parent.style; | |
st.backgroundColor = highlightColor; | |
R.focusOn({ | |
Destination:{ | |
Element: parent, | |
Page: page.Page | |
} | |
}); | |
//console.log("speak: ",text); | |
msg.text = text; | |
msg.onerror = function(event){ | |
return false; | |
} | |
msg.onstart = function(event){ | |
speaking = true; | |
} | |
msg.onend = function (event) { | |
speaking = false; | |
st.backgroundColor = ''; | |
// Next Text | |
setTimeout(function(){ | |
speak( num + 1, page ); | |
}, 10); | |
} | |
// Speak! | |
speechSynthesis.speak(msg); | |
} | |
function checkNode( node ){ | |
var text = ''; | |
var type = node.nodeType; | |
var name = node.nodeName; | |
var childNodes ,tagName; | |
var subText, i, l, c, str ,tmp; | |
var attrs; | |
if( type === 1 ) { // Element Node | |
attrs = node.attributes; | |
//console.log( node, attrs ); | |
// SSML | |
if( attrs["ssml:ph"] ){ | |
text = attrs["ssml:ph"].value; | |
//console.log(" SSML -- ", text ); | |
speakList.push([node, text]); | |
return; | |
} | |
tagName = node.tagName.toUpperCase(); | |
if( tagName === 'SCRIPT' || tagName === 'RP'){ | |
//console.log('skip tag ->' ,tagName); | |
return; | |
} | |
if( tagName === 'RT' ){ | |
if( X.TTS.Type === "TEXT"){ | |
//console.log('skip tag ->' ,tagName); | |
return; | |
} | |
} | |
if( tagName === 'IMG' ){ | |
text = node.alt; | |
if( text.length > 0){ | |
speakList.push([node, text]); | |
} | |
return; | |
} | |
} | |
if( type === 3 ) { // TEXT Node | |
text = node.nodeValue; | |
if( text.trim().length > 0 ){ | |
if( text.length < MAX_TEXT ){ | |
speakList.push([node, text]); | |
} | |
else{ | |
i=0; l= text.length; | |
str = ''; | |
subText = []; | |
for(i=0; i<l; i++){ | |
c = text.charAt(i); | |
if( msg.lang === 'ja-JP' && c.match(/[\s,。、(「(]/) ){ | |
//console.log( 'sub', str + c ); | |
subText.push(str + c); | |
str = ''; | |
} | |
else if( c.match(/[“".,(]/) ){ | |
subText.push(str + c); | |
str = ''; | |
} | |
else{ | |
str += c; | |
} | |
} | |
subText.push(str); | |
l = subText.length; | |
str = tmp =''; | |
for( i=0;i<l; i++){ | |
if( subText[i].trim() ==='' ){ continue; } | |
tmp = str + subText[i].trim(); | |
if( tmp.length > MAX_TEXT ){ | |
if( str.length > 0){ | |
speakList.push([node, str.trim()]); | |
} | |
str = subText[i]; | |
} | |
else{ | |
str = tmp; | |
} | |
} | |
if( str.length > 0 ){ | |
speakList.push([node, str.trim()]); | |
} | |
} | |
} | |
return; | |
} | |
if( typeof node.childNodes !== 'undefined' ){ | |
childNodes = node.childNodes; | |
for( n in childNodes ) { | |
checkNode( childNodes[n] ); | |
} | |
} | |
} | |
function startTTS(){ | |
var content = "start TTS"; | |
var currentPage = R.getCurrent(); | |
var currentPages = R.getCurrentPages(); | |
var pages = currentPage.Pages; | |
var i,l , item , body; | |
var currentItemNo; | |
// console.log("--start TTS--"); | |
// console.log("currentPage:Index:" , currentPage.Page.PageIndex); | |
// console.log("currentPage::" , currentPage); | |
// console.log("--Item--"); | |
// console.log("currentPage:Item:" , currentPage.Page.Item); | |
// console.log("currentPage:Item Index:" , currentPage.Page.Item.ItemIndex); | |
// console.log("StartPage :Item Index:" , currentPages.StartPage.Item.ItemIndex); | |
currentItemNo = currentPages.StartPage.Item.ItemIndex +1; | |
// console.log("currentItemNo :" , currentItemNo); | |
//console.log("currentPages::" , pages); | |
// console.log("start index::" , currentPages.StartPage.PageIndex); | |
// console.log("start page: " ,currentPages.StartPage); | |
// console.log("start body: " ,currentPages.StartPage.Item.Body); | |
// console.log("end index::" , currentPages.EndPage.PageIndex); | |
// console.log("end page: " ,currentPages.EndPage); | |
// console.log("end body: " ,currentPages.EndPage.Item.Body); | |
speakList = []; | |
speakList.length = 0; | |
/* | |
for( i=0; i<l; i++ ){ | |
item = pages[i].Item; | |
body = item.Body; | |
console.log( "page ", i , body); | |
checkNode(body); | |
} | |
*/ | |
checkNode( currentPages.StartPage.Item.Body ); | |
//console.table(speakList); | |
speak( 0, currentPage ); | |
} | |
//bibi:commands:move-by | |
E.bind("bibi:commands:move-by",function(){ | |
console.log("moved"); | |
stopSpeech(); // いったんストップ | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@charset "utf-8"; | |
@import "../../res/styles/_common-lib"; | |
$scaling: 1.44; | |
.bibi-icon-speech { | |
&:before { | |
@include font-icon("ElegantIcons"); | |
content: "\e069"; // | |
font-size: $icon-size * 16/31; | |
display: block; | |
position: absolute; | |
@include trbl(-100%); | |
margin: auto; | |
width: 1em; | |
height: 1em; | |
line-height: 1.1; | |
} | |
} | |
.bibi-icon-speech-on{ | |
&:before { | |
@include font-icon("ElegantIcons"); | |
content: "\e069"; // | |
font-size: $icon-size * 16/31; | |
display: block; | |
position: absolute; | |
@include trbl(-100%); | |
margin: auto; | |
width: 1em; | |
height: 1em; | |
line-height: 1.1; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment