mp4からPythonで文字起こし

mp4動画からPythonで文字起こししてみた。探せばやり方は山ほどあるので、今回はより実用的に、どんなデータでも使えるよう工夫してみた。

課題

Python で文字起こしするには、recognize_googleを使うのが一般的。ただしこのAPIはファイルサイズが大きすぎるとエラーになる。目安としては5MBくらい。

Speech Recognition Python Having Strange Request Error - Stack Overflow

なので長いファイルは自動で分割したい。ただし、変なところで分割してしまうと、文の途中で切れて文字起こしに悪い影響が出てしまう。そのため、文の切れ目で切っていく。

実装

mp4→wav

recognize_googleではwavファイルしか使用できないので変換する。

参考にさせていただいたサイト様↓

【簡単】Pythonで動画から音声(mp3・wav)を抽出する方法 | ジコログ

FFmpegffmpeg-pythonをインストールしたら、以下を実行。

import ffmpeg 

# 入力 
stream = ffmpeg.input("org.mp4") 
# 出力 
stream = ffmpeg.output(stream, "voice.wav") 
# 実行 
ffmpeg.run(stream)

ファイル分割

ファイルを分割していく。この時、「文の切れ目」を「0.1秒間声を出していない時」と考える。まずは声の出ている時と出ていないの閾値を決めるため、音声ファイルをグラフ化する。

参考にさせていただいたサイト様↓

Pythonで音声解析 – 音声データの周波数特性を調べる方法 |じょるブログ

import scipy.io.wavfile
import numpy as np
import matplotlib.pyplot as plt
 
wav_filename = "voice.wav"
rate, data = scipy.io.wavfile.read(wav_filename)
#縦軸(振幅)の配列を作成   #16bitの音声ファイルのデータを-1から1に正規化
data = data / 32768

データをプロット

time = np.arange(0, data.shape[0]/rate, 1/rate)  
plt.plot(time, data[:,0])
plt.xlim(0, 140)
plt.show()

このままでは大きすぎてわからないので拡大する。

time = np.arange(0, data.shape[0]/rate, 1/rate)  
#データプロット
plt.plot(time, data[:,0])
plt.xlim(0, 5)
plt.ylim(0, 0.25)
plt.show()

最小値は0.4あたりなので、余裕を持たせて0.25に設定する。

分割する秒数を取り出す。rangは動画の最低長さ。これがないと細切れになってしまう。 intervalは無言と思われる秒数。

rang = 20
times = []
s_time = -1
e_time = -1
tmp_e_time = -1
interval = 0.1

for t, v in zip(time, data[:,0]):
    if v > 0.025 and s_time == -1:
        s_time = t - 0.1
        if s_time < 0:
            s_time = 0
        tmp_e_time = -1
    if v < 0.025 and s_time != -1 and t - s_time > rang:
        if tmp_e_time == -1:
            tmp_e_time = t
        if t - tmp_e_time > interval :
            e_time = tmp_e_time + 0.1
            if e_time > time[-1]:
                e_time = time[-1]
            times.append([s_time, e_time])
            s_time = -1
            e_time = -1
            tmp_e_time = -1
            
print(len(times))
total =0
for s,e in times:
    total += e-s
print(times[-1], total)

文字起こし

最後に動画を分割しながら文字起こししていく。分割の注意点として、フレーム数はフレームレートの倍数である必要がある。今回は小数点以下の秒数を扱うので、フレームレートの倍数になるようフレームを付け足している。

参考にさせていただいたサイト様↓

【Python】WAVファイルを等間隔に分割するプログラム【サウンドプログラミング】 - ぽきたに 〜ありきたりな非凡〜

Pythonで音声からテキストへ変換【SpeechRecognition】 | ジコログ

import wave
import struct
import math
import os
from scipy import fromstring, int16

def cut_wav(filename, times):
    # timeの単位は[sec]

    # ファイルを読み出し
    wavf = filename
    wr = wave.open(wavf, 'r')

    # waveファイルが持つ性質を取得
    ch = wr.getnchannels()
    width = wr.getsampwidth()
    fr = wr.getframerate()
    fn = wr.getnframes()
    total_time = 1.0 * fn / fr
    integer = math.floor(total_time) # 小数点以下切り捨て

    # waveの実データを取得し、数値化
    data = wr.readframes(wr.getnframes())
    wr.close()
    X = fromstring(data, dtype=int16)

    for i, (s, e) in enumerate(times):
        frames = int(ch * fr * s-e)
        # 出力データを生成
        outf = 'tmp{}.wav'.format(i) 
        start_cut = int(fr * s)
        end_cut = int(fr * e)
        end_cut += (end_cut-start_cut)%fr
        Y = X[start_cut:end_cut]
        outd = struct.pack("h" * len(Y), *Y)

        # 書き出し
        with wave.open(outf, 'w') as ww:
            ww.setnchannels(ch)
            ww.setsampwidth(width)
            ww.setframerate(fr)
            ww.writeframes(outd)
            
        # 文字起こし
        r = sr.Recognizer()
        with sr.AudioFile(outf) as source:
            audio = r.record(source)
        text = r.recognize_google(audio, language='ja-JP')
        print(text)
        
filename = "voice.wav"
cut_wav(filename,times)

テスト

実際に動かしてみる。今回はういビーム検出器を作りたかったので以下の動画を使用してテスト。

www.youtube.com

結果は以下の通り。なぜかういビームだけが検出されてない気がする。。

渡部
君もまた 女バージョンだよ 今ねえよなんで勝手になったら怒られるんだから また目が怒られるんだから
8男性しても悪くないよ
本人より ランドセル
一日一年幸せでいられますように
まだ声に宿りし 羽衣の石は現在を持ってきてその辺を示せ
汝この世の不条理を増加するもの何

文字起こし部分がおかしいのか、BGMとかSEのせいでうまくいってないのか、今後調査していく(予定)