Pebble Coding

プログラマーの作業メモ

iOS10のSpeechToText APIを使って日本語PodCastをテキスト化してみる

iOS10で公開されたSpeechフレームワークを使って日本語PodCastをテキスト化してみました。 結果から言うと、テキスト化は可能でしたが、品質はかなり悪く実用として厳しかったです。 某IT系PodCastのファイルを食わせて見たんですが、 「ジャバスクリプト」とか「ディープラーニング」とか一般的でない語彙はほぼ認識できませんでした。

SFSpeechiRecognizerというクラスを使います。 音声ファイルを指定すると、iOSバイスからAppleサーバへリクエストが投げられ、 結果のテキストが得られます。

受け付ける音声ファイルの長さは60sec以内にしないといけないので、 PodCastのmp3などの音声ファイルを20secで区切って順番に処理する戦略をとります。 この方法でAPI制限に引っかかることはありませんでした。

基本のソースコードAppleのSampleCodeを見るか、WWDC2016のセッションを見るか、 ググるとわかりますので端折ります。

SpeakToMe: Using Speech Recognition with AVAudioEngine

ここでは、mp3ファイルを20secのファイルに区切る処理のソースだけを示しておきます。

import Foundation
import AudioToolbox
import AVFoundation

open class AudioDivider {
    init(url:URL) {
        self.url = url
    }
    private var url:URL
    
    public final class func diskCachePath(cacheName: String) -> String {
        let dstPath = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first!
        return (dstPath as NSString).appendingPathComponent(cacheName)
    }
    
    public func execute() -> [URL] {
        var urls:[URL] = []
        var inref: ExtAudioFileRef?
        var outref: ExtAudioFileRef?
        var st:OSStatus
        st = ExtAudioFileOpenURL(url as CFURL, &inref)
        var index = 0
        let pcmfmt:AVAudioFormat = AVAudioFormat.init(commonFormat: AVAudioCommonFormat.pcmFormatInt16,
                                                   sampleRate: 44100.0,
                                                   channels: 1,
                                                   interleaved: false)
        // read format setting
        guard let inref_unwrap = inref else {
            assert(false)
        }
        st = ExtAudioFileSetProperty(inref_unwrap,
                                     kExtAudioFileProperty_ClientDataFormat,
                                     UInt32(MemoryLayout<AudioStreamBasicDescription>.size),
                                     pcmfmt.streamDescription)
        let bufferByteSize:UInt32 = 1024
        var srcBuffer = [UInt8](repeating:0, count: 1024)
        // file loop
        var eof:Bool = false
        while true {
            let cachepath:String = AudioDivider.diskCachePath(cacheName:"")
            let outpath:String = "\(cachepath)/divider\(index).wav"
            let outurl:URL = URL(fileURLWithPath: outpath)
            urls.append(outurl)
            st = ExtAudioFileCreateWithURL(outurl as CFURL,
                                           kAudioFileWAVEType,
                                           pcmfmt.streamDescription,
                                           nil,
                                           AudioFileFlags.eraseFile.rawValue,
                                           &outref)
            guard let outref_unwrap = outref else {
               assert(false) 
            }
            var writeFrame:UInt32 = 0
            // data loop
            while true {
                let ab = AudioBuffer(mNumberChannels: 1, mDataByteSize: bufferByteSize, mData: &srcBuffer)
                var abl = AudioBufferList(mNumberBuffers: 1, mBuffers: ab)
                var numFrames: UInt32 = 0
                if pcmfmt.streamDescription.pointee.mBytesPerFrame > 0 {
                    numFrames = bufferByteSize / pcmfmt.streamDescription.pointee.mBytesPerFrame
                }
                st = ExtAudioFileRead(inref_unwrap, &numFrames, &abl)
                if numFrames <= 0 {
                    eof = true
                    break
                }
                st = ExtAudioFileWrite(outref_unwrap, numFrames, &abl)
                writeFrame += numFrames
                // terminate the file in 20sec 
                if writeFrame > 44100 * 20 {
                    break
                }
            }
            ExtAudioFileDispose(outref_unwrap)
            index += 1
            if eof {
                break
            }
        }
        ExtAudioFileDispose(inref_unwrap)
        return urls
    }
}

GoogleのSpeechToText API

Speech API - Speech Recognition  |  Google Cloud Platform

は精度がいいらしいですが、無料枠が1ヶ月で1hourしかないため、試す気になりませんでした。 ※1ヶ月で1hourまで無料 以降、$0.006/15sec (=$1.44/1hour)