Skip to content

Instantly share code, notes, and snippets.

@shunito
Created July 23, 2017 23:33
Show Gist options
  • Save shunito/f68ea64c9ac870091beb97e4eff5a321 to your computer and use it in GitHub Desktop.
Save shunito/f68ea64c9ac870091beb97e4eff5a321 to your computer and use it in GitHub Desktop.
BiB/i Extension: TTS
/*!
*
* # 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(); // いったんストップ
});
});
@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