핵심 답변 Whisper로 만든 .srt 자막이 곡보다 길게 끝나면, 파일 길이 말고 ffmpeg silencedetect의 -40dB 무음 시작 시각을 기준으로 그보다 늦게 끝나는 자막을 지웁니다(아주 작은 소리나 경계에 걸친 자막은 귀로 한 번 더 확인). 저희 127.56초 곡은 -40dB 기준 무음이 124.83초에 시작했고, 이 기준으로 자막 48개 중 14개를 지우자 마지막 자막이 124.76초에 끝났습니다.
무슨 일이 있었나
저희가 Gemini 앱으로 만든 AI 노래 한 곡(생성 기록상 모델 Lyria 3.5, MP3, ffprobe 기준 127.56초)을 mlx-whisper(large-v3-turbo, 오프라인)로 받아써 .srt 자막을 만들었습니다. 마지막 자막은 133.76초에 끝났고, 파일이 끝난 뒤에 시작하는 자막이 11개였습니다. 내용은 마지막 가사 반복, 글자 없는 빈 자막, 느낌표 하나였습니다. 영어권에서도 같은 일이 보고됐습니다. Whisper 공식 저장소 토론 게시판에 소리 파일 끝에 앞 문장이 반복되거나 뒤섞이는 ‘유령 자막’을 다룬 글이 있고, 추천 39개와 댓글 38개가 달렸습니다. 아래는 2026년 9월 29~30일 맥 한 대에서 곡 하나로 잰 결과이며 Whisper 전체의 규칙이 아닙니다.
같은 곡, 다른 끝
옵션 기록이 없는 첫 실행과 단어별 시각(word_timestamps) 없이 돌린 네 번, 모두 다섯 번이 파일 끝을 넘겼지만 끝난 자리는 달랐습니다. 첫 실행은 151.18초, 명령줄 실행은 133.76초, 파이썬 API 기본값(언어만 영어 지정) 세 번은 모두 154.74초였습니다. 첫 실행과 나중 실행은 문장과 줄 수도 달랐는데 이유는 확인하지 못했습니다. 반면 word_timestamps=True로 돌린 여섯 번은 모두 127.5초, 파일 안에서 끝났습니다.
1단계: 소리 끝 찾기
파일 끝이 곧 소리 끝은 아닙니다. 끝에 조용한 꼬리가 붙을 수 있습니다. ffmpeg 9.0.1의 silencedetect로 조용한 구간을 찾습니다.
ffmpeg -hide_banner -nostats -i song.mp3 -af silencedetect=noise=-40dB:d=0.5 -f null -
저희 곡에서는 silence_start 124.830181, silence_end 127.555918이 나왔습니다. -40dB 기준 무음이 124.83초쯤 시작하고 마지막 2.73초는 -40dB보다 조용하다는 뜻입니다. 기준값은 고르기 나름입니다. ffmpeg 기본값은 -60dB·2초이고, -50dB·0.5초로 재면 같은 곡이 124.95초로 나왔습니다. 쓴 값을 자막 파일 옆에 적어 둡니다.
2단계: 기준 하나로 자르기
‘남는 자막을 지운다’는 말도 기준에 따라 결과가 다릅니다. 같은 파일에 기준 네 가지를 적용했습니다.
| 지우는 자막 | 지움 | 남음 | 마지막 자막 끝 |
|---|---|---|---|
| 파일 끝(127.56초) 이후 시작 | 11 | 37 | 127.76초(여전히 파일 밖) |
| 파일 끝 이후 끝남 | 12 | 36 | 126.76초 |
| 소리 끝(124.83초) 이후 시작 | 13 | 35 | 125.76초 |
| 소리 끝 이후 끝남 | 14 | 34 | 124.76초 |
저희는 마지막 기준을 씁니다. 조용한 꼬리 위에 자막이 하나도 남지 않은 기준은 이것뿐이었습니다. 지운 14개는 마지막 가사 반복 8개, 빈 자막 5개, 느낌표 1개였습니다. 가장 자연스러워 보이는 첫 기준은 마지막 가사 세 줄을 무음 위에 남겼습니다. 아래 스크립트는 남기는 자막 블록의 글자를 바꾸지 않고(저희 입력처럼 UTF-8·LF 파일 기준 — CRLF 파일은 텍스트 모드로 열 때 줄바꿈이 LF로 바뀝니다) -40dB 무음 시작보다 늦게 끝나는 블록만 뺍니다. 저희 파일에서 kept 34, removed 14를 출력했고, 결과 파일은 더 긴 점검 도구로 먼저 만든 수정본과 바이트 단위로 같았습니다.
import re
SOUND_END = 124.83 # seconds: silence_start from ffmpeg silencedetect
TIME = r"(\d\d):(\d\d):(\d\d),(\d\d\d)"
CUE = re.compile(r"(?m)^(\d+)\r?\n" + TIME + r" --> " + TIME + r"[^\n]*\n")
def seconds(h, m, s, ms):
return int(h) * 3600 + int(m) * 60 + int(s) + int(ms) / 1000
raw = open("song.srt", encoding="utf-8").read()
cues = list(CUE.finditer(raw))
kept, removed = [], []
for i, cue in enumerate(cues):
block_end = cues[i + 1].start() if i + 1 < len(cues) else len(raw)
block = raw[cue.start():block_end]
end = seconds(*cue.group(6, 7, 8, 9))
(kept if end <= SOUND_END else removed).append(block)
open("song.trimmed.srt", "w", encoding="utf-8").write("".join(kept))
print(f"kept {len(kept)}, removed {len(removed)}")
번호는 다시 매기지 않습니다. 저희 파일은 지운 자막이 모두 끝에 있어 번호가 이어졌지만, 중간이 빠지면 번호를 다시 매기고 플레이어에서 확인한 뒤 올리세요.
확인하지 않는 것
- 가사가 맞는지는 따로 봐야 합니다. 가사·클리핑·끝부분 점검 글(영문)에 적었습니다.
- 곡 중간의 반복이나 지어낸 줄은 못 잡습니다. 꼬리만 정리합니다.
자주 묻는 질문
Whisper는 왜 곡이 끝난 뒤에도 자막을 쓰나요?
저희 파일의 원인은 확인하지 못했습니다. Whisper 원본 코드 설명에는 앞 구간 문장을 다음 구간에 넘기는 설정(condition_on_previous_text)이 반복이나 시각 어긋남 같은 루프를 만들 수 있다고 적혀 있습니다. 위 토론 글도 이 설정과 짧은 마지막 구간을 원인으로 추정했지만, 저희는 그 가설을 시험하지 않았습니다.
word_timestamps=True로 돌리면 해결되나요?
저희 곡에서는 여섯 번 모두 127.5초로 파일 안에서 끝났지만 소리 끝(124.83초)보다는 늦었습니다. 곡 하나의 결과이니 무음 확인은 그대로 하세요.
hallucination_silence_threshold는 무엇을 하나요?
Whisper와 mlx-whisper 코드 기준으로 단어별 시각을 켰을 때만 동작하고, 환각이 의심되면 기준보다 긴 무음을 건너뜁니다. 저희 곡에서는 2초로 줘도 끝 시각이 127.5초로 같았습니다.
참고 자료
- openai/whisper 토론 #679, KaiserChr (2026-10-01 기준 추천 39·댓글 38)
- FFmpeg 필터 문서: silencedetect
- openai/whisper transcribe.py
- ml-explore/mlx-examples: whisper