핵심 답변 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초로 같았습니다.

참고 자료


이 글을 쓴 사람

작성·검토 방식 · 오류 제보·문의