Greasy Fork is available in English.

Seisho Reader

biblica multilanguage TTS reader

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name             Seisho Reader
// @description      biblica multilanguage TTS reader
// @namespace        https://userscript.snomiao.com/
// @version          0.0.1
// @author           [email protected]
// @icon             https://www.google.com/s2/favicons?sz=64&domain=biblica.com
// @match            https://www.biblica.com/bible/*
// @grant            none
// @run-at           document-end
// @contributionURL  https://snomiao.com/donate
// @supportURL       https://github.com/snomiao/userscript/issues
// ==/UserScript==

/* eslint-disable */

// seisho-reader
(function () {
  "use strict";
  console.log("Seisho Reader Loaded");
  main();
  async function main() {
    const instance = (globalThis.seishoReader = +new Date());
    speechSynthesis.getVoices();
    speechSynthesis.cancel();
    function $$(selector = "*", parent = document.body) {
      return Array.from(parent.querySelectorAll(selector));
    }
    while (instance === globalThis.seishoReader) {
      try {
        console.log("Seisho Reader Waiting");
        await new Promise((r) => setTimeout(r, 3e3));
        console.log("Seisho Reader Scanning");
        const cols = [...$$(".bible-api .scripture-styles")];
        const sc = cols.map((ss) => {
          const e = ss.cloneNode(true);
          return (
            $$(".v,p.s1", e).map(
              (e2) =>
                (e2.textContent = ((s) => `
${s} `)(e2.textContent))
            ),
            (
              ss.parentElement?.querySelector("h3")?.textContent +
              "\n" +
              e.textContent
            )
              .trim()
              .split("\n")
              .flatMap((s, i, a) => [
                ...s
                  .split(/(?<=[、,。!]+|, |; )/)
                  .map(
                    (h, j) =>
                      String(
                        Number(
                          s.match(/^\d+/)?.[0] ||
                            a[i - 1]?.match(/^\d+/)?.[0] ||
                            "0"
                        )
                      ).padStart(3, "0") +
                      String(j).padStart(2, "0") +
                      " " +
                      h.replace(/^\d+/, "")
                  ),
                String(
                  Number(
                    s.match(/^\d+/)?.[0] || a[i - 1]?.match(/^\d+/)?.[0] || "0"
                  )
                ).padStart(3, "0") +
                  "99 " +
                  s,
              ])
              .flatMap((sen, i, a) =>
                ["0SLOW", "1FAST"].map(
                  (SPEE, STAGE) =>
                    String(
                      Number(
                        sen.match(/^\d+/)?.[0] ||
                          a[i - 1]?.match(/^\d+/)?.[0] ||
                          "0"
                      )
                    ).padStart(5, "0") +
                    " " +
                    SPEE +
                    String(STAGE).padStart(2, "0") +
                    " " +
                    String(
                      ss.parentElement
                        ?.querySelector("button")
                        ?.textContent?.replace("NIV", "0NIV")
                        .replace("JCB", "1JCB") ?? ""
                    ).padEnd(5, "_") +
                    " >> " +
                    sen.replace(/^\d+/, "")
                )
              )
              .map((e2) => e2.trim())
          );
        });
        const t = zip2(sc[0], sc[1]);
        console.log("Seisho Reader Reading");
        console.table(t);
        const nn = t.flat().sort().filter(Boolean);
        console.log(nn);
        for await (const e of nn) {
          if (instance !== globalThis.seishoReader) return;
          const [head, sentext] = e.split(">>").map((e2) => e2.trim());
          const lang = head.match(/JCB/)
            ? "ja"
            : head.match(/CCBT/)
            ? "zh-TW"
            : "en";
          const rate = head.match(/SLOW/) ? 0.75 : 1;
          const text = head.match(/SLOW/)
            ? sentext.split("").join("")
            : sentext;
          const voice = [...speechSynthesis.getVoices()]
            .reverse()
            .filter((e2) => e2.lang.match(lang))
            .sort(() => (Math.random() > 0.5 ? 1 : -1))[0];
          if (!voice) {
            throw new Error("wait for voice");
            await new Promise((r) => setTimeout(r, 1e4));
          }
          console.log(voice.name + ":");
          console.log(lang, rate, text);
          if (!text) continue;
          const uttr = new SpeechSynthesisUtterance(text);
          uttr.voice = voice;
          uttr.rate = rate;
          const t0 = +new Date();
          while (speechSynthesis.speaking) {
            if (+new Date() - t0 > 3e4) break;
            await new Promise((r) => setTimeout(r, 100));
          }
          speechSynthesis.cancel();
          speechSynthesis.speak(uttr);
        }
        const nextbtn = $$("a.next")[0];
        if (!nextbtn) throw new Error("nextbtn is not found");
        nextbtn.click();
      } catch (error) {
        console.error(error);
      }
      await new Promise((r) => setTimeout(r, 1e4));
    }
  }
  function zip2(a, b) {
    return a.length >= b.length
      ? a.map((e, i) => [e, b[i]])
      : b.map((e, i) => [e, a[i]]);
  }
})();