내가 졸업 프로젝트를 하는 날이 오다니…😱😱
우리 팀의 프로젝트 주제는 AI 기반 영어 말하기 분석 어플리케이션으로, 사용자가 영어로 말한 내용을 STT(Speech-To-Text) 기술을 활용하여 텍스트로 변환한 후 이를 활용하여 문법 검사, 발음 평가 등 다양한 분석 기능을 제공하고자 한다.
⬆️ 프로젝트의 와이어프레임
나는 클라이언트 담당으로, Swift를 활용하여 iOS 어플리케이션을 개발할 예정이다.
본격적으로 개발을 시작하기 전 iOS에서 Speech Recognition 및 발음 평가 API를 사용하는 방법을 알아보고자 한다. 👀
그럼 시작!
음성 인식(Speech Recognition) 또는 STT(Speech-to-Text)란 사람이 말하는 음성 언어를 컴퓨터가 해석해 그 내용을 문자 데이터로 전환하는 처리를 말한다.
iOS에서 사용할 수 있는 대표적인 STT API는 다음과 같다.
우리 프로젝트에서는 이 API들 중 Apple Speech를 사용해 STT를 구현하기로 하였다.
Apple Speech는 일단 무료(!!)이다. 또한 Apple에서 기본적으로 제공하는 프레임워크인 만큼 iOS 애플리케이션 내에서 구현하기 훨씬 수월할 것이라고 생각했다. 무엇보다 confidence levels, speaking rate, average pause duration 등 다양한 분석 결과를 제공하기 때문에 우리 프로젝트에서 유용하게 사용할 수 있을 것이라고 생각했다.
Speech 프레임워크를 사용한 Speech Recognition 과정은 위와 같다.
앱 내에서 사용자의 음성을 녹음하고 STT를 사용하기 위해서는 먼저 사용자로부터 권한을 부여받아야 한다.
iOS에서는 사용자의 프라이버시를 매우 중요하게 여긴다. 사용자의 정보를 얻기 위해서는 반드시 사전에 사용자의 동의를 얻어야 한다. 또한 사용자 정보 보호를 위한 적절한 단계를 거쳐야 하고, 사용 이유를 명확하게 밝혀야 한다.
실제 배포시에도 Privacy 관련 이슈로 reject이 되기도 한다니 처음부터 꼼꼼하게 설정해두자.
자세한 정보는 Apple Developer Documentation 에서 확인할 수 있다.
우리는 음성 녹음과 STT 두 가지 기능을 사용할 것이기 때문에 각각 Microphone Usage와 Speech Recognition Usage에 대한 권한이 필요하다.
Privacy 설정을 하기 위한 방법은 아래와 같다.
Privacy - Speech Recognition Usage Description key
와 Privacy - Microphone Usage Description
두 가지 Key를 추가하고, Value에는 이 기능을 사용해야 하는 이유를 작성한다.AVFoundation은 Apple 플랫폼에서 시청각 미디어를 검사, 재생, 처리 등 다양한 작업을 도와주는 기술들이 포함된 프레임워크이다.
AVFoundation 내에는 음성 녹음 기능이 포함된 클래스들이 여러 가지가 있다. 그래서 iOS에서 음성 녹음을 구현하는 방법을 검색해보면 사람들마다 다 다른 클래스를 사용해서 구현하고 있어 매우 혼란스러웠다. 🤯
나처럼 혼란을 겪을 분들을 위해 잠깐 음성 녹음과 관련된 클래스들의 특징을 정리하고 넘어가고자 한다.
샘플 버퍼 접근 | 비디오 지원 | 난이도 | |
---|---|---|---|
AVAudioRecorder | X | X | 낮음 |
AVCapture | O | O | 중간 |
AVAudioEngine | O | X | 높음 |
Audio Toolbox | O | X | 매우 높음 |
네 가지 API를 비교해본 결과, 우리 프로젝트에서는 녹음된 음성 데이터가 필요할 뿐, 복잡한 가공이 필요하지 않기 때문에 가장 간단한 AVAudioRecorder를 사용하기로 하였다.
AVAudioRecorder를 사용하기 위해 가장 먼저 ViewController 파일에서 AVFoundation 프레임워크를 import 해준다.
import AVFoundation
그리고 AVAudioRecorder와 관련된 프로퍼티를 선언해준다.
var recordingSession: AVAudioSession!
var audioRecorder: AVAudioRecorder!
override func viewDidLoad() {
super.viewDidLoad()
// ...
// Singleton 인스턴스를 얻어온다.
recordingSession = AVAudioSession.sharedInstance()
do {
// 오디오 세션의 카테고리와 모드를 설정한다.
try recordingSession.setCategory(.playAndRecord, mode: .default)
try recordingSession.setActive(true)
// 음성 녹음 권한을 요청한다.
recordingSession.requestRecordPermission() { allowed in
if allowed {
print("음성 녹음 허용")
} else {
print("음성 녹음 비허용")
}
}
} catch {
print("음성 녹음 실패")
}
}
ViewController의 viewDidLoad
메소드 내에서 audio session 싱글톤 인스턴스를 얻어온 후 음성 녹음과 관련된 권한을 요청하는 메소드를 호출한다.
위 코드를 작성한 후 시뮬레이터에서 애플리케이션을 실행하면 마이크 권한을 요청하는 팝업이 뜬다.
그리고 가운데 마이크 버튼을 누르면 녹음이 시작 또는 정지되도록 하였다.
@IBAction func recordButtonTapped() {
if let recorder = audioRecorder {
if recorder.isRecording {
finishRecording(success: true)
} else {
startRecording()
}
} else {
startRecording()
}
}
func startRecording() {
let audioFilename = getDocumentsDirectory().appendingPathComponent("recording.wav")
let settings = [
AVFormatIDKey: Int(kAudioFormatLinearPCM),
AVSampleRateKey: 16000,
AVNumberOfChannelsKey: 1,
AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
]
do {
audioRecorder = try AVAudioRecorder(url: audioFilename, settings: settings)
audioRecorder.delegate = self
audioRecorder.record()
print("녹음 시작")
} catch {
finishRecording(success: false)
}
}
func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0]
}
getDocumentsDirectory().appendingPathComponent("recording.wav")
를 사용하여 디바이스 내에 녹음된 음성이 저장될 경로를 지정한다.settings
에서 오디오와 관련된 설정값을 부여한다.record()
메소드를 사용해서 녹음을 시작한다. func finishRecording(success: Bool) {
audioRecorder.stop()
if success {
playButton.isEnabled = true
transcribeButton.isEnabled = true
pronunciationButton.isEnabled = true
print("finishRecording - success")
} else {
playButton.isEnabled = false
transcribeButton.isEnabled = false
pronunciationButton.isEnabled = false
print("finishRecording - fail")
}
}
녹음을 멈춰야 할 때는 AVAudioRecorder의 stop()
메소드를 사용한다. 그리고 재생, 텍스트 변환, 발음 평가 버튼을 활성화시킨다.
func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) {
if !flag {
finishRecording(success: false)
}
}
실제 사용자가 애플리케이션을 사용할 때는 전화가 오는 등 녹음이 중단되는 다양한 상황이 있을 수 있다. AVAudioRecorder의 delegate에 있는 audioRecorderDidFinishRecording
메소드에서 이런 상황에서 수행할 작업을 선언할 수 있다.
음성 녹음이 정상적으로 이뤄졌는지 확인하기 위해서는 음성을 재생하는 기능도 필요하다.
var audioPlayer: AVAudioPlayer?
iOS에서 오디오를 재생하기 위해서는 AVAudioPlayer를 사용해야 한다. 따라서 View Controller에서 AVAudioPlayer 변수를 선언해준다.
@IBAction func playButtonTapped() {
audioPlayer = try? AVAudioPlayer(contentsOf: audioRecorder.url)
audioPlayer?.delegate = self
audioPlayer?.play()
}
재생하기 버튼을 누르면 AVAudioPlayer를 사용해서 음성이 재생되도록 한다. 이때 음성 파일의 경로는 audioRecorder
의 url에 접근해서 알아올 수 있다.
이제 녹음된 음성을 사용해서 Speech Recognition을 구현해보자.
음성 녹음을 완료한 후 발음 평가
버튼을 누르면 새 화면에서 텍스트로 변환된 결과를 보여주고자 한다.
앞에서 언급했듯 Speech Recognition을 위해서 Apple의 Speech 프레임워크를 사용한다.
Speech 프레임워크는 실시간으로 사용자의 발화 내용을 인식할 수도 있고, 오디오 파일의 내용을 인식할 수도 있다.
우리는 오디오 파일을 사용해서 Speech Recognition을 진행할 것이기 때문에 SecondViewController
에게 음성 파일의 경로를 알려주어야 한다.
// SecondViewController.swift
var audioUrl: URL?
// ViewController.swift
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let secondViewController = segue.destination as? SecondViewController else {
return
}
secondViewController.audioUrl = audioRecorder.url
}
ViewController.swift에서 prepare
메소드를 사용하여 SecondViewController
의 audioUrl에 음성 파일 경로를 저장한다.
파일 경로까지 얻었으니 본격적으로 Speech Recognition을 구현해보자.
AVFoundation처럼 SecondViewController에서도 Speech 프레임워크를 import 해주어야 한다.
import Speech
그리고 Speech Recognition 작업을 초기화하기 위해서 먼저 SFSpeechRecognizer 객체를 생성한다.
let speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "en-US"))
SFSpeechRecognizer는 speech recognizer 과정을 관리하는 핵심 오브젝트이다. 이 오브젝트는 음성 인식 권한 요청, 음성 인식 중 사용 언어 설정, 음성 인식 작업 초기화를 할 때 사용한다.
객체를 생성할 때 locale
이 없으면 사용자의 기본 언어로 speech recognition이 이루어진다. 우리 프로젝트에서는 영어를 인식해야 하기 때문에 지역을 명시적으로 선택해준다.
녹음과 마찬가지로 speech recognition도 권한 요청을 반드시 해주어야 한다.
override func viewDidLoad() {
super.viewDidLoad()
SFSpeechRecognizer.requestAuthorization { (status) in
switch status {
case .notDetermined: print("Not determined")
case .restricted: print("Restricted")
case .denied: print("Denied")
case .authorized: print("We can recognize speech now.")
@unknown default: print("Unknown case")
}
}
}
SFSpeechRecognizer에 있는 requestAuthorization
메소드를 호출하여 음성 인식 권한 요청 팝업을 띄워준다.
권한 허용까지 받으면 이제 음성 인식을 사용할 수 있다. 음성 인식을 진행하고 결과를 보여주기 위한 transcribeAudio()
메소드를 만들어준다.
func transcribeAudio() {
guard let audioUrl = audioUrl else {
print("Can't find audio url")
return
}
if speechRecognizer!.isAvailable {
let request = SFSpeechURLRecognitionRequest(url: audioUrl)
speechRecognizer?.supportsOnDeviceRecognition = true
speechRecognizer?.recognitionTask(
with: request,
resultHandler: { (result, error) in
if let error = error {
print(error.localizedDescription)
} else if let result = result {
print(result.bestTranscription.formattedString)
if result.isFinal {
self.resultLabel.text = result.bestTranscription.formattedString
if let metaData = result.speechRecognitionMetadata {
self.speakingRateLabel.text = "WPM: \(metaData.speakingRate)"
}
}
}
})
}
}
speech recognizer 객체를 사용할 때는 isAvailable
프로퍼티를 사용해서 서비스를 사용할 수 있는지 먼저 확인해야 한다.
available 하면 recognition 요청 객체를 생성한다.
SFSpeechURLRecognitionRequest
를 사용, 실시간으로 오디오를 받아오는 애플리케이션이라면 SFSpeechAudioBufferRecognitionRequest
를 사용한다. 우리 프로젝트는 전자의 경우에 해당한다.recognitionTask(with:resultHandler:)
메소드를 사용해서 recognition을 시작한다.
resultHandler
에 있는 블록은 음성 인식 결과가 나오거나 에러가 발생할 때 실행된다.
result
파라미터는 음성 인식 과정의 결과들을 포함하는 객체이다. (SFSpeechRecognitionResult)
bestTranscription
프로퍼티를 사용해서 가장 정확도가 높은 결과를 얻어온다. isFinal
프로퍼티를 사용할 수 있다.이번에는 저장된 음성 데이터를 서버와의 통신에 활용해보자.
ETRI에서 인공지능을 활용한 영어 발음평가 API를 무료로 제공하고 있다.
이 API를 사용하기 위해 16kHz로 녹음된 음성 데이터를 base64 문자열로 변환한 다음 body에 넣어서 HTTP request를 보낸다.
음성 데이터는 base64EncodedString()
메소드를 사용하면 간단하게 base64 문자열로 변환할 수 있다.
func postPronunciation() {
let url = "http://aiopen.etri.re.kr:8000/WiseASR/Pronunciation"
let key = ""
let audioData = try? Data(contentsOf: (audioRecorder?.url)!)
let encodedString = audioData?.base64EncodedString()
guard let encodedString = encodedString else {
print("오디오 인코딩 실패")
return
}
let parameters: Parameters = [
"access_key": key,
"argument": [
"language_code": "english",
"audio": encodedString
]
]
AF.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default, headers: ["Content-Type": "application/json; charset=UTF-8"])
.validate()
.responseDecodable(of: Pronunciation.self) { response in
switch response.result {
case .success(let response):
print(response)
case .failure(let error):
print(error.localizedDescription)
}
}
}
정상적으로 요청을 보냈다면 텍스트로 변환된 문장과 발음 점수 데이터를 응답으로 받을 수 있다.
// PronunciationModel.swift
struct Pronunciation: Codable {
var requestId: String?
var result: Int?
var returnType: String?
var returnObject: PronuncationScore?
var reason: String?
enum CodingKeys: String, CodingKey {
case requestId = "request_id"
case result
case returnType = "return_type"
case returnObject = "return_object"
}
}
struct PronuncationScore: Codable {
var recognized: String
var score: Double
}
HTTP response를 받기 위한 데이터 모델은 위와 같다.
영상이 아니라 재생 기능은 생략했다. 🥲
이렇게 음성 데이터를 만지는 프로젝트는 처음인데, 공식 문서를 열심히 읽고 차근차근 따라가니 나름 원하는 대로 결과가 잘 나온 것 같다.
서버 구축까지 완료되면 서버와 음성 데이터를 주고 받는 것도 테스트 해보고 여러 음성 파일들을 목록으로 보여주는 것까지 부지런히 구현해봐야겠다.
졸프…. 파이팅!!!!🔥🔥
전체 코드는 GitHub에서 확인할 수 있다.
잘 읽었어요~