TweetDeckを読み上げるやつ・改

だいぶ前にTweetDeckのタイムラインを読み上げるブックマークレットの記事を書きましたが、

roodni.hatenablog.com

使ってみると

  • ホームタイムラインが複数あると同時に読み上げられて理解が追いつかない
  • 読み上げに時間のかかるツイートがある(例: 同じアルファベットの繰り返し)

など問題点がいくつかあったのでさらに改造しました。

このコードをコピペしてブックマークに登録すると使えます。デスクトップのGoogle ChromeFirefoxで動作確認済みです。 使い方は前回と同じで、TweetDeckのタブで実行すると読み上げが開始され、再度実行すると読み上げが中止されます。

圧縮していないソースコード(折り畳み)

(()=>{
  const stateKey = Symbol.for("read");

  if (window[stateKey] === undefined) {
    const createUttr = (name, text) => {
      const pitchTableKey = Symbol.for("pitch");
      if (window[pitchTableKey] === undefined) {
        window[pitchTableKey] = new Map();
      }
      const pitchTable = window[pitchTableKey];
      let pitch = pitchTable.get(name);
      if (pitch === undefined) {
        pitch = Math.random() * 0.4 + 0.8;
        pitchTable.set(name, pitch);
      }
      // console.log("name = %s, pitch = %f", name, pitch);
      const uttr = new SpeechSynthesisUtterance(text);
      uttr.pitch = pitch;
      uttr.lang = "ja-JP";
      return uttr;
    };

    const queue = [];
    const queueMax = 10;
    let timeout = false;
    const speakNext = () => {
      if (queue.length === 0) {
        // console.log("queue is empty");
        return;
      }
      if (queue.length > queueMax) {
        // console.log("queue overflow");
        queue.length = 0;
        speechSynthesis.cancel();
        return;
      }
      const uttr = queue.pop();
      const timer = setTimeout(() => {
        // console.log("timeout");
        timeout = true;
        if (queue.length >= 1) {
          speechSynthesis.cancel();
        }
      }, 5000);
      uttr.addEventListener("end", () => {
        // console.log("end: ", uttr.text);
        clearTimeout(timer);
        speakNext();
      });
      timeout = false;
      speechSynthesis.speak(uttr);
    }

    const extractText = node => {
      const tweetElement = node.firstElementChild.firstElementChild;
      const name = tweetElement.getElementsByClassName("fullname")[0].textContent;
      const text = tweetElement.getElementsByClassName("js-tweet-text")[0].textContent;
      return {name, text};
    };
    const target = document.getElementsByClassName("js-chirp-container");
    const isHomeTimeline = container => container.parentNode.parentNode.previousElementSibling.textContent.trim().substr(0, 4) === "Home";
    const columns = [...target].filter(isHomeTimeline);

    if (columns.length >= 1) {
      const columnNames = columns.map((c, i) =>
        `${i}: ${c.parentNode.parentNode.previousElementSibling.textContent.trim()}`
      );
      const input = (columns.length === 1) ? 0 : prompt(`タイムラインを選択してください\n${columnNames.join("\n")}`, 0);
      const column = columns[parseInt(input)] || columns[0];
      const observer = new MutationObserver(mutations => {
        for (mutation of mutations) {
          Array.from(mutation.addedNodes).reverse().forEach(tweetNode => {
            const {name, text} = extractText(tweetNode);
            // console.log(`text: "${text}"`);
            if (text !== "") {
              const uttr = createUttr(name, text);
              queue.unshift(uttr);
              if (!speechSynthesis.speaking) {
                speakNext();
              } else if (timeout) {
                speechSynthesis.cancel();
              }
            }
          });
        }
      });
      observer.observe(column, {childList: true});
      window[stateKey] = {
        observers: [observer],
        queue: queue
      };
      queue.unshift(createUttr("", "読み上げをはじめます"));
      speakNext();
    } else {
      alert("ホームタイムラインが存在しません");
    }
  } else {
    window[stateKey].observers.forEach(observer => { observer.disconnect(); });
    window[stateKey].queue.length = 0;
    window[stateKey] = undefined;
    speechSynthesis.cancel();
    alert("読み上げを中止しました");
  }
})()

変更点

  • ホームタイムラインが複数ある場合どれを読み上げるか選択できるようにしました。
  • ツイートの読み上げに5秒以上かかっているときに新しいツイートが来たら中断して新しいツイートを優先するようにしました。
  • タイムラインを下にスクロールすると大量のツイートが読み上げられてしまう問題を改善しました。
  • 声の選択機能を消去しました。
  • Android版のGoogle Chromeに対応しました。

雑記

  • FirefoxではSpeechSynthesisUtterancelang"ja-JP"に設定してあげないと日本語を読んでくれないみたいです。
  • スクリプトをminifyするのにはuglify-esを使いましたが、残念なことにuglify-esはNull合体演算子??に対応していないようです。なおuglify-esの派生元であるuglify-jsはES5専用で、アロー関数式にすら対応してないので雑なブックマークレット制作には使えそうにありませんでした。
  • スマホで使うにはTweetDeckをメインで開き続けている必要があり、使い勝手は非常に悪いです。PWA化すれば便利な読み上げアプリに化けるかもと一瞬考えましたが、このブックマークレットはTweetDeckに依存しているやつなのでやはり難しそうです。