小説家になろうの小説を誰かに朗読してもらえないかと思いついたので、ゆっくりに朗読してもらった。
なにをやるのか
- ブラウザでなろうページを開く
- ユーザスクリプトで文章を転送する
- ユーザスクリプトで棒読みちゃんボタンを設置する
- 棒読みちゃんボタンクリック時に文章を転送する
- ローカルで実行中の棒読みちゃんで朗読する
棒読みちゃんの設定
- 棒読みちゃんをインストールする
- 棒読みちゃん v0.1.11.0 β13 以上
- アプリケーション連動を有効にする(初期設定で有効)
BouyomiChan-WebSocket-Pluginを導入するPlugin_WebSocket.dllをBouyomiChan.exeと同じフォルダに配置する
- 棒読みちゃんをローカル環境で起動しておく
朗読用ユーザスクリプト
なろう棒読みちゃんボタン.user.js// ==UserScript==
// @name なろう棒読みちゃんボタン
// @description 小説家になろうを棒読みちゃんを使用して朗読する。
// 棒読みちゃん用のWebSocket受付プラグインの導入が必要です。
// @match *://ncode.syosetu.com/*/*
// @match *://novel18.syosetu.com/*/*
// @author toshi (https://github.com/k08045kk)
// @license MIT License | https://opensource.org/licenses/MIT
// @version 7
// @since 1 - 20160210 - 初版
// @since 2 - 20180423 - httpsからhttpへの遷移の記述をコメントアウト状態で追加
// @since 3 - 20180509 - リファクタリング
// @since 4 - 20190226 - 棒読みちゃんへの転送を遅延する(長時間待機後の転送でエラーする)
// @since 5 - 20201218 - 朗読内容を微調整(タイトルなし、強調ルビなし、短編にボタン追加)
// @since 5 - 20201218 - fix 自動朗読が動作していない
// @since 6 - 20210416 - HTTPS対応 + 文章分割を改善
// @since 7 - 20220210 - アプリケーション連動対応
// @see https://www.bugbugnow.net/2018/02/blog-post_10.html
// @grant none
// ==/UserScript==
(function() {
if (document.getElementById('novel_honbun') == null) {
// 対象ページではない時
return;
}
// 棒読みちゃん(アプリケーション連動)
const sendBouyomiChanAppLinkage = async (text) => {
const callee = sendBouyomiChanAppLinkage;
let ret = true;
try {
text = text.replace(/ /g, ' '); // 「 」が「+」に変換されて行間が崩れる問題を回避
const url = new URL('http://localhost:50080/Talk');
url.searchParams.set('text', text); // 文字列
url.searchParams.set('voice', 0); // 声質( 0:デフォルト, 1~8:AquesTalk, 10001~:SAPI5)
url.searchParams.set('volume',-1); // 音量(-1:デフォルト, 0~100)
url.searchParams.set('speed', -1); // 速度(-1:デフォルト, 50~300)
url.searchParams.set('tone', -1); // 音程(-1:デフォルト, 50~200)
await fetch(url);
} catch (e) {
ret = false;
if (!callee.error) {
callee.error = true;
alert('棒読みちゃんへの送信に失敗しました');
}
}
return ret;
};
// 文章を分割する
const segmenter = function*(text, segments, max) {
segments = segments || ['。','、','.',',','\n'];
max = max || 256;
const indexs = segments.map(() => 0);
const len = text.length;
let slen, next, prev = 0;
while (prev + max < len) {
// 区切り文字で分割する
// 分割不可(最大文字数分の文字列を出力する)
next = prev + max;
for (let s=0; s<segments.length; s++) {
slen = segments[s].length;
if (indexs[s] === -1) {
// 区切り文字なし(捜索済み)
continue;
} else if (prev < indexs[s]) {
if (indexs[s]+slen - prev < max) {
// 区切り文字を範囲内で発見(捜索済み)
next = indexs[s];
break;
} else {
// 区切り文字を範囲外で発見(捜索済み)
continue;
}
} else if ((indexs[s]=text.indexOf(segments[s], prev)) === -1) {
// 対象文字なし
continue;
} else if (indexs[s]+slen - prev < max) {
// 区切り文字を範囲内で発見
next = indexs[s];
while (true) {
// 範囲内で最後の区切り文字まで進める
indexs[s] = text.indexOf(segments[s], next+slen);
if (indexs[s] === -1) {
break;
} else if (indexs[s]+slen - prev < max) {
next = indexs[s];
} else {
break;
}
}
break;
} else {
// 区切り文字を範囲外で発見
continue;
}
}
// 分割文字列を出力する
if (next + slen <= prev + max) {
next += slen;
}
yield text.substring(prev, next);
prev = next;
}
if (prev < len) {
yield text.substring(prev, len);
}
};
// 作者情報を取得(再生ボタンを配置する前に取得)
let title = '';
try {
title = document.querySelector('.contents1').innerText;
} catch (e) {}
const onBouyomiChanButton = async function() {
//console.log('なろう棒読みちゃんボタン');
// 2重クリック防止
this.disabled = true;
let text = '';
// タイトル+作者名を追加
// タイトルなし(長いタイトルを毎回読むのが嫌なため)
//text += title + '…。';
// サブタイトルを追加
try {
let subtitle = document.querySelector('.novel_subtitle').innerText;
text += subtitle + '…。';
} catch (e) {}
// 本文を追加
// ルビあり文字の原文を削除(ルビあり文字を2重に読み上げるのを回避)
const novel = document.getElementById('novel_honbun').cloneNode(true);
novel.querySelectorAll('ruby rp').forEach((rp) => rp.remove());
novel.querySelectorAll('ruby').forEach((ruby) => {
const rb = ruby.querySelector('rb');
const rt = ruby.querySelector('rt');
if (!rt) {
} else if (rt.innerText.replace(/[・、]+/g, '') == '') {
// 傍点ならば、原文を優先する
rt.remove();
} else if(rb) {
rb.remove();
}
});
text += novel.innerText;
text += "…。\n以上、棒読みちゃんによる朗読でした。";
text = text.replace(/\r?\n\s*/g, '\n'); // 通信量を削減
// 一定文字数をこえると、プラグインがインデックス範囲外でハングするため、複数回送信する
// (文字数だけで区切ると単語の途中で送信してしまう可能性があるため)
// (単語の途中で送信してしまうと、棒読みちゃんの発音が意図しないものとなる可能性が高い)
const segments = ['\n\n','。','?','!','、','」', '】',')','…','・','.','?','!',',','}','\n',' '];
for (let s of segmenter(text, segments, 200)) {
// 休止符の連続使用を制限
s = s.replace(/\n\n\n+/g, '\n\n\n')
.replace(/・・・+/g, '・・・')
.replace(/、、、+/g, '、、、')
.replace(/。。。+/g, '。。。')
.replace(/……+/g, '……');
if (s.trim() == '') { continue; }
if (!(await sendBouyomiChanAppLinkage(s))) {
break;
}
}
// 一定の文字列を超えた場合、それ以降朗読しなくなる問題がある
// 例:http://ncode.syosetu.com/n4006r/12/
};
// 再生ボタンを配置
const element = document.querySelector('.contents1') || document.querySelector('.novel_title');
element.innerHTML += `
<div style="float:right">
<style>
#bouyomichan {
padding: .25em 1em;
background: #0076bf;
color: #fff;
cursor: pointer;
}
#bouyomichan:disabled {
background: #ccc;
color: #000;
}
</style>
<button id="bouyomichan">棒読みちゃん</button>
</div>`;
document.getElementById('bouyomichan').addEventListener('click', onBouyomiChanButton);
// 自動朗読
// URLにbouyomichan=trueを指定するとページ移動で自動朗読する
var url = new URL(location.href);
if (url.searchParams.has('bouyomichan')) {
onBouyomiChanButton.call(document.getElementById('bouyomichan'));
document.querySelectorAll('#novel_color .novel_bn a').forEach((v) => v.href+='?bouyomichan');
}
})();
変更履歴
更新日 | 説明 |
---|---|
2018/02/10 | v1 - 初版 |
2018/04/23 | v2 - httpsからhttpへの遷移の記述をコメントアウト状態で追加 |
2018/05/09 | v3 - リファクタリング httpsからhttpへの遷移の記述をコメントアウト解除 etc... |
2019/02/26 | v4 - 棒読みちゃんへの転送を遅延する (長時間待機後に連続で転送するとエラーことがあるため) |
2020/12/18 | v5 - 朗読内容を微調整(タイトルなし、強調ルビなし、短編にボタン追加) |
2020/12/18 | v5 - fix 自動朗読が動作していない |
2021/04/16 | v6 - HTTPS対応 + 文章分割を改善 |
2022/02/10 | v7 - アプリケーション連動対応 |
補足
自動再生
URL末尾に?bouyomichan
を追加すれば、ページ移動で自動的に朗読を開始します。
ただし、朗読の完了を検出できないため、完全な自動化はできていません。棒読みちゃんの API には、朗読中か判定するものがあるため、プラグイン側を変更できれば完全自動なページ移動が可能だと思われます。
※大量に文字列を送信しすぎると、棒読みちゃんが朗読しなくなる問題があります。
そのため、複数ページ分すべて送信するのは、あまり現実的ではありません。
他サイトへの対応
青空文庫・カクヨムなどの他ウェブ小説への対応は、時期未定です。必要にかられたら対応します。
個別サイト対応とは違いますが、選択文字列をコンテキストメニューから転送する拡張機能もあります。