【ffmpeg】英単語帳をText To Speechで読み上げてmp3化する

2021/05/29に公開

きっかけ

最近の英語参考書には当たり前のように音声教材が付いてます。
ジョギングしながらフレーズを覚えられたりしてちょうどいいですね。
ただ、参考書によってはちょっと利便性が足りてないことがあります。

  • 読み上げの速度
  • 英→日で読んでほしいのか日→英で読んでほしいのか
  • フレーズとフレーズの間隔
  • 特定の読み上げアプリの仕様を強要される

等々。
そこで、読み上げ音源を自作することにしました。

やること

  1. テキストデータの用意
  2. GCPのText To Speech APIで読み上げる
  3. ffmpegで編集する
  4. スマホで聞く

テキストデータの用意

一心不乱に単語帳をGoogle Sheetに打ち込んでいきます。
ここもOCRなどで簡略化できそうですが、
キーボードに打ち込むのも勉強だと思ってやり過ごします。

Text To Speech APIで読み上げる

https://cloud.google.com/text-to-speech/?hl=ja
ここを見てのとおりです。
いろいろ試した所、この設定が一番聞き苦しくない感じでした。

voice name:en-US-Wavenet-C
speed: 0.81

ffmpegで編集する

ディレイ

まず、APIの作った音源は前後に余白が無いので、
このまま聞くとものすごい速さで進んでしまいます。
なので、各フレーズの先頭に1秒ディレイを入れます。

ffmpeg -i [入力.ogg] -af "adelay=10s|10s" [出力.ogg]

concat

各音源を一つに繋げます。
これをすると、シークができなくなってしまうので、
このステップを入れるかどうかはお好みで。

このようなファイルを用意し

file out/0d.ogg
file out/1d.ogg
file out/2d.ogg

このコマンドで一個につながります。

ffmpeg -y -f concat -i list.txt -c copy out/sound.ogg

コード

前提

  • GCPのAPIのお約束で、環境変数に鍵のパスを渡す必要があります
    • export GOOGLE_APPLICATION_CREDENTIALS=~/data/key.json
  • ./data/sheet1.csv が読み上げたい英文のリストだとします
  • 出力用の ./out ディレクトリを作っておきます
  • ffmpegコマンドにパスが通っている必要があります

コード

main.ts
import tts from "@google-cloud/text-to-speech";
import * as fs from "fs";
import * as util from "util";
import { google } from "@google-cloud/text-to-speech/build/protos/protos";
import ISynthesizeSpeechRequest = google.cloud.texttospeech.v1.ISynthesizeSpeechRequest;
import { parse } from "csv/lib/sync";
import { execSync } from "child_process";
import * as Process from "process";

const client = new tts.TextToSpeechClient();

async function main() {
  const lines = readTextLines();
  for (const [index, text] of lines.entries()) {
    const ogg = await textToSpeech(text);
    await putOggFile(index, ogg);
  }
  for (const index of lines.keys()) {
    prependDelay(index);
  }
  const list = createList(lines);
  putListFile(list);
  await concat();
}

function readTextLines(): string[] {
  const data = parse(fs.readFileSync(`data/sheet1.csv`)) as string[][];
  return data.map((d) => d[0]);
}

async function textToSpeech(text: string): Promise<Uint8Array> {
  const request: ISynthesizeSpeechRequest = {
    input: { text: text },
    voice: { languageCode: "en-US", name: "en-US-Wavenet-C" },
    audioConfig: {
      audioEncoding: "OGG_OPUS",
      speakingRate: 0.81,
      pitch: 0,
    },
  };

  const [response] = await client.synthesizeSpeech(request);
  return response.audioContent as Uint8Array;
}

async function putOggFile(index: number, body: Uint8Array) {
  const wf = util.promisify(fs.writeFile);
  await wf(`out/${index}.ogg`, body, "binary");
}

function prependDelay(index: number) {
  // ffmpeg -i 0.ogg -af "adelay=10s|10s" od.ogg
  const cmd = `ffmpeg -y -i out/${index}.ogg -af "adelay=1s|1s" out/${index}d.ogg`;
  console.log(cmd);
  const cp = execSync(cmd);
}

function createList(lines: string[]): string {
  return lines.map((l, i) => `file out/${i}d.ogg`).join("\n");
}

function putListFile(list: string) {
  fs.writeFileSync("concat_list.txt", list);
}

function concat() {
  execSync(`ffmpeg -y -f concat -i concat_list.txt -c copy out/sound.ogg`);
}

main().then((_) => {
  console.log("end");
  Process.exit(0);
});
package.json
{
  "name": "english",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "main": "./node_modules/.bin/ts-node src/main.ts"
  },
  "devDependencies": {
    "prettier": "^2.3.0",
    "ts-node": "^10.0.0",
    "typescript": "^4.3.2"
  },
  "dependencies": {
    "@google-cloud/text-to-speech": "^3.2.1",
    "csv": "^5.5.0"
  }
}

完了

これで、out/sound.oggが出力されます。
いかがでしたか?

Discussion