Infllearn 자동 자막 생성 기능 구현 과정 (AI/인프라 편)
요약
본 글은 AI 기반 자동 자막 생성 기능을 구현한 과정을 다루며, 특히 오프라인 동작과 빠른 처리 속도를 핵심 목표로 삼았습니다. OpenAI의 Whisper 모델 중 C++로 이식된 whisper.cpp를 활용하여 ffmpeg으로 음성 데이터를 추출하고 srt 파일로 전사하는 파이프라인을 단기간에 구축했습니다.
핵심 포인트
- AI 자막 기능 구현은 사내 해커톤(Infthon) 아이디어에서 시작됨.
- 오프라인 동작, 비용 제로, 빠른 속도를 핵심 기술 요구사항으로 설정함.
- OpenAI Whisper 모델 중 whisper.cpp를 사용하여 성능을 최적화하고 데모에 성공함.
이 글은 2023년 10월 31일에 작성되었습니다.
안녕하세요. Inflab의 DevOps 엔지니어 SunB입니다.
2023년 9월, Inflearn에 자막 및 스크립트 기능이 추가되었습니다. 여러분들은 활용하고 계신가요?
이번에 추가된 자막 기능은 크게 두 부분으로 나뉩니다. 첫째는 영상의 음성 데이터를 기반으로 자동 자막을 생성하는 부분이고, 둘째는 이렇게 생성된 자막을 수강 화면에 표시하는 부분입니다.
오늘은 이 중 제가 구현을 담당했던 자막 생성 부분에 대해 어떤 기술을 기반으로, 어떤 과정을 거쳐 구현했는지 이야기해보고자 합니다.
먼저 'DevOps 엔지니어가 자막 기능을 구현했다고?'라고 생각하시는 분들도 계실 것 같아, 이번 자막 기능 구현 프로젝트가 어떻게 시작되었는지 소개해드리겠습니다.
Inflab에서는 올해부터 Infthon이라는 이름으로 사내 해커톤을 개최하고 있습니다. 참가자를 무작위로 4인 팀에 배정하여, 약 2일 동안 아이디어 논의와 MVP(Minimum Viable Product) 구현을 진행한 후 발표하는 형식으로 해커톤이 진행되었습니다.
그런데 우연히(?) Inflearn의 강의 플레이어나 VOD 인프라를 많이 다루었던 FE 개발자 Papico와 제가 같은 팀이 되면서, 자연스럽게 강의 재생에 관한 아이디어를 논하게 되었습니다. 그리고 제1회 Infthon의 주제였던 '팀 생산성 향상 또는 AI를 활용한 서비스 개선'에 가장 적합하다고 생각한 강의의 AI 자막 기능을 아이디어로 선택하게 되었습니다.
먼저, AI 기반 자막 생성 기능을 구현하기 위한 최소한의 조건으로 다음 사항들을 고려했습니다.
- 자막 생성이 오프라인에서 동작할 것 (데이터 외부 전송 방지)
- 자막 생성에 컴퓨팅 리소스를 제외한 추가 비용이 발생하지 않을 것 (오픈 소스 모델을 자체적으로 실행)
- 자막 생성에 걸리는 시간이 영상 길이를 초과하지 않을 것 (라이브 데모가 가능할 정도로 빨라야 함)
이러한 조건을 충족하는 기술을 찾아본 결과, OpenAI의 Whisper라는 모델이 눈에 띄었습니다.
Whisper 외에 검토 대상이었던 기술은 다음과 같습니다.
- KoSpeech: 보관됨(Archived)
- OpenSpeech + KsponSpeech: 자체적으로 학습시켜 모델을 만드는 경우, 기술 이해도도 아직 낮고 시간도 상당히 걸릴 것으로 예상되었습니다.
- 카카오의 PORORO: 보관됨(Archived)
- Meta의 SeamlessM4T: 테스트용 강의를 사용한 PoC 과정에서, SeamlessM4T large 모델은 Whisper large-v2 모델에 비해 정확도가 떨어진다는 것을 확인했습니다.
- 네이버의 CLOVA Speech 등 API 기반 서비스: 데이터 외부 전송과 API 비용이 발생합니다.
Infthon 이전에도 MacWhisper 같은 툴을 통해 OpenAI의 Whisper라는 STT(Speech-to-Text) 모델의 존재와 높은 정확도에 대해서는 알고 있었지만, 깊게 조사해 본 적은 없었습니다.
구현 기간이 2일로 짧았기 때문에, 구체적인 이해보다는 빠른 적용과 데모가 가능한지에 초점을 맞춰 작업을 진행했습니다.
그 결과, 모든 처리가 제 Mac에서 오프라인으로 동작하도록 구성할 수 있었습니다. 그 과정에서 Python으로 구현된 OpenAI의 Whisper는 느렸기 때문에, 대신 whisper.cpp라는 C++로 이식된 버전을 사용했습니다.
이처럼,
- 영상을 업로드하면
- ffmpeg을 사용하여 16-bit WAV 음성 데이터를 추출하고
- whisper.cpp + large-v2 모델을 사용하여 자막을 전사(transcribe)하여 srt 파일로 저장하며
- 수강 화면에서 이 srt 파일을 재생하는 부분까지
를 단기간에 구현할 수 있었습니다.
게다가 FE 개발자 Papico의 활약 덕분에, 스크립트 표시와 재생 위치 이동 기능, 자막 수정 요청 기능과 수정 요청 관리 기능까지 데모하여 제1회 Infthon에서 1등이라는 영예를 안을 수 있었습니다.
이렇게 즐거운 경험으로 막을 내린 줄 알았는데……
어느 의미에서는 당연하게도, 자막 기능은 곧바로 정식 프로젝트로 채택되어 새로운 시작을 하게 됩니다.
Infthon에서 이미 데모까지 마쳤었기 때문에 큰 걱정은 없었지만, 걸리는 점이 3가지 있었습니다.
- Shaka Player에서 m3u8의 HLS 플레이리스트와 srt 자막 파일을 동시에 표시할 때, 자막 동기화가 어긋나는 문제
- AWS 인프라에서 Whisper를 사용해 본 경험이 없었던 점
- 한국어 자막의 한 줄 길이와 개행 위치가 적절하지 않아 가독성이 떨어지는 문제
첫 번째 문제는 FE 개발자인 Papico 님이 Shaka Player의 기능을 사용하지 않고 React로 자막 표시 부분을 새로 구현하여 깔끔하게 해결해 주셨습니다!
두 번째와 세 번째 문제는 그대로 현실적인 과제로 다가왔습니다.
우선적으로 해결해야 했던 것은, Infthon 때와 달리 이번에는 실제 사용자에게 서비스를 제공해야 하므로, 저희 인프라 위에서 기능이 동작하도록 만들어야 한다는 점이었습니다.
자막 생성 인프라에 대해 말씀드리기 전에, 먼저 영상 인코딩 인프라를 개선한 과정을 소개해 드리겠습니다.
저희는 2022년 10월까지 영상 인코딩에 AWS MediaConvert를 사용했지만, 비용 절감과 인코딩 옵션 커스터마이징을 위해 EC2 c6i.xlarge Spot Instance 기반의 AWS Batch 구성으로 전환했습니다.
MediaConvert의 Reserved Queue 등 다양한 대안이나 시도가 있었지만, 예약한 인코딩 시작이 너무 늦거나 4K와 같은 일부 옵션을 사용할 수 없다는 제약이 있어 요구 사항을 충족하기 어렵다고 판단하여 자체적으로 구성하는 방향을 선택했습니다.
그 결과, 인프라 구성 변경은 효과적이었습니다. 길이 28분의 1080p 영상을 540p, 720p, 1080p로 인코딩할 경우, AWS Batch를 사용했을 때와 MediaConvert를 사용했을 때 걸리는 시간과 비용은 다음과 같았습니다.
| AWS Batch (ECS EC2 c6i.xlarge Spot) | MediaConvert (On-Demand Pro) | 차이점 |
|---|---|---|
| 시간 | 약 10분 | 약 7분 |
| 비용 | 약 $0.021 | 약 $1.895 (SD x 1, HD x 2) |
영상 길이에 따른 인코딩 시간에는 큰 차이가 없었지만, 비용은 무려 90배 이상의 차이가 있다는 것을 확인했습니다.
AWS Batch 구성 내에서 EC2 인스턴스 타입별 비용과 성능 분석 결과는 다음과 같았습니다. (길이 28분의 1080p 영상을 540p, 720p, 1080p로 인코딩할 경우, Spot Instance 미적용)
| 인스턴스 타입 | 온디맨드 시간당 비용 (A) | 인코딩 실행 시간 (B) | 실제 비용 (A x B) |
|---|---|---|
| c6i.xlarge | $0.192 | 10분 | $0.032 |
| ... |
위와 같이 개선된 인코딩 인프라를 바탕으로, 자막 인프라도 같은 구조로 구성해 보려고 시도했습니다.
AWS Batch 기반에서 c6i.xlarge 인스턴스 위에서 28분짜리 영상에 대해 whisper.cpp를 실행해 본 결과는 무려 69분 3초로, 예상보다 훨씬 느렸습니다.
Infthon의 데모용으로 Mac Studio (M1 Ultra)에서 같은 영상을 whisper.cpp로 실행했을 때 걸린 시간은 3분 30초였습니다.
이것을 확인하고 바로 GPU 인스턴스의 PoC(Proof of Concept)에 착수했습니다.
처음에는 g5 인스턴스보다 저렴한 ARM 기반의 g5g.xlarge 인스턴스를 사용해 보려고 했습니다.
하지만 NVIDIA 드라이버나 CUDA 버전을 아무리 바꿔가며 시도해도 PyTorch에서 CUDA를 인식하지 못하는 문제가 있었습니다.
CUDA 11의 Python cuDNN 라이브러리는 ARM 아키텍처를 지원하지 않았고, PyTorch 정식 버전은 CUDA 12를 지원하지 않아, PyTorch Nightly + CUDA 12 cuDNN 구성을 시도했지만, torch.cuda.is_available()
이 False를 반환하는 상태가 계속되었습니다.
그러던 중, AWS Batch에서는 아직 ARM 아키텍처 기반의 GPU 인스턴스를 지원하지 않는다는 문서를 발견하고, g5g 인스턴스 사용은 일단 포기했습니다.
그 후, g5.xlarge 인스턴스에서 OpenAI의 Python 기반 Whisper를 실행해 보니 7분 17초가 걸렸습니다.
영상 길이에 비하면 허용할 수 있는 시간이었지만, Mac Studio에서 whisper.cpp를 사용했을 때 3분 30초였던 것을 생각하니 다른 방법이 없을까 고민하게 되었습니다.
그리고 SeamlessM4T에 대해 검색하던 중, 우연히 Hacker News에서 WhisperX를 언급하는 댓글을 보게 되었습니다.
WhisperX를 사용하여 g5.xlarge 인스턴스에서 28분짜리 영상의 자막 생성을 시도해 본 결과, 59초 만에 완료되어 가장 빠른 결과를 얻었습니다. 또한 그 출력 결과 역시 Whisper나 whisper.cpp와 비교했을 때 정확도가 떨어진다는 느낌은 없었고, 각각 장단점이 있는 것으로 보였습니다.
아래는 출력 결과를 비교하며 작성했던 메모의 일부입니다.
Whisper, WhisperX는 g5.xlarge에서, whisper.cpp는 Mac Studio에서 추출한 버전
세 가지 모두 상당히 차이가 있지만, 어느 하나가 가장 뛰어나다고 할 수는 없고 각각 잘 받아 적은 부분이 있었습니다.
WhisperX가 정확하게 추출한 부분
- 뽑았는데 (Whisper: 꼽았는데)
- 등허리 (Whisper: 등어리)
- 복부 (Whisper: 복구)
whisper.cpp가 정확하게 추출한 부분 - Eye Reflect (WhisperX: I Reflect)
Whisper가 정확하게 추출한 부분 - Z-Remesh (WhisperX: gremesher)
- 아이리플렉트 (WhisperX: I Reflect)
WhisperX는 다른 두 가지보다 훨씬 빠르게 실행할 수 있었고, 음절 단위로 align하기 때문에 행 재구성(reconstruction)에 유리하며, 자막 수정 요청 기능의 유효성도 비교적 높을 것으로 추정되어 WhisperX를 사용하는 것이 가장 좋을 것 같다고 생각했습니다.
다만, 가끔 여러 문장을 빠르게 말하는 부분에서 빠진 문장이 있는데, 정확도가 낮다고 판단하여 의도적으로 제외한 것처럼 보였습니다.
처음에 시도했던 whisper.cpp + c6i.xlarge를 기준으로, 시도해 본 조합과의 차이를 정리하면 다음과 같습니다. (large-v2 모델을 사용하여 28분 분량의 음성 데이터를 받아 적었을 경우)
| 도구 | 머신 | 실행 시간 | 차이(속도) |
|---|---|---|---|
| whisper.cpp | c6i.xlarge | 69분 3초 | - (x1) |
| ... | WhisperX | g5.xlarge | 59초 |
최종적으로, WhisperX + g5.xlarge 조합을 선택했습니다.
이렇게 첫 절망감을 극복하고, 속도를 70배 이상 개선하여 인프라 위에서 자막 생성 기능이 정상적으로 작동하도록 만들었습니다.
그 과정에서 GPU 인스턴스나 영상 인코딩에 대한 분석도 진행되었고, 인코딩 관련 프로세스도 개선되었습니다.
자막을 어떻게 생성할 것인가라는 과제는 단순히 음성 데이터에서 텍스트를 추출하는 것으로 끝나지 않았습니다.
whisper.cpp, Whisper, WhisperX 어느 것 모두 --max_line_width와 같이 한 줄의 길이를 제한하는 옵션이 있었지만, 문제는 이 옵션이 작동하지 않는 것이었습니다。(……)
한국어라서 작동하지 않는다고 추측하고 있습니다.
그 때문에 모델이 출력한 자막 데이터를 보면, 행마다 길이가 제각각이고 문장 끝에서 줄 바꿈되는 경우도 거의 없어 가독성은 최악의 상태였습니다.
이를 개선하기 위해 마침표/물음표/느낌표가 있으면 줄 바꿈하는 방안, 일정 글자 수 이상이면 단순히 공백을 기준으로 반으로 나누는 방안, 형태소 분석을 통해 행을 재구성하는 방안 등 다양한 방법을 모색했습니다.
그러던 중, WhisperX로 전환하면서 음절 단위로 시간을 추적할 수 있게 되면서 차라리 모든 줄을 새로 만들자고 생각하게 되었습니다. 이후 다음과 같은 기준을 정하고, WhisperX의 실행부터 행 구성까지 한 번에 처리하는 Python 스크립트를 구현했습니다.
- WhisperX가 시간 추적에 실패한 음절의 시간은, 시간 추적에 성공한 가장 가까운 이전 또는 이후 음절의 시간, 혹은 문장의 시작/종료 시간으로부터 선형 보간합니다. (wav2vec의 한국어 모델을 기반으로 했기 때문에, 한글 외에는 음절 시간을 추적할 수 없다는 문제가 있었습니다.)
- 직전 단어까지의 1행이 0.5초 미만이면, 다른 어떤 조건을 충족하더라도 줄 바꿈하지 않습니다.
- 직전 단어까지의 1행이 10초를 초과하면, 이번 단어부터 줄 바꿈합니다.
- 이번 단어로 1행이 10자를 초과하고, 형태소 분석 결과로 문장 수에 변화가 있으면, 이번 단어부터 줄 바꿈합니다.
- 이번 단어로 1행이 50자를 초과하면, 이번 단어부터 줄 바꿈합니다.
- 문장의 시작이 직전 문장의 끝에서 0.5초 이내인 경우에는, 문장 시작 시간으로 직전 문장의 종료 시간을 대신 사용합니다.
사실, 이 중 형태소 분석을 기반으로 문장 수를 계산하고 이를 이용해 줄 바꿈하는 부분은 나중에 만든 기준입니다.
이 기준을 정하기 전까지는 WhisperX를 CLI로 실행한 후, JSON으로 저장된 데이터를 읽어와 행을 구성하는 부분을 모두 Go로 구현했습니다.
하지만 형태소 분석과 문장 수 계산에 Kiwi를 사용하게 되면서, WhisperX를 Python 내에서 직접 실행하도록 하고, 행을 구성하는 부분까지 Python으로 재구현했습니다.
그리고 이렇게 작성한 Python 스크립트를 실행하고, 최종적으로 구성된 자막 데이터를 스토리지에 저장하는 것만 AWS Batch 내의 하나의 작업(job) 라이프사이클을 관리하는 부분을 Go가 담당하도록 구조화했습니다.
이렇게 하여 현재 제공되는 자막처럼 가독성이 크게 개선된 자막 데이터를 생성할 수 있게 되었습니다.
문제가 되었던 부분은 모두 해결되었다고 생각하여, 실제로 지금까지 업로드된 강의를 대상으로 자막을 일괄 생성하는 작업을 진행하려고 했습니다.
그런데 AWS Batch에 작업을 투입해도 기다려도 EC2 인스턴스가 기동되지 않았습니다. Spot Request를 확인해 보니, 서울 리전에 g5.xlarge 인스턴스의 Spot Capacity가 없어 기동할 수 없었던 것입니다.
Spot Instance는 AWS에서 사용되지 않는 컴퓨팅 리소스를 최대 90% 할인된 가격으로 제공하는 기능입니다.
실행 중인 Spot Instance가 가끔 중단되는 일도 있지만, AWS Batch 등을 사용하여 워크플로우의 내결함성(fault tolerance)을 확보한다면, Spot Instance를 적용하는 것만으로 60% 이상의 비용 절감 효과를 얻을 수 있습니다.
실제로, 동영상 인코딩 인프라에 Spot Instance를 적용함으로써 평균 70%의 비용을 절감할 수 있었기 때문에, 애초에 비용이 많이 드는 GPU 인스턴스를 사용하는 데 있어 Spot Instance 적용은 필수였습니다.
GPU 인스턴스가 부족하다는 이야기는 자주 들었지만, 실제로 경험한 것은 처음이었습니다. 그렇게 3일 동안 기다려도 기동되지 않는 것을 보고, 다른 방법을 찾기 시작했습니다.
먼저, Spot Placement Score라는 기능을 사용해 보았습니다. 미국 버지니아, 오하이오, 오리건 리전은 g5.xlarge 인스턴스가 가장 저렴하고, Placement Score도 높게 나왔기 때문에, 먼저 버지니아 리전에 가보았습니다.
처음 사용하는 리전이라 아무것도 없어 VPC, Subnet, EIP, NAT Gateway를 비롯해 AWS Batch 관련 각종 리소스 등을 모두 새로 생성했습니다. 그리고 실행해 보니, 역시 Spot Capacity가 없다는 오류가 발생했습니다.
이런!! 하고 바로 옆의 오하이오 리전에도 같은 구성을 만들어 보았지만, 역시 Spot Capacity가 없다는 오류가 발생했습니다. (……)
Placement Score는 믿을 수 없다고 생각해서 알아본 결과, 스팟 인스턴스 어드바이저(Spot Instance Advisor)라는 것을 발견했습니다.
g5.xlarge 스팟 인스턴스의 중단 빈도가 낮은 리전을 찾아보니, 오리건 리전이 낮다는 것을 알게 되었습니다.
버지니아, 오하이오 리전에 만든 것은 모두 삭제하고, 오리건 리전에 구성하여 실행해 보니, 겨우 자막 생성 작업(job)이 움직이기 시작했습니다.
일괄 처리를 위해 2,000대의 g5.xlarge 인스턴스를 동시에 기동할 계획이었지만, 오리건 리전에서도 인스턴스 부족으로 최대 500대 정도밖에 기동할 수 없었습니다. 그래도 약 4시간 만에 자막 일괄 생성을 완료할 수 있었습니다.
| 기반 | 단가 | 비용 | 차이 |
|---|---|---|---|
| AWS Batch (g5.xlarge Spot) | - | $807.22 | - (x1) |
| ... |
자막 일괄 생성 작업에 소요된 비용은 $807.22이며, 약 24,000시간에 달하는 영상의 음성 데이터를 API 기반 서비스로 처리할 경우와 비교하여, 1/10 이하의 비용으로 자막을 생성할 수 있는 구성을 구현했습니다.
이러한 과정을 거쳐 약 2개월 만에 자막 생성 기능을 무사히 구현할 수 있었습니다.
Infthon을 계기로 2일 동안 아이디어 선정부터 기술 조사, 데모용 구현까지를 마치고, 이후 2개월 동안 이를 사용자에게 배포 가능한 수준으로 끌어올리기 위해 다양한 시도를 한 귀중한 경험이었습니다.
자막 생성에 관한 기술적인 부분도 주목할 만하지만, 특히 강조하고 싶은 것은 1인(一人)이 단기간에 진행했다고는 믿기 어려울 정도로 인프라 관련 작업량이 많았다는 점입니다.
리소스가 전혀 없는 리전(Region)을 세 번이나 돌아다니며 VPC부터 AWS Batch 환경까지 하룻밤 사이에 구성할 수 있었던 것은, 무언가가 준비되어 있지 않았다면 불가능한 일입니다.
특히, 수강권을 가진 사용자에게만 자막 데이터를 제공해야 하는 부분이나, 자막의 스토리지를 S3에서 MongoDB로 마이그레이션(migrate)하는 작업 등 본문에는 쓰지 않은 수많은 인프라 작업들이 있었습니다. 이것들에 대해 언급하지 못한 것은, 거의 고민할 필요 없이 빠르게 대응할 수 있었기 때문입니다.
이것이 가능했던 것은 그동안 열심히 구축해 온 IaC (Infrastructure as Code) 덕분이라고 생각합니다.
저희가 어떻게 IaC를 구축했는지에 대해서는 (이번에는 정말로) 곧 공개될 예정인 'Inflab IaC 구축기(Part 1)' 글에서 확인하실 수 있습니다.
AWS CDK부터 Terraform으로의 기사에서 Terragrunt와 Terratest에 대한 기사를 예고하면서도, 1년 이상 새로운 글을 올리지 못했던 이유에 대해 자세히 적었으니, IaC에 관심 있는 분들은 꼭 읽어보세요!
감사합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기