2025. 1. 31. 14:06ㆍFramework, Library
Apple의 Speech Framework 이해하기
지난 학기 학교 캡스톤 프로젝트에서 구현했던 기술을 이제서야 글로 옮기려고 해요!
간략하게 내용을 소개하고 넘어가자면, 지난 학기 프로젝트는 <RESPECT ZONE : 욕설과 혐오없는 깨끗한 공간>이라는 주제로 사람들의 욕설이나, 혐오, 차별 표현 등을 일상 대화 속에서 인식해 - 사람들에게 올바른 언어 사용을 유도할 수 있도록 피드백을 제공해주는 블루투스 연결 기반 iOS 애플리케이션이었죠.
이때 "사람들의 욕설이나, 혐오, 차별 표현을 일상 대화 속에서 인식"하기 위해 필요했던 기능이
바로 실시간으로 사용자가 말하는 음성을 텍스트로 변환해주는 기능인 STT (Speech-To-Text) 혹은 음성 인식 (Speech Recognition)이라고 불리는 기술이었습니다.
*반대로, 텍스트를 음성으로 변환해주는 기능은 TTS (Text-To-Speech)라고 부릅니다. 모두 인공지능 기반의 기술이죠.
STT를 구현하기 위해 구글이나 네이버 등의 API를 활용하는 방법도 있지만, 유료기도 하고.. 복잡하기도 하고...여러 번거로움이 있습니다.
하지만 Apple에서는 손쉽게 무료로 Speech라고 불리는 음성 인식(Speech Recognition) API를 제공해주기 때문에,
Apple의 Speech Framework를 사용해서 캡스톤 프로젝트의 실시간 음성-텍스트 변환 기능을 구현했던 내용을 소개하고자 합니다!
그럼 일단 Speech 프레임워크의 공식문서 상 간략한 정보부터 살펴보겠습니다.
- Speech는 iOS 10.0 이상부터 지원되며, 실시간 음성 혹은 사전 녹음된 오디오에 대해서도 음성 인식을 수행하도록 하는 Apple의 프레임워크라고 합니다. (Perform speech recognition on live or prerecorded audio)
- Speech 프레임워크는 기본적으로 앱의 네트워크 연결을 전제로 동작하게 됩니다!
일부 언어에 대해서 로컬 음성 인식을 지원한다고는 소개하지만, 일단 프레임워크가 음성 인식을 위해 Apple의 서버를 의존한다고 해요. (이 부분은 아래 부분에서별도로 언급해보도록 할게요!) - 또한 Speech는 한국어와 영어를 포함한 다양한 언어를 지원합니다.
이때, 하나의 음성 인식 객체 (SpeechRecognizer)는 단일 언어로 동작하게 된다고 하네요. (이 부분 역시 아래 부분에서 코드로 이해할 수 있습니다.)
중요한 것은 인공지능 기반의 기술인 STT (Speech-To-Text)를 Apple에서는 무료로 제한없이 구현할 수 있다는 점.
그리고 꽤 높은 정확도로 한국어 음성 인식을 제공한다는 점입니다.
본격적으로 아래에서 Speech 구현 코드를 살펴볼게요!
Speech의 기본 객체 이해하기
위에서도 설명했던 것처럼 Speech는 Apple의 서버를 활용하는 네트워크 통신 기반의 API입니다.
즉 다시 말해, 해당 API를 사용하는 데 있어 비동기 처리가 필수적이라는 의미죠. -> 이 점에 대해 이번 코드에서는 Swift의 최신 Concurrency 기술인 Async/Await, Task, Actor 등의 기능을 적극 활용하겠습니다. (해당 문법 내용은 최근 블로그 글로 옮기는 중에 있습니다..!)
import AVFoundation
import Speech
import SwiftUI
actor SpeechRecognizer: ObservableObject {
@MainActor @Published var transcript: String = ""
...
위의 코드를 살펴보겠습니다.
우선, 실시간 텍스트-음성 변환 기능을 수행하는 객체인 SpeechRecognizer에서는 세 개의 프레임워크가 import되고 있습니다.
각 프레임워크의 역할은 아래와 같이 정리할 수 있을 것 같네요!
- AVFoundation : 오디오 입력, 즉 여기서는 실시간 사용자의 음성 Input을 받기 위한 마이크를 설정하기 위해 사용
- Speech : 마이크로부터 받은 음성을 텍스트로 변환 (Speech-To-Text) 하기 위해 사용
- SwiftUI : SpeechRecognizer의 결과를 SwiftUI 기반 View에 바로 연동하기 위함 (ObservableObject, @Published 코드 해당)
또한, 이 객체는 Data-race 이슈를 해결할 수 있는 actor형으로 선언된 것을 확인할 수 있죠.
STT 변환된 텍스트를 담을 공간인 String 타입의 transcript 변수는 UI와 연관되기 때문에 @MainActor를 붙여 메인 스레드 동작 실행을 보장하고 있습니다.
이어서 음성 인식 (Speech Recognition) 기능을 사용하기 위해서 사용되는 네 가지 핵심 객체를 살펴봐야 합니다.
*SF는 Speech Framework의 약자, AV는 AVFoundation Framework의 약자 Audio & Video에서 따온 것으로 보이는군요!
private let recognizer = SFSpeechRecognizer(locale: Locale(identifier: "ko-KR"))
private var request: SFSpeechAudioBufferRecognitionRequest?
private var recognitionTask: SFSpeechRecognitionTask?
private let audioEngine = AVAudioEngine()
- SFSpeechRecognizer : Apple의 음성 인식 기능 주체 -> 음성 인식에 대한 권한 요청, 어떤 언어를 인식할지 지정 (여기서는 "ko-KR"로 한국어로 지정한 것을 확인할 수 있음), 음성 인식 작업을 시작하는 데 사용
- SFSpeechAudioBufferRecognitionRequest : 오디오 데이터에 대해 음성을 인식해 달라는 요청을 처리하는 객체
- SFRecognitionTask : 실시간 음성 인식 진행 상황을 학인하는 객체 -> 음성 인식 상태 (state) 확인, 진행 중인 작업을 시작하거나, 작업 종료를 알림
- AVAudioEngine : 마이크로부터 실시간 오디오 녹음 기능을 통한 데이터를 캡처하기 위한 객체
(SFSpeechRecognizer 개념 확장하기)
위에서 하나의 음성 인식 객체는 단일 언어로만 동작한다고 설명했던 것을 기억하시나요?
이 부분이 해당 SpeechRecognizer 객체에 해당합니다.
초기화 시 locale에 지정한 언어로만 인식하게 되며, 다른 언어에 대한 지원을 원하는 경우 별도의 객체를 생성해야 한다는 의미죠!
(또한, locale을 별도로 초기화시 지정하지 않으면, 키보드에서 사용하는 언어로 인식을 자동으로 시도한다고 하네요~)
(RecognitionRequest 개념 확장하기)
해당 코드에서는 실시간 오디오 입력 기반의 SFSpeechAudioBufferRecognitionRequest 객체가 사용되었습니다.
하지만 실시간 마이크 음성이 아닌, URL을 기반으로 녹음된 오디오 파일에 접근해서 음성 인식을 요청하는 객체로는 SFSpeechURLRecognitionRequest 객체가 준비되어 있습니다.
-> 결국 두 클래스는 공통의 SFSpeechRecognitionRequest라는 추상 클래스의 구현부에 해당하기 때문에, 사용처만 다를 뿐 제공하는 기능은 동일하게 사용할 수 있을 겁니다!
권한 요청하기 (Speech Recognition Usage, Microphone Usage)
아 그런데 그 전에! 위에서 살펴봤던 STT 객체의 기능을 사용하기 위해서 두 개의 권한 요청이 필요합니다.
바로 음성 인식 권한 (Speech Recognizer Usage)과 음성 데이터의 Input에 해당하는 마이크 권한 (Microphone Usage)이죠.
Xcode의 [App TARGETS] - [Info]에 들어가서 두 가지 권한 요청 시 표출될 메시지를 우선 작성해 줍시다.
Privacy - Speech Recognition Usage Description과
Privacy - Microphone Usage Description 키 값을 Info에 각각 추가해주고, 메시지를 Value에 자유롭게 작성해주세요.
해당 부분에 작성한 메시지는,
앱 최초 실행 시 사용자에게 권한을 요청하는 Alert에 아래와 같은 모습으로 보여지게 될 겁니다!
권한은 SpeechRecognizer 객체가 처음 생성될 때 (init 메서드 부분이 해당) 자동으로 실행되어 요청하도록 설정해야 합니다.
(Speech Recognizer 혹은 Microphone에 대한 권한 둘 중 어느 하나라도 받지 못한다면 - STT 기능 자체를 사용할 수 없을거니까요!)
음성 인식에 대한 권한은 SFSpeechRecognizer.requestAuthorization 메서드를 통해 받고,
마이크 사용에 대한 권한은 AVAudioSession.shareInstance().requestRecordPermission 메서드를 통해 받을 수 있습니다.
두 메서드가 모두 콜백 기반 API이기 때문에,
Async/Await 스타일의 코드를 구현하기 위해서 withCheckedContinuation을 사용해 래핑해서 비동기 API 코드를 작성해준 것을 확인할 수 있죠.
이후, 반환되는 status 값 (speechAuthStatus, audioAuthStatus가 해당)에 따라 권한 상태 값을 print해서 확인하고 있네요.
init() {
Task {
await requestAuthorization()
}
}
func requestAuthorization() async {
// Authorization Speech Recognizer
let speechAuthStatus = await withCheckedContinuation { continuation in
SFSpeechRecognizer.requestAuthorization { status in
continuation.resume(returning: status)
}
}
speechAuthStatus == .authorized
? print("음성 인식 권한이 허용되었습니다.")
: print("음성 인식 권한이 허용되지 않았습니다.")
// Authorization Audio Session
let audioAuthStatus = await withCheckedContinuation { continuation in
AVAudioSession.sharedInstance().requestRecordPermission { status in
continuation.resume(returning: status)
}
}
audioAuthStatus
? print("오디오 녹음 권한이 허용되었습니다.")
: print("오디오 녹음 권한이 허용되지 않았습니다.")
}
위 코드에서는 단순하게 권한 허용 / 허용되지 않음 두 개의 분기만 나눠주도록 했습니다.
하지만, 권한 요청의 case가 여러 개가 있다면, 각 case별로 나누어서 에러 분기 처리를 나눠주는 것이 더 좋겠죠?
[SpeechRecognizer에 대한 권한 요청 결과 case ]
-> .authorized (권한 부여) / .denied (권한 거부) / .notDetermined (권한 미설정, 초기상태) / .restricted (권한 제한)
[AVAudioSession에 대한 권한 요청 결과 case]
-> true (마이크 권한 부여) / false (마이크 권한 거부)
startTranscribe() 메서드 - 음성 인식 시작하기
자 이제 권한까지 요청했으니 본격적으로 음성 인식을 수행하는 메서드 startTranscribe() 코드 부분을 설명해 보겠습니다.
코드가 길기 때문에 줄바꿈이 되어있는 순으로 큰 프로세스를 먼저 설명하고, 자세하게 각 코드에 대해서 추가적으로 설명이 필요한 부분에 대해서만 더 이어가 보려고 해요.
우선 큰 틀에서 음성 인식 메서드의 작동 과정은 아래와 같아요!
1️⃣ SpeechRecognizer의 유효성 확인 -> 사용자의 음성 인식 권한 허용 여부와, 네트워크 연결 여부를 확인하는 것!
2️⃣ AVAudioSession (마이크 입력 관련) 설정 -> 마이크 녹음 모드 활성화
3️⃣ 음성 인식 요청 (~RecognitionRequest) 객체 생성 및 설정
4️⃣ 오디오 입력 엔진 설정 및 오디오 엔진 시작 -> 음성을 실시간으로 버퍼링하여 전달, 마이크 실제 활성화
5️⃣ 음성 인식 Task 실행 -> 실시간으로 받아오는 텍스트를 transcript 프로퍼티에 업데이트
func startTranscribe() async throws {
// 1️⃣
guard let recognizer, recognizer.isAvailable else { return }
// 2️⃣
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(.record, mode: .measurement, options: .duckOthers)
try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
} catch {
throw error
}
// 3️⃣
let request = SFSpeechAudioBufferRecognitionRequest()
request.shouldReportPartialResults = true
request.addsPunctuation = true
self.request = request
// 4️⃣
let inputNode = audioEngine.inputNode
let recordingFormat = inputNode.outputFormat(forBus: 0)
inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { buffer, _ in
request.append(buffer)
}
self.audioEngine.prepare()
try self.audioEngine.start()
// 5️⃣
self.recognitionTask = recognizer.recognitionTask(
with: request
) { [weak self] result, error in
guard let self else { return }
if let result {
let transcription = result.bestTranscription.formattedString
Task { @MainActor in
self.transcript = transcription
}
}
}
}
이제 중요하게 살펴봐야 하는 부분을 더 자세하게 설명해 볼게요.
우선 2️⃣번의 오디오 세션과 관련된 설정 부분입니다.
setCategory의 파라마터 값은 순서대로
"record -> 녹음 전용, measurement -> 고품질 오디오 캡처, duckOthers -> 앱 활성화 시, 다른 오디오 볼륨을 줄임"을 나타냅니다.
즉, setCategory 메서드는 "앱에서 사용하는 오디오 세션이 어떤 용도로 사용될 것인가"를 나타내기 위해 사용되는 거죠!
setActive는 오디오 세션을 활성화하기 위해 사용됩니다!
추가 파라미터로 setActiveOption 값을 설정할 수 있어요.
해당 코드에서는 notifyOtherOnDeactivation을 사용해 음성 인식 이후, 다른 앱에서 자동으로 다시 소리를 키울 수 있도록 옵션을 설정했습니다.
4️⃣번 부분은 저도 생소하니 간략하게만 이해하면 될 것 같아요!
마이크 입력을 audioEngine.inputNode를 통해 가져온다. -> 그 마이크 입력의 오디오 포맷을 지정하고, intallTap을 통해 오디오 데이터를 buffer? 단위로 캡처한다. -> 캡처된 데이터를 음성 인식 요청 객체 (~request)에 추가해 음성 인식을 수행하도록 한다.
정도까지만 알고 넘어가죠! (버퍼? 캡처? 너무 생소하고 어려운 용어라 그냥 그렁가보다 하고 넘어갑시다...)
5️⃣번 부분에서는 실제 음성 인식을 수행하고 결과가 반환되는 비동기 작업이 포함되어 있습니다.
recognizer.recognitionTask(with:) 메서드를 통해서 실시간으로 음성 데이터를 요청하고 -> 그 결과를 transcript에 받고 있어요!
이때 transcript는 UI와 관련되어 있기 때문에 메인 스레드 동작을 보장하는 @MainActor 코드를 지정해줬구요.
텍스트는 bestTranscription 이라는 프로퍼티로 인식된 텍스트 중 최상의 텍스트를 가져올 수 있는 속성을 사용합니다.
설명 없이 넘어간 3️⃣번 Request 부분은 이 글 맨 마지막 하단에서 별도로 더 자세하게 알아볼 거예요!
stopTranscribe() 메서드 - 음성 인식 중단하기
음성 인식을 멈추는 메서드 stopTranscibe는 시작과 관련된 부분을 모두 중단하기만 하면 됩니다. 간단하네요!
- 마이크 입력과 관련된 오디오 엔진 중단 -> audioEngine.stop / inputNode.removeTab 코드가 해당
- 음성 인식 작업 (recognitionTask) 취소 및 초기화
- 음성 인식 요청 (speechRequest) 종료 및 초기화
func stopTranscribe() {
audioEngine.stop()
audioEngine.inputNode.removeTap(onBus: 0)
recognitionTask?.cancel()
recognitionTask = nil
request?.endAudio()
request = nil
}
결국 핵심은! SpeechRecognitionRequest입니다.
음성 인식 기능의 주체가 되는 객체는 위에서 SpeechRecognizer라고 했지만,
직접 STT 기능을 구현해보면서 느낀 것은 음성 인식의 세부 사항과 핵심 기능을 지정하는 것은 요청 객체인 SpeechRequest가 담당하고 있다는 점이었습니다.
입력 주체인 마이크 (AudioSession)와 - 음성 인식 주체인 음성 인식기 (SpeechRecognizer) 사이에서 중간 소통을 담당하는 다리 역할인 거죠!
더 정확하게 말하면, 오디오 데이터를 STT 엔진에 전달하는 역할이라고 볼 수 있습니다!
제가 이 Request가 핵심이라고 설명하는 이유는,
STT (Speech-To-Text) 기능을 구현하는 데 있어 자잘한 속성을 해당 RecognitionRequest에서 지정하기 때문인데요.
마치 음성 데이터를 Recognizer에게 전달하면서 "STT 변환을 의뢰서에 이런이런 내용을 담아 요청합니다."라고 요청서를 전달하는 느낌이랄까요...?
요청서에 담을 수 있는 주요 속성으로는 아래와 같은 것들이 있어요!
특히, 자동으로 구두점을 추가하는 것이나 / 부분적으로 문장을 반환하도록 하는 것은 실시간 텍스트 변환 기능에서 핵심으로 사용할 수 있는 기능이니 저처럼 오래 헤매지 마시고... 잘 사용하시길 바랍니다...!
shouldReportPartialResults | 부분적으로 STT 결과를 반환하는지 여부, 실시간 텍스트 변환과 관련 (default = True) |
addsPunctuation | 자동으로 구두점 (마침표, 물음표, 느낌표 등)을 텍스트에 추가해서 변환하는지 여부 (default = False) |
requiresOnDeviceRecognition | 온디바이스 STT 기능을 사용하는지 여부 단, 네트워크 연결을 사용할 때보다 정확하게 텍스트 변환이 이루어지지 않을 수 있다고 설명한다. |
taskHint | STT 최적화를 위한 힌트를 인식기에 제공 (.unspecified 기본 / .dictation 긴 문장 / .search 짧고 간결한 문장 / .confirmation 예/아니오 같은 응답) |
let request = SFSpeechAudioBufferRecognitionRequest()
request.shouldReportPartialResults = true
request.addsPunctuation = true
request.requiresOnDeviceRecognition = true
request.taskHint = .dictation
Reference
Speech | Apple Developer Documentation
Perform speech recognition on live or prerecorded audio, and receive transcriptions, alternative interpretations, and confidence levels of the results.
developer.apple.com
Asking Permission to Use Speech Recognition | Apple Developer Documentation
Ask the user’s permission to perform speech recognition using Apple’s servers.
developer.apple.com
Transcribing speech to text | Apple Developer Documentation
In this tutorial, you’ll add a feature to Scrumdinger that captures and logs meeting transcripts. You’ll request access to device hardware like the microphone and integrate the Speech framework to transcribe live audio to text.
developer.apple.com
Speech recognition using the Speech framework
The article introduces the “Speech framework” as a cornerstone of the no-interface approach.
medium.com
'Framework, Library' 카테고리의 다른 글
[TipKit] iOS에서 숨겨진 기능을 알려주는 툴팁 (ToolTip) 만드는 방법 (1) | 2025.02.18 |
---|---|
[AVFoundation] iOS에서 텍스트-음성 변환 기능 TTS (Text-to-Speech) 구현하기 (0) | 2025.02.05 |
[HealthKit] Mental wellbeing in HealthKit - State of Mind 알아보기 (1) | 2025.01.28 |
[Share Extension] 다른 앱의 "공유하기" 버튼에 우리 앱을 설정하고 싶다면? (feat. NSExtensionActivationRule) (5) | 2024.10.06 |
[WebKit] WKWebView를 사용해서 앱 사용 중, 웹으로 연결시켜보자 (1) | 2024.02.15 |