
디지털청의 GenAI Web(`genai-web`)에서 '음성 받아쓰기'를 활성화하고 whisper.cpp를 통합하기
요약
GenAI Web 프로젝트에서 AWS Transcribe 대신 whisper.cpp를 활용하여 로컬 음성 받아쓰기 기능을 통합하는 방법을 다룹니다. 프론트엔드 구조를 유지하면서 Local API를 통해 ffmpeg와 whisper.cpp를 호출하는 아키텍처를 설명합니다.
핵심 포인트
- 프론트엔드 변경을 최소화하는 API 경계 유지 전략
- whisper.cpp와 ffmpeg를 활용한 로컬 음성 변환 구현
- 환경 변수를 통한 CUDA 및 CPU 백엔드 자동 폴백 설정
- isUseCaseEnabled를 이용한 기능 노출 제어
genai-web은
본래 AWS를 전제로 한 구성이지만, API 경계를 잘 유지하면 음성 받아쓰기(Transcription)도 로컬 실행으로 전환할 수 있습니다.
이번 포인트는 "프론트엔드(Frontend)를 크게 개조하지 않는 것"입니다.
브라우저 측은 기존과 동일하게,
- 음성 파일을 선택한다
- 업로드 URL을 취득한다
- 음성을 업로드한다
- 음성 받아쓰기 작업을 시작한다
- 결과를 폴링(Polling)한다
라는 흐름을 사용합니다.
변경된 것은 그 다음 단계입니다. AWS Transcribe가 아니라, Local API가 ffmpeg와 whisper.cpp를 호출합니다.
로컬 음성 받아쓰기는 다음과 같은 구성으로 동작합니다.
Browser
|
| /transcribe/url
...
중요한 점은 whisper.cpp를 프론트엔드에서 직접 호출하지 않는 것입니다.
브라우저는 기존의 /transcribe/* API만을 바라봅니다.
Local API 내부에서 음성 변환, CLI 실행, JSON 파싱(Parsing)을 한꺼번에 흡수합니다.
로컬 기동은 평소와 같습니다.
npm run local:dev
이때 scripts/local-dev.mjs에서는 로컬 모드용 환경 변수(Environment Variable)가 일괄 설정됩니다.
VITE_APP_LOCAL_MODE: 'true',
VITE_APP_API_ENDPOINT: `http://127.0.0.1:${port}`,
VITE_APP_HIDDEN_USE_CASES: JSON.stringify({ image: true }),
...
여기서 VITE_APP_HIDDEN_USE_CASES에 transcribe가 포함되어 있지 않기 때문에, 음성 받아쓰기 화면은 표시됩니다.
라우트(Route) 측도 심플합니다.
isUseCaseEnabled('transcribe')
? { path: 'transcribe', element: lazyElement(<TranscribePage />) }
: null
헤더(Header)도 동일한 판정을 사용합니다.
{isUseCaseEnabled('transcribe') && (
<GlobalMenuLink to='/transcribe'>음성 받아쓰기</GlobalMenuLink>
)}
즉, 음성 받아쓰기를 노출할지 여부는 isUseCaseEnabled('transcribe')가 결정합니다.
whisper.cpp는 리포지토리(Repository)에 포함하지 않습니다.
실행 파일, 모델, ffmpeg는 로컬에 두고 환경 변수로 전달합니다.
LOCAL_TRANSCRIBE_ENGINE=whisper
LOCAL_WHISPER_CPP_CPU_BIN=C:\path\to\whisper-cli.exe
LOCAL_WHISPER_CPP_CUDA_BIN=C:\path\to\whisper-cli.exe
...
LOCAL_WHISPER_BACKEND는 auto / cpu / cuda를 지정할 수 있습니다.
auto인 경우, Windows에서 NVIDIA GPU를 발견하고 CUDA 버전의 whisper-cli.exe가 설정되어 있다면 CUDA를 우선합니다. 실패하면 CPU로 폴백(Fallback)합니다.
Local API에서는 기존의 /transcribe/* API를 그대로 받습니다.
if (segments[0] === 'transcribe') {
if (segments[1] === 'capabilities') {
sendJson(res, 200, transcribe.getCapabilities());
...
프론트엔드 입장에서 보면 Cloud든 Local이든 API의 형태는 거의 동일합니다.
덕분에 화면 측의 변경을 최소화할 수 있습니다.
업로드된 음성은 그대로 whisper.cpp에 전달하지 않습니다.
먼저 ffmpeg를 사용하여 16kHz / mono / PCM WAV로 변환합니다.
await this.commandRunner(ffmpegBin, [
'-y',
'-i', inputPath,
...
이 변환 작업을 Local API 측에 배치함으로써, 브라우저는 MP3 / MP4 / WAV / FLAC / OGG / AMR / WebM / M4A를 업로드할 수 있습니다.
실제로 whisper.cpp를 호출하는 핵심 부분은 여기입니다.
const args = [
'-m', modelPath,
'-f', wavPath,
...
포인트는 -oj입니다.
whisper.cpp의 출력을 JSON으로 만들어, 후속 단계에서 Transcript[]로 변환합니다.
또한, CPU 실행 시에는 --no-gpu를 명시하고 있습니다. CUDA 버전과 CPU 버전을 모두 다룰 수 있도록 하기 위함입니다.
whisper.cpp의 JSON을 그대로 화면에 반환하지는 않습니다. 앱 측의 공통 타입(Common Type)에 맞춥니다.
const rawSegments =
output.transcription ||
output.segments ||
...
최종적으로는 다음과 같은 형태가 됩니다.
type Transcript = {
speakerLabel?: string;
transcript: string;
...
이렇게 하면 프론트엔드(Frontend)는 엔진의 차이를 거의 의식하지 않아도 됩니다.
이 구현에서 whisper.cpp 엔진의 화자 인식(Speaker Diarization)은 비활성화되어 있습니다.
getCapabilities() {
return {
engine: this.config.transcribeEngine,
...
화면 측에서도 이 값을 참조합니다.
{supportsSpeakerDiarization && (
<Switch label='화자 인식' checked={speakerLabel} onSwitch={setSpeakerLabel} />
)}
즉, LOCAL_TRANSCRIBE_ENGINE=whisper일 때는 일반적인 받아쓰기(Transcription)만 수행합니다. 화자 인식이 필요한 경우에는 funasr 엔진을 사용한다는 정리입니다.
이번 구현에서 중요했던 점은 whisper.cpp를 억지로 화면에 노출시키지 않는 것이었습니다.
프론트엔드는 기존의 받아쓰기 API를 호출할 뿐입니다. Local API가 ffmpeg, whisper.cpp, JSON 파싱(Parsing), 작업 관리(Job Management)를 담당합니다.
이러한 구조로 만들면 다음과 같은 장점이 있습니다.
- AWS Transcribe에서 로컬 ASR(Automatic Speech Recognition)로 교체하기 쉽다
- 화면 측의 변경 사항이 적다
- CPU / CUDA 전환을 백엔드(Backend) 내부에 가둘 수 있다
- 향후 FunASR 등 다른 엔진도 추가하기 쉽다
개인적으로는 로컬 AI 대응에 있어 "모델을 구동하는 것"보다 "기존 앱의 경계에 어떻게 자연스럽게 포함시킬 것인가"가 더 중요하다고 느꼈습니다.
whisper.cpp는 CLI(Command Line Interface)로서는 단독으로 편리하지만, 웹 애플리케이션에 통합한다면 이번처럼 Local API 안에 가두는 것이 다루기 쉽습니다.
참고한 공개 페이지:
- whisper.cpp README: https://github.com/ggml-org/whisper.cpp/blob/master/README.md
- whisper.cpp models README: https://github.com/ggml-org/whisper.cpp/blob/master/models/README.md
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기