7. 첫 번째 구현: 파일 기반 오프라인 음성 인식 만들기
첫 번째 구현: 파일 기반 오프라인 음성 인식 만들기

가장 먼저 만들 것은 아주 작은 성공이다
처음부터 실시간 마이크 입력, 스트리밍 버퍼, 끊김 없는 자막, 안드로이드 최적화를 한꺼번에 붙이면 높은 확률로 머리가 먼저 끊깁니다. 그래서 첫 구현은 더 단순해야 합니다.
오디오 파일 하나를 넣었더니, 텍스트와 타임스탬프가 나온다.
이 한 줄짜리 성공이 이번 장의 목표입니다.
파일 기반 오프라인 전사는 온디바이스 ASR의 입문 코스로 아주 좋습니다. 입력이 이미 파일로 고정되어 있으니, 마이크 권한이나 실시간 오디오 수집 문제를 잠시 잊을 수 있습니다. 덕분에 우리는 딱 세 가지만 보면 됩니다.
입력 파일
모델 추론
결과 저장
그리고 지금 단계에서는 CPU 우선 전략이 가장 안전합니다. 안드로이드 쪽 가속 경로는 기기와 런타임 편차가 크고, GPU 가속은 여전히 실험과 검증의 성격이 강한 영역이라 "누구에게나 바로 통하는 정답"이라고 말하기 어렵습니다.12 그래서 첫 구현은 욕심을 줄이고, 성공 확률을 올리는 쪽으로 갑니다.
오늘 만들 앱의 모습
오늘 만들 것은 거창한 모바일 앱이 아닙니다. 하지만 분명한 작은 전사 앱입니다. 동작은 아주 단순합니다.
사용자가 오디오 파일 하나를 고릅니다.
Whisper tiny 또는 base 모델이 그 파일을 읽습니다.
전체 전사 텍스트를
.txt로 저장합니다.구간별 타임스탬프를
.json으로 저장합니다.
이 정도만 되어도 이미 쓸모가 있습니다. 회의 음성 초안 만들기, 강의 녹음 훑어보기, 자막 전처리, 음성 메모 검색용 저장까지 충분히 할 수 있습니다.
왜 파일 기반부터 시작하나
실시간 ASR는 재미있지만, 초보자에게는 움직이는 부품이 너무 많습니다. 마이크 입력, 버퍼 길이, VAD, 중간 결과 갱신, 끊김 처리까지 한꺼번에 들어옵니다. 반면 파일 기반 전사는 이미 녹음이 끝난 오디오를 다루므로, 모델이 말을 알아듣는가에 집중하기 좋습니다.
또 Whisper의 기본 전사 흐름 자체도 파일 입력과 잘 맞습니다. 공식 저장소의 transcribe()는 긴 오디오를 내부적으로 나눠 처리하는 방식으로 동작합니다.3 즉, 우리는 파일 하나만 넘겨도 긴 오디오를 구간별로 읽는 흐름을 자연스럽게 경험할 수 있습니다.
초보자 관점에서 보면 장점은 아주 분명합니다.
실패 지점이 적습니다.
결과를 반복해서 비교하기 쉽습니다.
타임스탬프 구조를 눈으로 확인하기 좋습니다.
Step 1. 입력 오디오를 먼저 정리하자
ASR에서 모델보다 먼저 잡아야 할 것은 의외로 오디오 포맷입니다. 오디오가 뒤죽박죽이면 모델은 억울합니다. 사람으로 치면, 시험지는 맞는데 글자가 번져 있는 상황과 비슷합니다.
Whisper는 ffmpeg를 통해 다양한 파일 형식을 다룰 수 있습니다.3 하지만 첫 구현에서는 변수 수를 줄이는 편이 좋습니다. 그래서 가능하면 입력을 16kHz, 모노, WAV로 통일해 두는 것을 추천합니다.
예를 들어 mp3 파일을 안전한 실습용 WAV로 바꾸려면 이렇게 합니다.
ffmpeg -i input.mp3 -ar 16000 -ac 1 -c:a pcm_s16le input.wav뜻은 간단합니다. 샘플레이트를 16kHz로 맞추고, 채널을 1개로 줄이고, PCM WAV로 저장하겠다는 뜻입니다. 첫 실습에서는 이런 "표준 도시락" 같은 입력이 훨씬 편합니다.
오디오 길이는 너무 길지 않은 것이 좋습니다. 10초에서 60초 사이의 깨끗한 음성 파일 하나면 충분합니다. 처음부터 2시간짜리 회의록을 넣는 건, 운전면허 첫날에 바로 고속도로 한복판으로 들어가는 느낌에 가깝습니다.
Step 2. 모델은 tiny와 base 중에서 고른다
이번 장의 주인공은 Whisper tiny와 Whisper base입니다. 둘 다 가볍고, 첫 구현에 적합한 크기입니다. OpenAI 공식 저장소에서도 tiny와 base는 빠른 실험과 속도-정확도 균형 측면에서 자주 쓰이는 작은 모델 라인입니다.3
여기서 중요한 점이 하나 있습니다.
한국어를 포함한 다국어 오디오라면 tiny 또는 base를 고르고, 영어 전용이면 tiny.en 또는 base.en을 고르는 편이 좋다는 점입니다.3
아주 쉽게 정리하면 이렇습니다.
tiny: 가장 가볍고 빠릅니다. 첫 성공을 만들기 좋습니다.
base: tiny보다 조금 무겁지만 결과가 더 안정적인 편입니다.
tiny.en / base.en: 영어 전용 테스트에 적합합니다. 한국어 실습에는 보통 쓰지 않습니다.
한국어 파일을 다룬다면 시작은 보통 이렇게 잡으면 됩니다.
노트북이 조금 약하다면
tiny결과를 조금 더 안정적으로 보고 싶다면
base
처음엔 tiny로 성공하고, 같은 파일을 base로 다시 돌려 차이를 비교해 보는 것도 아주 좋은 공부입니다. "왜 이 단어는 tiny에서 틀리고 base에서 맞았지?" 같은 순간이 모델 감각을 키워 줍니다.
Step 3. 가장 짧은 코드로 먼저 전사해 보자
이제 진짜로 음성을 글자로 바꿔 보겠습니다. 가장 짧은 성공 코드는 놀랄 만큼 짧습니다.
먼저 설치가 되어 있지 않다면 Whisper와 ffmpeg를 준비합니다.3
pip install -U openai-whisper그리고 다음처럼 아주 짧은 스크립트를 만듭니다.
import whisper
model = whisper.load_model("tiny")
result = model.transcribe(
"input.wav",
language="ko",
task="transcribe",
verbose=False,
)
print(result["text"])이 코드는 단순하지만 핵심이 다 들어 있습니다. 모델을 읽고, 파일을 넣고, 전사 결과를 출력합니다.
여기서 language="ko"를 넣은 이유도 초보자에게는 중요합니다. 언어 자동 감지도 가능하지만, 입력 언어가 분명할 때는 언어를 명시하는 편이 해석이 더 안정적인 경우가 많습니다.3
이 단계에서 해야 할 일은 딱 하나입니다.
화면에 텍스트가 정상적으로 나오느냐를 확인하는 것입니다.
줄바꿈이 예쁘지 않아도 괜찮고, 구두점이 조금 어색해도 괜찮습니다. 지금은 "엔진이 돌아간다"가 더 중요합니다.
Step 4. 전사 결과를 파일로 저장하는 작은 앱으로 바꾸자
화면에만 찍히면 금방 사라집니다. 우리가 원하는 것은 다시 열어볼 수 있는 결과물입니다. 그래서 이번에는 .txt와 .json 파일로 저장해 보겠습니다.
아래 코드는 입력 파일을 받아 전사하고, 전체 텍스트와 세그먼트 타임스탬프를 저장하는 아주 작은 오프라인 전사 앱입니다.
import argparse
import json
from pathlib import Path
import whisper
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--input", required=True, help="입력 오디오 파일 경로")
parser.add_argument("--model", default="tiny", choices=["tiny", "base"], help="Whisper 모델")
parser.add_argument("--language", default="ko", help="언어 코드. 예: ko, en")
parser.add_argument("--output_dir", default="outputs", help="결과 저장 폴더")
args = parser.parse_args()
input_path = Path(args.input)
output_dir = Path(args.output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
model = whisper.load_model(args.model)
result = model.transcribe(
str(input_path),
language=args.language,
task="transcribe",
verbose=False,
)
text = result["text"].strip()
segments = result.get("segments", [])
stem = input_path.stem
txt_path = output_dir / f"{stem}.txt"
json_path = output_dir / f"{stem}.json"
txt_path.write_text(text, encoding="utf-8")
json_path.write_text(
json.dumps(
{
"language": result.get("language"),
"text": text,
"segments": [
{
"id": seg.get("id"),
"start": seg.get("start"),
"end": seg.get("end"),
"text": seg.get("text", "").strip(),
}
for seg in segments
],
},
ensure_ascii=False,
indent=2,
),
encoding="utf-8",
)
print(f"[완료] 텍스트 저장: {txt_path}")
print(f"[완료] 세그먼트 저장: {json_path}")
print(f"n전체 전사 결과:n{text}")
if __name__ == "__main__":
main()실행은 이렇게 하면 됩니다.
python transcribe_offline.py --input input.wav --model tiny --language ko조금 더 안정적인 결과를 보고 싶다면 tiny 대신 base로 바꾸면 됩니다.
python transcribe_offline.py --input input.wav --model base --language ko이제부터는 단순한 실험이 아니라, 입력과 출력이 있는 작은 전사 도구가 됩니다. 파일 하나 넣으면 결과 파일 두 개가 나옵니다.
Step 5. 타임스탬프 흐름을 읽는 법
초보자가 여기서 가장 기뻐하는 순간이 바로 이것입니다.
"오, 글자만 나오는 게 아니라 시간도 같이 나오네?"
Whisper의 전사 결과는 전체 텍스트뿐 아니라 세그먼트 정보도 함께 담고 있습니다.3 세그먼트는 쉽게 말하면 자막 한 줄짜리 덩어리입니다. 예를 들어 결과 JSON은 대략 이런 모양이 됩니다.
{
"language": "ko",
"text": "안녕하세요. 오프라인 전사를 시작합니다.",
"segments": [
{
"id": 0,
"start": 0.0,
"end": 1.8,
"text": "안녕하세요."
},
{
"id": 1,
"start": 1.8,
"end": 4.7,
"text": "오프라인 전사를 시작합니다."
}
]
}여기서 봐야 할 핵심은 세 가지입니다.
start: 이 문장이 시작하는 시간end: 이 문장이 끝나는 시간text: 그 구간에서 인식된 문장
즉, 타임스탬프 흐름은 오디오 시간축 위에 문장을 붙여 놓은 지도라고 생각하면 됩니다. 이 정보가 있으면 나중에 다음 같은 기능으로 확장할 수 있습니다.
특정 구간 클릭 시 해당 오디오로 이동하기
SRT 자막 파일 만들기
긴 녹음을 문장 단위로 검색하기
여기서 한 가지는 꼭 기억해 두면 좋습니다. 이 단계의 타임스탬프는 문장 또는 세그먼트 수준으로 이해하는 편이 좋습니다. 단어별 타임스탬프까지 정밀하게 다루는 것은 다음 단계의 주제입니다.
Step 6. 결과가 이상할 때 어디를 먼저 의심할까
처음 구현은 보통 코드보다 입력에서 더 많이 틀립니다. 아래 네 가지는 정말 자주 만납니다.
한국어 파일인데 영어 전용 모델을 쓴 경우:
tiny.en,base.en은 영어용입니다. 한국어에는tiny,base를 쓰는 편이 맞습니다.3오디오 포맷이 들쭉날쭉한 경우: mp3, m4a, stereo, 44.1kHz가 섞이면 디버깅이 꼬입니다. 첫 실습은 WAV 16kHz 모노로 통일하는 게 좋습니다.
파일이 너무 길거나 잡음이 많은 경우: 첫 성공은 짧고 깨끗한 파일로 만드는 편이 낫습니다.
tiny 결과를 보고 "모델이 바보다"라고 결론 내리는 경우: tiny는 원래 빠른 대신 정확도가 낮을 수 있습니다. base로 한 번 더 확인해 보세요.
실전에서는 작은 실수 하나가 큰 오해를 만듭니다. 예를 들어 파일 품질이 안 좋은데 모델을 탓하거나, tiny를 써 놓고 large급 정확도를 기대하는 일이 흔합니다. 처음에는 모델을 평가하기보다 입력과 설정을 먼저 평가하는 습관이 더 중요합니다.
Step 7. 직접 만든 결과를 바로 검증하는 법
내가 만든 전사 앱이 맞게 동작하는지 빠르게 감을 잡고 싶다면, 같은 파일을 이미 존재하는 로컬 전사 도구에도 넣어 보면 좋습니다. 비교 대상이 하나 생기면 디버깅이 훨씬 쉬워집니다.
예를 들어:
whisper.cppCLI로 같은 파일을 다시 돌려 본다.4안드로이드에서는
whisper_android같은 오픈소스 예제를 비교 기준으로 삼아 본다.5
여기서 중요한 포인트는 하나입니다.
비교 대상이 있으면 마음이 편해진다는 것입니다.
내가 만든 스크립트만 보고 있으면 무엇이 잘못인지 감이 잘 안 옵니다. 하지만 같은 파일을 다른 로컬 도구에 넣어 봤을 때 비슷한 결과가 나온다면, 내 파이프라인이 대체로 맞게 흘러간다고 볼 수 있습니다.
Step 8. 안드로이드로 옮기기 전에 머릿속에 남겨 둘 그림
지금 만든 것은 Python 스크립트지만, 구조는 안드로이드 앱과 크게 다르지 않습니다.
흐름을 다시 그려 보면 이렇습니다.
파일 선택
오디오 디코딩 또는 정규화
모델 로드
전사 실행
텍스트와 세그먼트 표시
파일 저장 또는 공유
즉, 나중에 안드로이드로 가도 바뀌는 것은 주로 UI와 런타임이지, 본질적인 데이터 흐름은 거의 같습니다. 그래서 지금 배우는 입력 파일, 전사 결과, 타임스탬프 구조가 아주 중요합니다.
그리고 다시 한 번 강조하자면, 안드로이드에서는 CPU와 파일 기반으로 기준선을 만드는 것이 맞는 출발점입니다. GPU 가속은 여전히 기기와 백엔드에 따라 계속 검증해야 하는 영역이기 때문입니다.12
이 장을 마치면 되어 있어야 하는 것
이 장을 끝냈다면, 적어도 다음 상태에는 도달해 있어야 합니다.
Whisper tiny 또는 base 모델로 로컬 전사가 된다.
입력 파일 하나를
.txt로 저장할 수 있다.세그먼트별
start,end,text를 JSON으로 저장할 수 있다.같은 파일을 모델만 바꿔 비교할 수 있다.
파일 기반 오프라인 전사가 왜 첫 구현으로 적합한지 설명할 수 있다.
이제 우리는 "음성을 텍스트로 만드는 첫 성공"을 손에 넣었습니다. 다음 단계부터는 여기에 실시간성이라는 성질을 붙이면 됩니다. 지금은 이미 사진 촬영에는 성공한 셈이고, 다음 장에서는 이걸 동영상처럼 바꾸는 일이 기다리고 있습니다.
참고
1Neural Networks API | Android Developers
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.