⛳
【ffmpeg】英単語帳をText To Speechで読み上げてmp3化する
きっかけ
最近の英語参考書には当たり前のように音声教材が付いてます。
ジョギングしながらフレーズを覚えられたりしてちょうどいいですね。
ただ、参考書によってはちょっと利便性が足りてないことがあります。
- 読み上げの速度
- 英→日で読んでほしいのか日→英で読んでほしいのか
- フレーズとフレーズの間隔
- 特定の読み上げアプリの仕様を強要される
等々。
そこで、読み上げ音源を自作することにしました。
やること
- テキストデータの用意
- GCPのText To Speech APIで読み上げる
- ffmpegで編集する
- スマホで聞く
テキストデータの用意
一心不乱に単語帳をGoogle Sheetに打ち込んでいきます。
ここもOCRなどで簡略化できそうですが、
キーボードに打ち込むのも勉強だと思ってやり過ごします。
Text To Speech APIで読み上げる
いろいろ試した所、この設定が一番聞き苦しくない感じでした。
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