티스토리 뷰

iOS

iOS10 ) STT(Speech-To-Text) 구현

Zedd0202 2017. 3. 17. 14:50
반응형

안녕하세요!! 오늘은 TTS에 이어서 STT를 만들어보겠습니다!!!ㅎㅎㅎ

TTS보다는 복잡하더라구요. 그리고 저는 외부 api(네이버나..구글)는 쓰지않았습니다.

애플에서 제공하는 speech 프레임워크 있답니다. 저는 이걸 썼어요.

나중에 네이버 speech api도 한번 써볼려고해요 :)

재밌을 것 같지 않나요?!XD

그럼 시작해볼게요.



 STT(Speech-To-Text) 




먼저 프로젝트를 열어주시고


버튼과 텍스트뷰를 추가해주세요!

저 말하기 버튼을 누르고 말하면 

텍스트뷰에 제가 말하는게 적히는 

간단한 앱이에요.


제가 위에서 speech 프레임워크를 썼다고 말씀드렸죠?




speech를 import 해주세요.

그리고 info.plist에 가셔서, 

Privacy - Speech Recognition Usage Description Privacy - Microphone Usage Description를 추가해주세요!


그러면,

이렇게 뜬답니다. ㅎㅎ



다시 코드로 돌아가서.. 

그리고 delegate채택 작업도 잊지마세요!

SFSpeechRecognizerDelegate가 뭐하는 프로토콜인지 한번 봤더니..



일단

@available(iOS 10.0, *)이게 보이네요?


이 코드가 의미하는 바는 아시죠? iOS10이상부터 가능하다는 말입니다.

그리고 함수가 딱 하나있네요? 

음..이 함수가 하는일은 


"주어진 인식기의 유효성에 변화가 일어날 때 호출된다"

?..



일단 주어진 인식기가 뭔지부터 봅시다.

파라미터를 보니 SFSpeechRecognizer네요.

이게 뭔지 한번볼까요?




NSObject이고.. 잘은 모르겠지만 이 객체가 변하면 저 SFSpeechRecognizerDelegate안에 있는 speechRecognizer함수가 자동으로 불려지는 것 같네요 :)



자, 다음으로 넘어가볼까요 XD

우선, 위에서 말했던 '인식기'가 필요하겠죠?

그럼 이 인식기는 한국어를 인식할까요?아니면 영어?


우리는 이 인식기에게 무슨 '언어'를 인식할건지 정해줘야 한답니다.


저 위의 스크린샷을 잘 살펴보면,



음성 인식의 로케일(위치)를 지원.

지원되는 것이 현재 사용 가능한 것은 아닙니다. 일부 로케일은 인터넷 연결이 필요할 수 있습니다.

라고 하네요.


그래서!우리는 어느나라 말을 인식할 건지 제일 먼저 정해줘야 한답니다:)


private let speechRecognizer = SFSpeechRecognizer(locale: Locale.init(identifier: "ko-KR"))    

저는 한국어를 인식하도록 할게요!!


그리고.. 또 뭐가 필요할까요?


일단 우리는 마이크에대고 '한국어'를 말할거죠?

그리고 어떤 프로세스를 거쳐 우리가 말한 '한국어'를 인식하여

텍스트뷰에 써주는 기능을 하는것이 이 앱입니다.


일단 '한국어'처리는 했고...

그럼 이제 우리가 말한 것을 인식하는 부분을 구현해야겠네요.



그럼 이제 할 일은 음성을 인식하는 부분을 처리해야합니다.

일단 우리가 말하는 것을 들어야겠죠??


private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest?


이 객체는 음성인식요청을 처리한답니다. 
제가 말을 하면, 이 recognitionRequest에서 음성인식 프로세스를 진행하는 거죠.



private var recognitionTask: SFSpeechRecognitionTask?


리고 이 객체는 인식 요청 결과를 제공합니다. 

인식요청결과? 그게 뭘까요?

SFSpeechRecognitionTask에 뭐가 있는지 살펴볼까요?





SFSpeechRecognitionTask의 state는 SFSpeechRecognitionTaskState타입인데,

SFSpeechRecognitionTaskState에는 여러 상태들이 있네요. 


starting부터 completed까지!

네, 보시는바와 같이 이 개체를 사용하면 중간에 인식을 취소하거나

중지 할 수 있으므로 편리해진답니다 :)


private let audioEngine = AVAudioEngine()


그리고  이것이 바로 정말 순수 소리만을 인식하는 오디오 엔진 객체입니다.


자, 이제 필요한 변수들을 선언이 끝났어요.

이제 구현에 들어가봅시다.


앱이 실행되고 가장 먼저 실행하는 viewDidLoad()에는 뭐가 필요할까요?



위에서 SFSpeechRecognizerDelegate를 채택한 것. 기억나시나요?

delegate를 채택하면 꼭!!해야 할일이 있죠 ㅎㅎ?

바로 대리자를 임명하는 과정입니다.

보통 viewDidLoad에서 해주게되죠.


speechRecognizer?.delegate = self


이 speechRecognizer의 대리자는 지금 현재 viewController라고 임명해줍시다.


그리고 이제 뭘 해주어야 할까요?



저 말하기 버튼을 누르면 음성인식이 시작된다고 했죠?

저 버튼에 대한 IBAction메소드를 구현해주어야합니다. 


대충 플로우가 어떻게 될까요?

저 버튼을 누르면, 음성인식이 시작되고

음성인식을 하는동안 저 버튼을 다시 누르면 음성인식이 중지되어야 겠죠?



IBAction func speechToText(_ sender: Any) {


        if audioEngine.isRunning { // 현재 음성인식이 수행중이라면

            audioEngine.stop() // 오디오 입력을 중단한다.

            recognitionRequest?.endAudio() // 음성인식 역시 중단

            button.isEnabled = false

            button.setTitle("말하기!", for: .normal)

        } else {

            startRecording()

            button.setTitle("말하기 멈추기", for: .normal)

        }

    }


코드 이해가 가시나요?

현재 음성인식이 수행중인데, 버튼을 한번 더 누르면 중단한다는 소리니까

오디오와 음성인식을 중단해준거에요.


그리고, 만약 음성인식이 수행중이 아니라면!

이제 사용자의 음성을 입력받고(현재는 한국어)

그 음성을 텍스트로 변환하는 작업을 수행해야겠죠?

startRecording()은 밑에 나올 함수랍니다.

이 startRecording()이 상당히 기니까 

정신 바짝 차리세요:) 

밑에 나올 코드들은 startRecording함수가 끝났다고 하기 전까지는 startRecording안에 있는 코드들이에요 :)


 func startRecording() {


        //인식 작업이 실행 중인지 확인합니다. 경우 작업과 인식을 취소합니다.

        if recognitionTask != nil {

            recognitionTask?.cancel()

            recognitionTask = nil

        }


이 부분이 정확하진 않는데.. 제가 볼때는

현재 음성인식을 처리하고 있으면 그 작업을 중지하고 새로운 음성인식처리를 하라는 부분 같아요.

지금 음성인식을 시작했는데, 다른 음성인식을 처리하고있으면

현재 음성을 처리못하니까요!


자, 이제 오디오세션을 만들 차례입니다. 


//오디오 녹음을 준비 AVAudioSession 만듭니다. 여기서 우리는 세션의 범주를 녹음, 측정 모드로 설정하고 활성화합니다. 이러한 속성을 설정하면 예외가 발생할 있으므로 try catch 절에 넣어야합니다.

        let audioSession = AVAudioSession.sharedInstance()

        do {

            try audioSession.setCategory(AVAudioSessionCategoryRecord)

            try audioSession.setMode(AVAudioSessionModeMeasurement)

            try audioSession.setActive(true, with: .notifyOthersOnDeactivation)

        } catch {

            print("audioSession properties weren't set because of an error.")

        }        


이 오디오세션은 정말 저의 음성'소리'를 인식하는 거에요. 




//recognitionRequest 인스턴스화합니다. 여기서 우리는 SFSpeechAudioBufferRecognitionRequest 객체를 생성합니다. 나중에 우리는 오디오 데이터를 Apple 서버에 전달하는 사용합니다.

recognitionRequest = SFSpeechAudioBufferRecognitionRequest()    


위에서

private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest?

변수를 선언하신 것. 기억하시나요?

이 recognitionRequest변수를 인스턴스화해준겁니다.

이 변수는 음성인식을 처리해준다고 그랬죠? 



SFSpeechAudioBufferRecognitionRequest에 가면,

SFSpeechRecognitionRequest를 상속받는 것을 볼 수 있습니다.

이걸 잘 기억해두세요!


다음으로,

// audioEngine (장치) 녹음 오디오 입력이 있는지 확인하십시오. 그렇지 않은 경우 치명적 오류가 발생합니다.

        guard let inputNode = audioEngine.inputNode else {

            fatalError("Audio engine has no input node")

        }

  //recognitionRequest 객체가 인스턴스화되고 nil 아닌지 확인합니다.

        guard let recognitionRequest = recognitionRequest else {

            fatalError("Unable to create an SFSpeechAudioBufferRecognitionRequest object")

        }


첫 부분에, 왜 inputNode를 검사할까요? 

왜냐하면 이 inputNode를 통해 오디오입력이 수행되기 때문이에요. 

이 inputNode가 없으면 오디오입력 자체가 되지 않겠죠?

그러니 먼저 inputNode를 검사해줍니다.


그리고,  두번째 가드문에서 recognitionRequest가 있는지 검사해줍니다.

recognitionRequest가 있어야 음성인식을 처리하니까요!



//사용자가 말할 때의 인식 부분적인 결과를보고하도록 recognitionRequest 지시합니다.

recognitionRequest.shouldReportPartialResults = true

위에서



SFSpeechAudioBufferRecognitionRequest에 가면,

SFSpeechRecognitionRequest를 상속받는 것을 볼 수 있습니다.

이걸 잘 기억해두세요!


..라고 그랬죠?

이 recognitionRequest는 SFSpeechAudioBufferRecognitionRequest타입이지만, 

이 SFSpeechAudioBufferRecognitionRequest가 SFSpeechRecognitionRequest를 상속받으므로 SFSpeechRecognitionRequest에 있는 함수들을 사용할 수 있습니다. 



shouldReportPartialResults는 무슨역할을 하는지... 보면

만약 이 shouldReportPartialResults이면, 각 발화(사용자가 말한 음성) 부분(최종은 아님) 결과가 보고된다고 하네요.


우리는

recognitionRequest.shouldReportPartialResults = true

true(참)으로 해주었으니 각 발화 부분의 결과를 보고받겠다!!!

라는 것이 되겠네요 XD



// 인식을 시작하려면 speechRecognizer recognitionTask 메소드를 호출합니다. 함수는 완료 핸들러가 있습니다. 완료 핸들러는 인식 엔진이 입력을 수신했을 , 현재의 인식을 세련되거나 취소 또는 정지 때에 불려 최종 성적표를 돌려 준다.

        recognitionTask = speechRecognizer?.recognitionTask(with: recognitionRequest, resultHandler: { (result, error) in

          

            //  부울을 정의하여 인식이 최종인지 확인합니다.

            var isFinal = false

            if result != nil {


                //결과가 nil 아닌 경우 textView.text 속성을 결과의 최상의 텍스트로 설정합니다. 결과가 최종 결과이면 isFinal true 설정하십시오.

                self.myTextView.text = result?.bestTranscription.formattedString

                isFinal = (result?.isFinal)!

            }


            //오류가 없거나 최종 결과가 나오면 audioEngine (오디오 입력) 중지하고 인식 요청 인식 작업을 중지합니다. 동시에 녹음 시작 버튼을 활성화합니다.

            if error != nil || isFinal {

                self.audioEngine.stop()

                inputNode.removeTap(onBus: 0)


                self.recognitionRequest = nil

                self.recognitionTask = nil

                self.button.isEnabled = true

            }

        })


와.. 엄청길죠?

하나하나 살펴볼게요.

recognitionTask = speechRecognizer?.recognitionTask(with: recognitionRequest, resultHandler: { (result, error) in

가 핵심이라고 할 수 있겠는데요.

이걸 보면 정말 어지럽습니다 ㅠㅠㅠㅠㅠ

원형을 일단 봐볼까요? 


주석을 해석하자면,

// 요청에 따라 음성 발화를 인식

// request.shouldReportPartialResults가 true의 경우, Result 핸들러가 호출됩니다.

// 부분적인 결과를 반복하고 마지막에 최종 결과 또는 오류를 반환합니다.


위에서 

recognitionRequest.shouldReportPartialResults를 

true로 해주었으니 result handler가 실행되겠네요!!

 

(result, error) in 에는

@escaping (SFSpeechRecognitionResult?, Error?) -> Swift.Void) -> SFSpeechRecognitionTask라고 하네요. 


result는 SFSpeechRecognitionResult.

error는 Error.

가 각각 대응되겠네요.




result에 뭐가 들어갈지 봅시다.


result에는 현재 인식된 음성의 번역본의 가능성?들이

 들어있다고 볼 수 있습니다.


var isFinal = false

            

if result != nil {

                

                

           self.myTextView.text =result?.bestTranscription.formattedString

           isFinal = (result?.isFinal)!

            

}


isFinal은 나중에 보고, result가 nil이 아니면,

즉, 번역의 가능성들이 nil이 아니라면, 

result에서 가장 정확한 번역을 텍스트뷰에 써줍니다. 


bestTranscription은 SFTranscription타입인데 

SFTranscription이 어떤 타입인지 볼까요?



formattedString이 있어 간편하게 스트링타입으로 인식 결과를 리턴할 수 있네요.



자, 이제 인식결과를 텍스트뷰에 쓰고


isFinal = (result?.isFinal)!


를 해주네요?

isFinal이 무슨 역할을 하는지 봅시다.




가설이 변하지 않는다면 참(true), 음성 처리가 완료.

위에서

var isFinal = false


라고 해줬죠? 그럼 가설이 계속 변하고 있다는 소리겠고, 음성처리가 끝나지 않았다는 소리입니다.

result에는 우리가 말하는 '음성'의 가능성들이 쌓이고 result는 nil이 아니게 됩니다.

그리고 if문을 수행하게 되죠.



isFinal = (result?.isFinal)!

이 부분에서 isFinal이 true로 되는지, false로 되는지는 저도 잘 모르겠습니다..


다음으로,


if error != nil || isFinal {


                self.audioEngine.stop()

                inputNode.removeTap(onBus: 0)

                

                self.recognitionRequest = nil

                self.recognitionTask = nil

                

                self.button.isEnabled = true

            }


이 코드.

error가 없거나 또는! isFinal이 true라면 (= 음성처리가 끝났다면) if문을 수행합니다.

오디오엔진을 정지하고, inputNode를 없애는 작업같네요.

그리고 recognitionRequest,recognitionTask를 nil로 만들어줍니다.




//recognitionRequest 오디오 입력을 추가하십시오. 인식 작업을 시작한 후에는 오디오 입력을 추가해도 괜찮습니다. 오디오 프레임 워크는 오디오 입력이 추가되는 즉시 인식을 시작합니다.

        let recordingFormat = inputNode.outputFormat(forBus: 0)

        inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (buffer, when) in

            self.recognitionRequest?.append(buffer)

        }

        

        // Prepare and start the audioEngine.

        audioEngine.prepare()

        

        do {

            try audioEngine.start()

        } catch {

            print("audioEngine couldn't start because of an error.")

        }

        

        myTextView.text = "아무거나 말해보세요!"


그러나 우리는 아직 음성인식이 끝나지 않았을 수 있겠죠?

잠깐 멈추고, 다시 말할 수 있겠죠?

그러니 언제든지 오디오 입력을 받아들일 준비를 합니다. 

인식이 되는 즉시

self.recognitionRequest?.append(buffer)


recognitionRequest에 인식한 음성을 추가해줍니다.








자,이렇게 길고긴 STT가 끝났는데요 ㅠㅠ

솔직히 말씀드리면 저도 이 코드를 해석하느라 정말 많은 시간이

걸렸고, 아직까지도 제 해석이 맞는지 불분명합니다.

하지만 최대한 찾아보면서 정확한 해석을 하려고 노력했어요..

이 STT만 거의 3일 내내 작성한 것 같네요 ㅠㅠ

그러니 보시고 잘못된 부분이 있으면 지적 환영입니다.

그리고 프로젝트는 

여기에 가시면 다운 받을 수 있습니다!


ㅠㅠㅠ정말 오랜시간이 걸려서 끝내는 STT네요.

도움이 되었으면 좋겠습니다!!







반응형