html Javascript プログラミング

Webページで選択した文章を読み上げるブックマークレットを作った

長文の記事は読み上げのサポートがあると楽

Webで長文の記事を読むのは結構大変ですが、読み上げのサポートがあるとだいぶ楽になります。

特にロングインタビューなんかは、元が音声なので個人的には読み上げに合うと思います。

MacにはもともとOSに読み上げ機能があって、Web上の選択テキストを読み上げることもできます。

しかし、一度読み上げ始めたら一時停止出来ない、進む・戻る・速度変更もできないという不便さがあります。

そこで 選択したテキストを読み上げるブックマークレットをつくりました。

コントロールパレットを使って 一時停止と再会、一文進む、一文戻る、速度変更が行えます。

使い方は簡単です。

ブックマークレットを起動すると、コントロールパレットが表示されます。

先にテキストを選択してあればすぐに読み上げがスタートします。

パレットはドラッグで移動できます。

読み上げたい部分を選択して「Read Aloud」(読み上げ)ボタンを押します。

Stopで停止します。

Resumeで止まった場所から再開します。Read Aloudを押せばまた最初から始まります。

数字は読み上げ速度です。速度変更は次の1文からになります。ちなみに1文は読点を区切りとしています。

Rwdで1文戻し、Fwdで1文先送りします。

時々、パネルをクリックしたときにテキスト選択が外れてしまったりするんですが、原因はよく分かりません。

文章を配列に分けて一文ずつ発声

コードは以下のような感じです。

選択されたテキストを取得したら、ピリオドや改行で分割して配列に入れます。

あとは配列からひとつずつ取り出してWeb Speech APIで発声させます。

これにより、早送り、 巻き戻し、速度変更等が可能になっています。

あと、私はBingブラウザが装備しているボイス"Nanami"が気に入っているので、"Nanami"ボイスが見つかった時には、その声を使うようになっています。

また、どういうわけか文中に“・”(中黒)があると読み上げが止まってしまうので、Web Speech APIに送り前に中黒をスペースに変換しています。

javascript:
(function () {
  //List Voices and Select "Nanami"
  const synth = window.speechSynthesis;
  let selectedVoice = null;
  function populateVoiceList() {
    let voices = synth.getVoices().filter(voice => voice.lang.includes("ja"));
    selectedVoice = voices[0];

    voices.forEach(voice => {
      if (voice.name.includes("Nanami")) {
        selectedVoice = voice;
        synth.defaultVoice = voice;
      }
      console.log(voice.name + " (" + voice.lang + ")");
    });

    // let utt = new SpeechSynthesisUtterance("ボイスリストが更新されました");
    // utt.voice = selectedVoice;
    // synth.speak(utt);
  }
  populateVoiceList();
  speechSynthesis.onvoiceschanged = populateVoiceList;




  // create floating palette
  var newDiv = document.createElement("div");
  newDiv.style.position = "fixed";
  newDiv.style.top = "30px";
  newDiv.style.right = "10px";
  newDiv.style.width = "220px";
  newDiv.style.height = "120px";
  newDiv.style.backgroundColor = "#F0F0F0";
  newDiv.style.border = "3px solid #000000";
  newDiv.style.cursor = "move";
  newDiv.style.padding = "0px 3px 3px 3px";

  var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;

  // create close button
  var closeButton = document.createElement("div");
  closeButton.innerHTML = "✕";
  closeButton.style.width = "fit-content";
  closeButton.style.cursor = "pointer";
  closeButton.onclick = function () {
    document.body.removeChild(newDiv);
  };

  newDiv.appendChild(closeButton);

  // create controller 
  var buttonDiv = document.createElement("div");
  buttonDiv.style.display = "block";
  buttonDiv.style.margin = "3px";

  var speakButton = document.createElement("button");
  speakButton.innerHTML = "Read Aloud";
  speakButton.style.border = "solid 1px black";
  speakButton.style.fontSize = "10px";
  speakButton.style.padding = "5px";
  speakButton.style.marginRight = "3px";

  var stopButton = document.createElement("button");
  stopButton.innerHTML = "Stop";
  stopButton.style.border = "solid 1px black";
  stopButton.style.fontSize = "10px";
  stopButton.style.padding = "5px";
  stopButton.style.marginRight = "3px";

  var resumeButton = document.createElement("button");
  resumeButton.innerHTML = "Resume";
  resumeButton.style.border = "solid 1px black";
  resumeButton.style.fontSize = "10px";
  resumeButton.style.padding = "5px";
  resumeButton.disabled = true;

  buttonDiv.appendChild(speakButton);
  buttonDiv.appendChild(stopButton);
  buttonDiv.appendChild(resumeButton);

  newDiv.appendChild(buttonDiv);

  //create reading rate selector 
  var rateValues = [1.0, 1.25, 1.5, 1.75, 2.0];
  var rateButtons = document.createElement("div");
  rateButtons.style.display = "block";
  rateButtons.style.margin = "3px";

  for (var i = 0; i < rateValues.length; i++) {
    var radioInput = document.createElement("input");
    radioInput.setAttribute("type", "radio");
    radioInput.setAttribute("name", "rate");
    radioInput.setAttribute("value", rateValues[i]);
    radioInput.id = "rate" + rateValues[i];


    if (i == 0) {
      radioInput.checked = true;
    }

    var label = document.createElement('label');
    label.htmlFor = "rate" + rateValues[i];
    label.style.fontSize = "12px";
    label.style.marginRight = "3px";
    label.appendChild(document.createTextNode(rateValues[i].toString()));

    rateButtons.appendChild(radioInput);
    rateButtons.appendChild(label);
  }

  newDiv.appendChild(rateButtons);
  document.body.appendChild(newDiv);

  //create Backward Forward Button

  var buttonDivFwdRwd = document.createElement("div");
  buttonDivFwdRwd.style.display = "block";
  buttonDivFwdRwd.style.margin = "3px";

  var bwdButton = document.createElement("button");
  bwdButton.innerHTML = "Bwd";
  bwdButton.style.border = "solid 1px black";
  bwdButton.style.fontSize = "10px";
  bwdButton.style.padding = "5px";
  bwdButton.style.marginRight = "3px";

  var fwdButton = document.createElement("button");
  fwdButton.innerHTML = "Fwd";
  fwdButton.style.border = "solid 1px black";
  fwdButton.style.fontSize = "10px";
  fwdButton.style.padding = "5px";
  fwdButton.style.marginRight = "3px";

  buttonDivFwdRwd.appendChild(bwdButton);
  buttonDivFwdRwd.appendChild(fwdButton);

  newDiv.appendChild(buttonDivFwdRwd);


  // functions 

  var utteranceArray = [];
  var currentIndex = 0;
  var utterance = null;

  speakButton.onclick = function (e) {
    e.preventDefault();
    speechSynthesis.cancel();
    var textToSpeak = window.getSelection().toString();
    if (!textToSpeak) { console.log("No selection"); }
    utteranceArray = textToSpeak.split(/[\n。]+/).filter(paragraph => paragraph.trim() !== '');
    currentIndex = 0;
    speak();
  };

  stopButton.onclick = function (e) {
    e.preventDefault();
    speechSynthesis.pause();
    resumeButton.disabled = false;
  };

  resumeButton.onclick = function (e) {
    e.preventDefault();
    speechSynthesis.resume();
    this.disabled = true;
  };

  bwdButton.onclick = function () {
    if (currentIndex > 0) {
      speechSynthesis.cancel();
      currentIndex--;
      speak();
    }
  };

  fwdButton.onclick = function (e) {
    e.preventDefault();
    if (currentIndex < utteranceArray.length - 1) {
      speechSynthesis.cancel();
      currentIndex++;
      speak();
    }
  };

  function speak() {
    console.log(currentIndex, utteranceArray.length);
    if (currentIndex < utteranceArray.length) {
      utterance = new SpeechSynthesisUtterance(utteranceArray[currentIndex].replace(/・/g, " "));//中黒によるバグ対策
      var rateValue = document.querySelector('input[name="rate"]:checked').value;
      utterance.rate = rateValue;
      utterance.voice = selectedVoice;
      utterance.onend = function (event) {
        currentIndex++;
        speak();
      }
      // speechSynthesis.speak(utterance);
      synth.speak(utterance);
    }
  }

  newDiv.onmousedown = dragMouseDown;

  function dragMouseDown(e) {
    e.preventDefault();
    e = e || window.event;
    pos3 = e.clientX;
    pos4 = e.clientY;
    document.onmouseup = closeDragElement;
    document.onmousemove = elementDrag;
  }

  function elementDrag(e) {
    e.preventDefault();
    e = e || window.event;
    pos1 = pos3 - e.clientX;
    pos2 = pos4 - e.clientY;
    pos3 = e.clientX;
    pos4 = e.clientY;
    newDiv.style.top = (newDiv.offsetTop - pos2) + "px";
    newDiv.style.left = (newDiv.offsetLeft - pos1) + "px";
  }

  function closeDragElement(e) {
    e.preventDefault();
    document.onmouseup = null;
    document.onmousemove = null;
  }

  speechSynthesis.cancel();
  var textToSpeak = window.getSelection().toString();
  if (!textToSpeak) { console.log("No selection"); }
  utteranceArray = textToSpeak.split(/[\n。]+/).filter(paragraph => paragraph.trim() !== '');
  currentIndex = 0;
  speak();


})();

ブックマークレットにしたものが以下になります。ブックマークのURL欄に貼ってください。

javascript:!function(){const e=window.speechSynthesis;let t=null;function n(){let n=e.getVoices().filter((e=>e.lang.includes("ja")));t=n[0],n.forEach((n=>{n.name.includes("Nanami")&&(t=n,e.defaultVoice=n)}))}n(),speechSynthesis.onvoiceschanged=n;var l=document.createElement("div");l.style.position="fixed",l.style.top="30px",l.style.right="10px",l.style.width="220px",l.style.height="120px",l.style.backgroundColor="#F0F0F0",l.style.border="3px solid #000000",l.style.cursor="move",l.style.padding="0px 3px 3px 3px";var i=0,o=0,c=0,d=0,a=document.createElement("div");a.innerHTML="✕",a.style.width="fit-content",a.style.cursor="pointer",a.onclick=function(){document.body.removeChild(l)},l.appendChild(a);var s=document.createElement("div");s.style.display="block",s.style.margin="3px";var p=document.createElement("button");p.innerHTML="Read Aloud",p.style.border="solid 1px black",p.style.fontSize="10px",p.style.padding="5px",p.style.marginRight="3px";var r=document.createElement("button");r.innerHTML="Stop",r.style.border="solid 1px black",r.style.fontSize="10px",r.style.padding="5px",r.style.marginRight="3px";var u=document.createElement("button");u.innerHTML="Resume",u.style.border="solid 1px black",u.style.fontSize="10px",u.style.padding="5px",u.disabled=!0,s.appendChild(p),s.appendChild(r),s.appendChild(u),l.appendChild(s);var m=[1,1.25,1.5,1.75,2],y=document.createElement("div");y.style.display="block",y.style.margin="3px";for(var h=0;h<m.length;h++){var f=document.createElement("input");f.setAttribute("type","radio"),f.setAttribute("name","rate"),f.setAttribute("value",m[h]),f.id="rate"+m[h],0==h&&(f.checked=!0);var v=document.createElement("label");v.htmlFor="rate"+m[h],v.style.fontSize="12px",v.style.marginRight="3px",v.appendChild(document.createTextNode(m[h].toString())),y.appendChild(f),y.appendChild(v)}l.appendChild(y),document.body.appendChild(l);var x=document.createElement("div");x.style.display="block",x.style.margin="3px";var g=document.createElement("button");g.innerHTML="Bwd",g.style.border="solid 1px black",g.style.fontSize="10px",g.style.padding="5px",g.style.marginRight="3px";var b=document.createElement("button");b.innerHTML="Fwd",b.style.border="solid 1px black",b.style.fontSize="10px",b.style.padding="5px",b.style.marginRight="3px",x.appendChild(g),x.appendChild(b),l.appendChild(x);var S=[],k=0,w=null;function C(){if(k<S.length){w=new SpeechSynthesisUtterance(S[k].replace(/・/g," "));var n=document.querySelector('input[name="rate"]:checked').value;w.rate=n,w.voice=t,w.onend=function(e){k++,C()},e.speak(w)}}function E(e){e.preventDefault(),e=e||window.event,i=c-e.clientX,o=d-e.clientY,c=e.clientX,d=e.clientY,l.style.top=l.offsetTop-o+"px",l.style.left=l.offsetLeft-i+"px"}function T(e){e.preventDefault(),document.onmouseup=null,document.onmousemove=null}p.onclick=function(e){e.preventDefault(),speechSynthesis.cancel();var t=window.getSelection().toString();S=t.split(/[\n。]+/).filter((e=>""!==e.trim())),k=0,C()},r.onclick=function(e){e.preventDefault(),speechSynthesis.pause(),u.disabled=!1},u.onclick=function(e){e.preventDefault(),speechSynthesis.resume(),this.disabled=!0},g.onclick=function(){k>0&&(speechSynthesis.cancel(),k--,C())},b.onclick=function(e){e.preventDefault(),k<S.length-1&&(speechSynthesis.cancel(),k++,C())},l.onmousedown=function(e){e.preventDefault(),e=e||window.event,c=e.clientX,d=e.clientY,document.onmouseup=T,document.onmousemove=E},speechSynthesis.cancel();var D=window.getSelection().toString();S=D.split(/[\n。]+/).filter((e=>""!==e.trim())),k=0,C()}();

-html, Javascript, プログラミング