
-
SpeechAnalyzer로 앱에 고급 음성 텍스트 변환 기능 가져오기
음성 텍스트 변환용 새로운 SpeechAnalyzer API를 알아보세요. 메모, 음성 메모, 일기 등에서 다양한 기능을 제공하는 Swift API와 그 기능에 대해 자세히 알아보겠습니다. 음성에서 텍스트로 변환되는 방식과 SpeechAnalyzer 및 SpeechTranscriber가 어떻게 흥미롭고 성능이 뛰어난 기능을 구현하도록 지원할 수 있는지 자세히 살펴보겠습니다. 그리고 코딩 실습을 통해 SpeechAnalyzer와 실시간 전사문을 앱에 통합하는 방법을 학습할 수 있습니다.
챕터
- 0:00 - 서론
- 2:41 - SpeechAnalyzer API
- 7:03 - SpeechTranscriber 모델
- 9:06 - 음성 텍스트 변환 모델 빌드하기
리소스
관련 비디오
WWDC23
-
비디오 검색…
안녕하세요 Speech Framework 팀의 엔지니어 Donovan입니다 저는 Shantini입니다 Notes 팀의 엔지니어죠 더욱 발전된 음성 텍스트 변환 API 및 기술인 SpeechAnalyzer가 올해 도입되었습니다 이 세션에서는 SpeechAnalyzer API 및 이와 관련된 핵심 개념을 살펴보겠습니다 또한 API의 기반이 되는 모델에 도입된 새 기능에 관해 간단히 설명하겠습니다 마지막으로 API 사용 방법 실시간 코딩 데모를 보여드리겠습니다 SpeechAnalyzer는 이미 메모, 음성 메모, 일기 등 여러 시스템 앱의 기능을 구동하고 있습니다
SpeechAnalyzer를 Apple Intelligence와 결합하면 통화 요약과 같은 강력한 기능을 구현할 수 있죠 나중에 이 API를 활용해서 나만의 실시간 전사 기능을 빌드하는 방법을 보여드릴게요 우선 새로운 SpeechAnalyzer API를 Donovan이 소개해 드리겠습니다 자동 음성 인식, ASR이라고도 하는 음성 텍스트 변환은 멋진 사용자 경험을 생성할 수 있는 다재다능한 기술입니다 실시간 말하기 또는 녹음된 음성을 사용해 텍스트 형식으로 변환하여 기기에서 텍스트를 쉽게 표시하고 해석할 수 있도록 하는 기술이죠 앱은 해당 텍스트를 실시간으로 저장, 검색 또는 전송하거나 텍스트 기반의 대규모 언어 모델로 전달할 수 있습니다
iOS 10에서 SFSpeechRecognizer를 선보였습니다 해당 클래스는 Siri 음성 텍스트 변환 모델에 접근할 수 있게 하고 짧은 형태의 받아쓰기에 잘 작동했으며 리소스가 제한된 기기에서는 Apple 서버를 사용할 수 있었지만 일부 사용 사례에는 충분히 대응하지 못했고 사용자가 언어를 직접 추가해야 하는 불편함이 있었죠 이제 iOS 26에서는 새로운 API를 모든 플랫폼에서 선보입니다 SpeechAnalyzer API로, 더 많은 사용 사례를 더 잘 지원합니다 새 API는 Swift의 장점을 활용해 음성 텍스트 변환 처리를 실행하고 사용자 기기의 모델 애셋도 관리할 수 있게 합니다 간단한 코드만으로도 말이죠 API와 함께 새 음성 텍스트 변환 모델이 출시되었습니다 이 모델은 이미 Apple 플랫폼에서 여러 앱 기능을 지원하고 있죠 새 모델은 SFSpeechRecognizer를 통해 제공되었던 모델보다 더 빠르고 유연합니다 강의, 회의, 대화처럼 멀리서 들리는 긴 형태의 오디오에 유용하죠 이러한 개선 사항 덕분에 Apple은 앞서 언급한 메모와 같은 애플리케이션에서 새 모델과 API를 활용하고 있습니다 개발자는 이러한 새 기능을 사용하여 메모 등의 애플리케이션에서 제공하는 것과 비슷한 음성 텍스트 변환 기능을 갖춘 앱을 빌드할 수 있습니다 먼저 API의 디자인을 살펴보겠습니다 이 API는 SpeechAnalyzer를 비롯한 여러 클래스로 구성되어 있습니다 SpeechAnalyzer 클래스는 분석 세션을 관리합니다 세션에 ‘모듈’ 클래스를 추가하여 특정 유형의 분석을 수행할 수 있죠 세션에 transcriber 모듈을 추가하면 음성 텍스트 변환을 처리하는 전사 세션이 됩니다 오디오 버퍼를 Analyzer 인스턴스에 전달하면 Transcriber를 통해 음성 텍스트 변환 모델로 전달됩니다 모델은 음성 오디오에 해당하는 텍스트를 예측한 후 일부 메타데이터와 함께 텍스트를 애플리케이션으로 반환하죠
이 모든 과정은 비동기적으로 진행됩니다 애플리케이션에서는 사용 가능한 오디오를 추가하는 작업과 별개로 다른 작업을 통해 결과를 표시하거나 추가로 처리할 수 있죠 Swift의 비동기 시퀀스는 입력과 결과를 버퍼링하고 분리하죠
WWDC21의 ‘AsyncSequence 만나 보기’ 세션에서는 입력 시퀀스를 제공하고 결과 시퀀스를 읽는 방법을 다룹니다
입력과 결과를 연결하기 위해 API는 대응되는 오디오의 타임 코드를 사용합니다 실제로 모든 API 작업은 오디오 타임라인의 타임 코드를 통해 예약되므로 순서를 예측할 수 있고 호출 시기와 무관하죠 타임 코드는 개별 오디오 샘플에서도 정확하거든요 Transcriber에서 결과를 순서대로 제공한다는 점에 주목하세요 각 결과는 고유한 오디오 범위를 반영하며 겹치지 않습니다 음성 텍스트 변환은 일반적으로 이렇게 진행됩니다 그러나 선택적 기능을 통해 특정 오디오 범위 내에서 반복적인 전사를 수행할 수도 있습니다 애플리케이션 UI에서 더 즉각적인 피드백을 제공하고 싶을 때 이 기능을 사용할 수 있죠 즉시 대략적인 결과를 보여준 다음 몇 초 이내에 더 나은 결과를 점진적으로 보여줄 수 있습니다 즉각 표시되는 대략적인 결과를 ‘임시 결과’라고 부릅니다 이러한 결과는 발언자가 말하자마자 제공되지만 정확도가 떨어집니다 그러나 모델은 맥락과 오디오를 더 확보함에 따라 전사 내용을 개선하고 결국에는 최선의 결과를 얻어 Transcriber에서 마지막으로 ‘최종 결과’를 전달하게 됩니다 그 후 Transcriber는 해당 오디오 범위에 대해 더 이상 결과를 전달하지 않고 다음 범위로 이동합니다 나중에 생성된 더 좋은 결과가 이전 결과를 대체하면서 타임 코드도 바뀝니다 이는 임시 결과를 활성화했을 때만 발생합니다 일반적으로 Transcriber는 최종 결과만 전달하며 이전 결과를 대체하지 않습니다 파일을 읽고 전사만 반환하면 되는 경우라면 하나의 함수만으로 전사 기능을 구축할 수 있습니다 이런 작업에는 임시 결과 처리나 높은 동시성이 필요하지 않거든요 함수는 이렇습니다, 여기에서 transcriber 모듈을 만들고 전사할 언어의 로케일을 모듈에 알려 줍니다 결과가 아직 없지만 들어오는 대로 읽어서 AsyncSequence 버전의 reduce를 이용해 연결할 것입니다 이 작업은 async let를 이용해 백그라운드에서 처리됩니다 여기에서는 analyzer를 만들고 transcriber 모듈을 추가합니다 그런 다음 파일 분석을 시작합니다 analyzeSequence 메서드는 파일을 읽고 해당 오디오를 입력 시퀀스에 추가합니다 파일을 모두 읽은 후에는 작업을 종료하도록 analyzer에 알립니다 더 처리할 오디오가 없으니까요 마지막으로 백그라운드에서 조합한 전사를 반환합니다 이는 파일에 담긴 음성을 하나의 속성 문자열 형태로 반환한 것이죠 작업을 마쳤습니다
지금까지 API의 개념과 기본적인 사용 방법을 알아봤습니다 분석 세션에 모듈을 추가하여 전사 등의 작업을 수행할 수 있죠 동시적이고 비동기적으로 작동하여 오디오 입력과 결과 생성을 분리합니다 오디오, 결과 및 작업은 세션의 오디오 타임라인을 통해 연결됩니다 원한다면 일부 결과는 가변 형태로 받을 수 있고 나머지 결과는 최종 결과로 변경되지 않습니다 하나의 함수로 API를 사용하는 사례도 소개해 드렸습니다 나중에 Shantini가 함수의 작업을 여러 뷰, 모델 및 뷰 모델에서 활용하는 방법을 시연해 드릴 것입니다 일반적인 요구 사항 처리를 위해 SpeechAnalyzer 및 Transcriber 클래스의 여러 메서드와 속성을 활용하는 방법도 보여드립니다 이 내용은 문서에서도 확인하실 수 있습니다 이제 SpeechTranscriber 클래스의 새로운 음성 텍스트 변환 모델이 가진 장점을 설명해 드릴게요 SpeechTranscriber는 Apple이 새롭게 설계한 모델로 구동되며 다양한 사용 사례를 지원합니다 예를 들어, 회의 녹화처럼 일부 발언자가 마이크에 가까이 있지 않은 상황에서도 긴 형태의 대화나 실시간 전사 등 다양한 활용이 가능합니다 또한 짧은 지연 시간이 요구되는 실시간 전사 경험을 가능하게 하고자 했으며 정확성과 가독성을 유지하고 음성 데이터의 개인 정보 보호도 중요하게 고려했습니다 이 모든 것은 새로운 온디바이스 모델로 구현됩니다 내부 파트너와 긴밀히 협업해 개발자들이 최적의 경험을 할 수 있도록 설계했습니다 이제 여러분의 앱에서도 동일한 사용 사례를 지원할 수 있죠 SpeechTranscriber는 강력한 음성 텍스트 변환 모델을 제공하므로 이러한 모델을 직접 만들거나 관리하지 않아도 됩니다 새로운 AssetInventory API를 통해 관련 모델 애셋을 설치하면 됩니다 필요할 때 다운로드할 수 있습니다 모델은 시스템 스토리지에 저장되며 앱의 다운로드 또는 스토리지 용량을 늘리지 않습니다 런타임 메모리 크기도 늘어나지 않습니다 이 모델은 애플리케이션의 메모리 공간 외부에서 작동하므로 크기 제한을 초과할까 봐 걱정하지 않아도 됩니다 Apple은 모델을 지속적으로 개선하고 있으며 사용 가능한 업데이트가 있으면 시스템에서 자동으로 설치합니다 SpeechTranscriber는 현재 이러한 언어를 전사할 수 있으며, 추후 지원 언어가 추가될 예정입니다 watchOS를 제외한 모든 플랫폼에서 사용할 수 있으며, 일부 하드웨어 요구 사항이 있습니다 지원되지 않는 언어나 기기가 필요한 경우를 위해 Apple은 DictationTranscriber라는 transcriber 클래스도 제공합니다 이 클래스는 iOS 10의 온디바이스 SFSpeechRognizer와 동일한 언어, 음성 텍스트 변환 모델 및 기기를 지원하지만 SFSpeechRecognizer보다 향상된 클래스로서 사용자에게 설정으로 이동하여 특정 언어에 대해 Siri나 키보드 받아쓰기를 켜도록 안내하지 않아도 됩니다 지금까지 새 API와 모델을 소개해 드렸습니다 지금까지는 다소 추상적이었지만 이제 구체적으로 살펴보겠습니다 이제 Shantini가 앱에 SpeechAnalyzer를 통합하는 방법을 설명해 드릴 겁니다 Donovan, 개요 설명 고마워요 iOS 18의 메모 앱에 추가된 놀라운 기능을 보셨을 텐데요 전화 통화, 실시간 오디오, 녹음된 오디오를 녹음하고 전사할 수 있죠 이 기능은 Apple Intelligence와 통합되어 시간을 절약해 주는 요약 기능도 제공합니다 저희는 Speech 팀과 긴밀히 협력해 SpeechAnalyzer와 SpeechTranscriber를 통해 고품질의 메모 기능을 제공할 수 있도록 했습니다 SpeechTranscriber는 빠르며 원거리에서도 정확하고 온디바이스로 작동하기 때문에 정말 훌륭합니다 우리 팀은 또한 메모에 추가된 것과 같은 기능을 개발자가 구축하고 사용자의 요구 사항에 따라 맞춤화할 수 있도록 지원하는 것을 목표로 삼았습니다 지금부터 그 첫걸음을 함께 해볼게요 실시간 전사 기능이 탑재된 제가 만든 앱을 한번 살펴보죠 어린이를 위한 이 앱은 자기 전에 듣기 좋은 이야기를 녹음하고 텍스트로 전사하며 녹음된 이야기를 다시 재생할 수 있도록 합니다 실시간 전사 결과는 이렇습니다
오디오를 다시 재생하면 대응되는 텍스트 세그먼트가 강조 표시되므로 아이들은 텍스트를 보면서 이야기를 따라갈 수 있죠 프로젝트 설정을 살펴보겠습니다
제 샘플 앱 코드에는 Recorder 클래스와 SpokenWordTranscriber 클래스가 있습니다 두 클래스에 Observable 매크로를 적용해 두었죠
또한 전사 정보를 비롯한 세부 정보를 표시할 수 있도록 캡슐화하기 위해 이 Story 모델도 만들었습니다 마지막으로 실시간 전사와 재생 뷰 녹음 및 재생 버튼을 포함하는 TranscriptView가 있습니다 이 뷰는 녹음 및 재생 상태도 처리하죠 먼저 전사를 살펴보겠습니다 실시간 전사는 간단한 세 단계로 설정할 수 있습니다 SpeechTranscriber를 구성하고 모델이 존재하는지 확인한 다음 결과를 처리하면 됩니다 Locale 객체와 필요한 Option으로 SpeechTranscriber를 초기화하여 설정합니다 Locale의 언어 코드는 전사 결과를 받고 싶은 언어에 해당합니다 Donovan이 아까 강조했듯이 임시 결과는 실시간 추측이며 최종 결과가 가장 정확한 추측입니다 여기서는 두 가지 모두 사용되는데 임시 결과가 연한 색으로 표시되었다가 최종 결과가 나오면 해당 결과로 대체됩니다 SpeechTranscriber에서 이 기능을 구성하려면 Option 유형을 설정해야 합니다 타이밍 정보를 얻기 위해 audioTimeRange를 추가하겠습니다
이렇게 하면 텍스트 재생을 오디오와 동기화할 수 있습니다
다른 Option을 제공하는 사전 구성된 preset도 있습니다
이제 SpeechAnalyzer 객체를 설정하겠습니다 SpeechTranscriber 모듈을 사용해서요
이렇게 하면 필요한 오디오 형식을 얻을 수 있습니다
또한 음성 텍스트 변환 모델이 준비되었음을 알 수 있죠
이제 레퍼런스를 AsyncStream 입력에 저장하고 analyzer를 시작하여 SpeechTranscriber 설정을 마무리합니다
SpeechTranscriber 설정을 마쳤으므로 모델을 얻는 방법을 살펴보겠습니다 ensureModel 메서드에서 우리가 원하는 언어의 전사를 SpeechTranscriber가 지원하는지 검사하는 기능을 추가해 보겠습니다
또한 해당 언어가 다운로드 및 설치되었는지도 검사해 볼게요
언어가 지원되지만 다운로드되지 않았다면 AssetInventory에 요청을 전송하여 지원을 다운로드하겠습니다
전사는 완전히 온디바이스로 진행되지만 모델은 가져와야 한다는 점을 잊지 마세요 다운로드 요청의 ‘progress’ 객체를 사용하면 진행 중인 작업을 사용자에게 알릴 수 있습니다
앱에서 지원할 수 있는 언어는 제한되어 있습니다 제한을 초과한 경우 AssetInventory에 요청하여 하나 이상의 언어를 할당 해제해 공간을 확보할 수 있습니다
이제 모델을 준비했으니 결과를 확인해 봅시다
SpeechTranscriber 설정 코드 옆에 Task를 만들고 레퍼런스를 여기에 저장하겠습니다
임시 결과 및 최종 결과 추적을 위해 변수도 2개 만들게요
SpeechTranscriber는 결과를 AsyncStream을 통해 반환합니다 각 result 객체에는 몇 가지 다른 필드가 있습니다
먼저 얻어야 할 객체는 text입니다 이는 AttributedString으로 표현됩니다 오디오 세그먼트에 대한 전사 결과입니다 stream에서 결과를 얻을 때마다 isFinal 속성을 사용하여 임시 결과인지 최종 결과인지 검사하겠습니다
변동성이 있는 경우 volatileTranscript에 저장할게요
최종 결과를 얻을 때마다 volatileTranscript를 삭제한 다음 결과를 finalizedTranscript에 추가하죠
임시 결과를 삭제하지 않으면 중복이 생길 수 있습니다
또한 최종 결과를 얻을 때마다 이를 나중에 활용할 수 있도록 결과를 Story 모델에도 작성하겠습니다
SwiftUI AttributedString API를 사용하여 조건부 형식도 설정할게요
이렇게 하면 전사가 임시 결과에서 최종 결과로 전환되는 과정을 시각화할 수 있습니다
전사의 타이밍 데이터는 어떻게 가져올까요? 편리하게도 속성이 지정된 문자열에 내장되어 있습니다
각 실행에는 CMTimeRange로 표시되는 ‘audioTimeRange’ 속성이 있습니다 이를 뷰 코드에서 사용해서 올바른 세그먼트를 강조 표시할게요 이제 오디오 입력을 설정하는 방법을 보여드릴게요
사용자가 ‘녹음’을 누를 때 호출되는 record 함수에서 오디오 권한을 요청한 다음 AVAudioSession을 시작할게요 또한 프로젝트 설정에서 앱이 마이크를 사용하도록 구성되었는지 확인해야 합니다
그런 다음 아까 만들어 둔 setUpTranscriber 함수를 호출하겠습니다
마지막으로 오디오 스트림의 입력을 처리하겠습니다 이제 설정 방법을 살펴보죠 몇 가지 작업이 진행됩니다 AsyncStream을 반환하도록 AVAudioEngine을 구성하고 들어오는 버퍼를 스트림으로 전달합니다
또한 오디오를 디스크에 기록합니다
마지막으로 audioEngine을 시작합니다
record 함수로 돌아와 AsyncStream 입력을 transcriber에 전달하겠습니다
오디오 소스마다 출력 형식과 샘플링 속도가 다릅니다 SpeechTranscriber는 사용 가능한 bestAvailableAudioFormat을 제공해 줬습니다
오디오 버퍼를 전환 단계로 전달하여 형식이 bestAvailableAudioFormat와 일치하도록 합니다
그런 다음 AsyncStream을 SpeechTranscriber의 inputBuilder 객체로 전달합니다 녹음을 중지할 때는 몇 가지 작업을 수행해야 합니다 audioEngine과 transcriber를 중지했습니다 작업을 취소하고 analyzer 스트림에서 finalize를 호출해야 합니다 이렇게 하면 임시 결과가 모두 최종 결과로 바뀝니다 이 모든 내용을 뷰에서 연결해 보겠습니다
TranscriptView는 현재 Story 및 SpokenWordTranscriber에 바인딩되어 있습니다 녹음 중인 경우 SpokenWordTranscriber 클래스에서 관찰하는 volatileTranscript와 finalizedTranscript를 연결하여 표시합니다 재생 중일 때는 데이터 모델의 최종 전사문만 표시합니다 문장을 나누는 메서드를 추가해서 덜 복잡하게 보이도록 했어요
오디오를 재생하면서 각 단어를 강조 표시하는 기능을 언급했죠 몇 가지 helper 메서드를 사용하여 audioTimeRange 속성과 현재 재생 시간을 기반으로 각 실행을 강조 표시할지를 계산합니다
SpeechTranscriber의 정확성은 여러 가지 이유로 뛰어나지만 특히 Apple Intelligence와 결합해 유용한 출력 변환을 수행할 수 있다는 점이 가장 큰 장점입니다
여기에서 새로운 FoundationModels API를 사용해 이야기가 끝나면 제목을 생성하고 있습니다 이 API 덕분에 멋진 제목을 쉽게 생성할 수 있죠 제가 직접 제목을 지어낼 필요가 없습니다 FoundationModels API에 대해 자세히 알아보려면 ‘파운데이션 모델 프레임워크 만나보기’ 세션을 확인해 보세요
이제 기능을 직접 확인해 봅시다 더하기 버튼을 탭해서 새 이야기를 만든 다음
녹음을 시작하겠습니다 "Once upon a time in the mystical land of Magenta, there was a little girl named Delilah who lived in a castle on the hill. Delilah spent her days exploring the forest and tending to the animals there."
녹음을 마친 후 사용자는 이야기를 다시 재생할 수 있으며 각 단어가 오디오에 맞춰 강조 표시되죠
Once upon a time, in the mystical land of Magenta, there was a little girl named Delilah who lived in a castle on the hill.
Delilah spent her days exploring the forest and tending to the animals there. SpeechAnalyzer와 SpeechTranscriber 덕분에 거의 지연 없이 전체 앱을 만들 수 있었습니다 자세한 내용은 Speech 프레임워크 문서를 확인하세요 이 세션에서 만든 샘플 앱도 포함되어 있습니다 SpeechAnalyzer에 대한 세션을 마치겠습니다 여러분도 멋진 기능을 만들어 보시기 바랍니다 시청해 주셔서 감사합니다
-
-
5:21 - Transcribe a file
// Set up transcriber. Read results asynchronously, and concatenate them together. let transcriber = SpeechTranscriber(locale: locale, preset: .offlineTranscription) async let transcriptionFuture = try transcriber.results .reduce("") { str, result in str + result.text } let analyzer = SpeechAnalyzer(modules: [transcriber]) if let lastSample = try await analyzer.analyzeSequence(from: file) { try await analyzer.finalizeAndFinish(through: lastSample) } else { await analyzer.cancelAndFinishNow() } return try await transcriptionFuture
-
11:02 - Speech Transcriber setup (volatile results + timestamps)
func setUpTranscriber() async throws { transcriber = SpeechTranscriber(locale: Locale.current, transcriptionOptions: [], reportingOptions: [.volatileResults], attributeOptions: [.audioTimeRange]) }
-
11:47 - Speech Transcriber setup (volatile results, no timestamps)
// transcriber = SpeechTranscriber(locale: Locale.current, preset: .progressiveLiveTranscription)
-
11:54 - Set up SpeechAnalyzer
func setUpTranscriber() async throws { transcriber = SpeechTranscriber(locale: Locale.current, transcriptionOptions: [], reportingOptions: [.volatileResults], attributeOptions: [.audioTimeRange]) guard let transcriber else { throw TranscriptionError.failedToSetupRecognitionStream } analyzer = SpeechAnalyzer(modules: [transcriber]) }
-
12:00 - Get audio format
func setUpTranscriber() async throws { transcriber = SpeechTranscriber(locale: Locale.current, transcriptionOptions: [], reportingOptions: [.volatileResults], attributeOptions: [.audioTimeRange]) guard let transcriber else { throw TranscriptionError.failedToSetupRecognitionStream } analyzer = SpeechAnalyzer(modules: [transcriber]) self.analyzerFormat = await SpeechAnalyzer.bestAvailableAudioFormat(compatibleWith: [transcriber]) }
-
12:06 - Ensure models
func setUpTranscriber() async throws { transcriber = SpeechTranscriber(locale: Locale.current, transcriptionOptions: [], reportingOptions: [.volatileResults], attributeOptions: [.audioTimeRange]) guard let transcriber else { throw TranscriptionError.failedToSetupRecognitionStream } analyzer = SpeechAnalyzer(modules: [transcriber]) self.analyzerFormat = await SpeechAnalyzer.bestAvailableAudioFormat(compatibleWith: [transcriber]) do { try await ensureModel(transcriber: transcriber, locale: Locale.current) } catch let error as TranscriptionError { print(error) return } }
-
12:15 - Finish SpeechAnalyzer setup
func setUpTranscriber() async throws { transcriber = SpeechTranscriber(locale: Locale.current, transcriptionOptions: [], reportingOptions: [.volatileResults], attributeOptions: [.audioTimeRange]) guard let transcriber else { throw TranscriptionError.failedToSetupRecognitionStream } analyzer = SpeechAnalyzer(modules: [transcriber]) self.analyzerFormat = await SpeechAnalyzer.bestAvailableAudioFormat(compatibleWith: [transcriber]) do { try await ensureModel(transcriber: transcriber, locale: Locale.current) } catch let error as TranscriptionError { print(error) return } (inputSequence, inputBuilder) = AsyncStream<AnalyzerInput>.makeStream() guard let inputSequence else { return } try await analyzer?.start(inputSequence: inputSequence) }
-
12:30 - Check for language support
public func ensureModel(transcriber: SpeechTranscriber, locale: Locale) async throws { guard await supported(locale: locale) else { throw TranscriptionError.localeNotSupported } } func supported(locale: Locale) async -> Bool { let supported = await SpeechTranscriber.supportedLocales return supported.map { $0.identifier(.bcp47) }.contains(locale.identifier(.bcp47)) } func installed(locale: Locale) async -> Bool { let installed = await Set(SpeechTranscriber.installedLocales) return installed.map { $0.identifier(.bcp47) }.contains(locale.identifier(.bcp47)) }
-
12:39 - Check for model installation
public func ensureModel(transcriber: SpeechTranscriber, locale: Locale) async throws { guard await supported(locale: locale) else { throw TranscriptionError.localeNotSupported } if await installed(locale: locale) { return } else { try await downloadIfNeeded(for: transcriber) } } func supported(locale: Locale) async -> Bool { let supported = await SpeechTranscriber.supportedLocales return supported.map { $0.identifier(.bcp47) }.contains(locale.identifier(.bcp47)) } func installed(locale: Locale) async -> Bool { let installed = await Set(SpeechTranscriber.installedLocales) return installed.map { $0.identifier(.bcp47) }.contains(locale.identifier(.bcp47)) }
-
12:52 - Download the model
func downloadIfNeeded(for module: SpeechTranscriber) async throws { if let downloader = try await AssetInventory.assetInstallationRequest(supporting: [module]) { self.downloadProgress = downloader.progress try await downloader.downloadAndInstall() } }
-
13:19 - Deallocate an asset
func deallocate() async { let allocated = await AssetInventory.allocatedLocales for locale in allocated { await AssetInventory.deallocate(locale: locale) } }
-
13:31 - Speech result handling
recognizerTask = Task { do { for try await case let result in transcriber.results { let text = result.text if result.isFinal { finalizedTranscript += text volatileTranscript = "" updateStoryWithNewText(withFinal: text) print(text.audioTimeRange) } else { volatileTranscript = text volatileTranscript.foregroundColor = .purple.opacity(0.4) } } } catch { print("speech recognition failed") } }
-
15:13 - Set up audio recording
func record() async throws { self.story.url.wrappedValue = url guard await isAuthorized() else { print("user denied mic permission") return } #if os(iOS) try setUpAudioSession() #endif try await transcriber.setUpTranscriber() for await input in try await audioStream() { try await self.transcriber.streamAudioToTranscriber(input) } }
-
15:37 - Set up audio recording via AVAudioEngine
#if os(iOS) func setUpAudioSession() throws { let audioSession = AVAudioSession.sharedInstance() try audioSession.setCategory(.playAndRecord, mode: .spokenAudio) try audioSession.setActive(true, options: .notifyOthersOnDeactivation) } #endif private func audioStream() async throws -> AsyncStream<AVAudioPCMBuffer> { try setupAudioEngine() audioEngine.inputNode.installTap(onBus: 0, bufferSize: 4096, format: audioEngine.inputNode.outputFormat(forBus: 0)) { [weak self] (buffer, time) in guard let self else { return } writeBufferToDisk(buffer: buffer) self.outputContinuation?.yield(buffer) } audioEngine.prepare() try audioEngine.start() return AsyncStream(AVAudioPCMBuffer.self, bufferingPolicy: .unbounded) { continuation in outputContinuation = continuation } }
-
16:01 - Stream audio to SpeechAnalyzer and SpeechTranscriber
func streamAudioToTranscriber(_ buffer: AVAudioPCMBuffer) async throws { guard let inputBuilder, let analyzerFormat else { throw TranscriptionError.invalidAudioDataType } let converted = try self.converter.convertBuffer(buffer, to: analyzerFormat) let input = AnalyzerInput(buffer: converted) inputBuilder.yield(input) }
-
16:29 - Finalize the transcript stream
try await analyzer?.finalizeAndFinishThroughEndOfInput()
-
-
- 0:00 - 서론
Apple은 API 및 iOS 26 버전에서 새로운 음성 텍스트 변환 기술인 SpeechAnalyzer를 선보여 iOS 10 버전에서 도입된 SFSpeechRecognizer를 대체합니다. Swift 기반으로 개발된 SpeechAnalyzer는 더욱 빠르고, 유연하며, 장문 및 원거리 오디오를 지원하여 강의, 회의, 대화 등 다양한 사용 사례에 적합합니다. 새로운 API를 사용하면 실시간 대본 작성 기능을 만들 수 있고 메모, 음성 메모, 저널과 같은 앱 시스템을 이미 지원하고 있습니다. Apple Intelligence와 결합하면 통화 요약과 같은 강력한 기능을 활용할 수 있습니다.
- 2:41 - SpeechAnalyzer API
API 디자인은 세션 분석을 관리하는 SpeechAnalyzer 클래스를 중심으로 구성됩니다. 전사 모듈을 추가하면 세션은 음성 텍스트 변환 처리를 수행할 수 있는 전사 세션이 됩니다. 오디오 버퍼는 분석기 인스턴스로 전달되어 분석기 인스턴스는 전사자의 음성 텍스트 변환 모델을 통해 라우팅합니다. 이 모델은 텍스트와 메타데이터를 예측하여 Swift의 비동기 시퀀스를 사용함으로써 애플리케이션에 비동기적으로 반환됩니다. 모든 API 작업은 오디오 타임라인의 타임코드를 사용하여 예약되기 때문에 예측 가능한 순서와 독립성이 보장됩니다. 전사자는 특정 오디오 범위를 포괄하여 결과를 순서대로 제공합니다. 선택 기능을 사용하면 범위 내에서 반복적인 전사가 가능하여 즉각적인 결과를 얻을 수 있지만, 정확도가 떨어져 빠른 UI 피드백을 위한 ‘변덕스러운 결과’를 얻을 수 있는데, 이 결과는 나중에 최종 결과로 다듬어집니다. 이 프레젠테이션에서는 전사자 모듈을 만드는 방법, 로캘을 설정하는 방법, 파일에서 오디오를 읽는 방법, 비동기 시퀀스를 사용하여 결과를 연결하는 방법, 최종 전사본을 속성이 있는 문자열로 반환하는 방법을 보여주는 사용 사례를 설명합니다. API는 동시 및 비동기 처리를 지원하고, 오디오 입력을 결과에서 분리하며, 나중에 설명하게 될 다양한 뷰, 모델, 뷰 모델에서 더 복잡한 요구 사항을 처리하도록 확장할 수 있습니다.
- 7:03 - SpeechTranscriber 모델
Apple은 장문 녹음, 회의, 실시간 전사 등 다양한 시나리오를 저지연과 높은 정확도로 처리하도록 설계된 SpeechTranscriber 클래스를 위한 새로운 음성 텍스트 변환 모델을 개발했습니다. 이 모델은 전적으로 온디바이스로 작동하기 때문에 개인 정보 보호와 효율성이 보장됩니다. 앱의 크기나 메모리 사용량을 늘리지 않고 자동으로 업데이트합니다. AssetInventory API를 사용하면 모델을 애플리케이션에 쉽게 통합할 수 있습니다. SpeechTranscriber 클래스는 현재 여러 언어를 지원하고 대부분의 Apple 플랫폼에서 사용할 수 있으며, 지원되지 않는 언어나 기기를 위한 대체 옵션인 DictationTranscriber가 제공됩니다.
- 9:06 - 음성 텍스트 변환 모델 빌드하기
iOS 18에서는 메모 앱에 전화 통화, 실시간 오디오, 녹음된 오디오를 녹음하고 전사할 수 있는 새로운 기능이 추가되었습니다. 이러한 기능은 요약을 생성하기 위해 Apple Intelligence와 통합됩니다. Speech 팀은 SpeechAnalyzer와 SpeechTranscriber를 개발하여 먼 거리에서도 빠르고 정확한 고품질의 온디바이스 음성 변환을 지원했습니다. 이제 이러한 툴을 사용하여 나만의 맞춤형 전사 기능을 만들 수 있습니다. 이 예제 앱은 어린이를 위해 고안된 것으로, 취침 전 이야기를 녹음하고 전사해 줍니다. 앱은 실시간 전사 결과를 표시하고 오디오 재생 중에 해당 텍스트 세그먼트를 강조 표시합니다. 앱에서 라이브 전사를 구현하려면 세 가지 주요 단계를 따르세요. 적절한 로케일과 옵션으로 SpeechTranscriber를 구성하고, 필요한 음성 텍스트 변환 모델이 기기에 다운로드되어 설치되었는지 확인한 다음, AsyncStream을 통해 수신된 전사 결과를 처리합니다. 결과에는 휘발성 텍스트(실시간 추측)와 최종 텍스트가 모두 포함되어 텍스트와 오디오 재생 간의 원활한 동기화가 가능합니다. 최종 결과를 확보하면 ‘volatileTranscript’는 지워지고 중복을 방지하기 위해 결과는 ‘finalizedTranscript’에 추가됩니다. 최종 결과는 추후 사용할 수 있도록 Story 모델에도 기록되고 SwiftUI AttributedString API를 사용하여 조건부 서식을 적용하여 시각화됩니다. 권한을 요청하고, ‘AVAudioSession’을 시작하며, AVAudioEngine을 구성하여 AsyncStream을 반환하여 오디오 입력을 설정합니다. 오디오는 디스크에 기록되고 가장 적합한 오디오 형식으로 변환된 후 전사자에게 전달됩니다. 녹음을 중지하면 오디오 엔진과 전사자가 멈추고 변동이 있는 결과는 최종 확정됩니다. ‘TranscriptView’는 녹음 중에는 최종화된 전사본과 불안정한 전사본의 연결을 표시하고, 재생 중에는 데이터 모델의 최종 필사본을 표시하며, 오디오에 맞춰 단어가 강조 표시됩니다. 예제 앱에서 Apple Intelligence는 FoundationModels API를 사용하여 스토리의 제목을 생성하는 데 활용되고 Apple Intelligence를 사용하여 음성 텍스트 변환 출력에 유용한 변환을 수행하는 방법을 보여 줍니다. Speech Framework를 사용하면 최소한의 시작 시간으로 앱을 개발할 수 있고 자세한 내용은 해당 설명서에서 확인할 수 있습니다.