import sys import os import subprocess import json import tempfile import re import chardet # 추가: 인코딩 감지를 위한 라이브러리 from PyQt5.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QWidget, QPushButton, QLabel, QFileDialog, QTextEdit, QProgressBar, QSlider, QMessageBox) from PyQt5.QtCore import QThread, pyqtSignal, QTimer, Qt from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent from PyQt5.QtMultimediaWidgets import QVideoWidget from PyQt5.QtCore import QUrl
class WhisperTranscriptionThread(QThread): progress_updated = pyqtSignal(int) transcription_ready = pyqtSignal(list) error_signal = pyqtSignal(str) def __init__(self, video_path): super().__init__() self.video_path = video_path def run(self): try: # 임시 자막 파일 생성 temp_srt = tempfile.mktemp(suffix='.srt') self.progress_updated.emit(10) # FFmpeg의 Whisper 필터를 사용하여 직접 자막 생성 cmd = [ 'ffmpeg', '-i', self.video_path, '-af', 'aresample=16000', # 16kHz로 리샘플링 '-f', 'wav', '-', # WAV를 stdout으로 출력 '|', 'ffmpeg', '-f', 'wav', '-i', '-', # stdin에서 WAV 입력 '-af', 'whisper=model=base:language=ko', # Whisper 필터 적용 '-f', 'srt', temp_srt # SRT 형식으로 출력 ] # 더 간단한 방법: 직접 한 번에 처리 cmd = [ 'ffmpeg', '-i', self.video_path, '-af', 'whisper=model=base:language=ko', '-f', 'srt', '-y', temp_srt ] self.progress_updated.emit(30) # FFmpeg 실행 - UTF-8 인코딩 명시적 지정 process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, encoding='utf-8', # 추가: UTF-8 인코딩 명시 errors='replace') # 추가: 오류 문자 처리 stderr_output = "" while True: output = process.stderr.readline() if output == '' and process.poll() is not None: break if output: stderr_output += output # 진행률 추정 (시간 정보를 기반으로) if 'time=' in output: time_match = re.search(r'time=(\d+):(\d+):(\d+)', output) if time_match: progress = min(90, 30 + (60 * int(time_match.group(3)) / 100)) self.progress_updated.emit(int(progress)) process.wait() if process.returncode == 0: # SRT 파일 파싱 subtitles = self.parse_srt_file(temp_srt) self.progress_updated.emit(100) self.transcription_ready.emit(subtitles) # 임시 파일 삭제 if os.path.exists(temp_srt): os.unlink(temp_srt) else: error_msg = f"FFmpeg Whisper 처리 실패\n\n상세 오류:\n{stderr_output}" self.error_signal.emit(error_msg) except Exception as e: self.error_signal.emit(f"음성 인식 오류: {str(e)}") def parse_srt_file(self, srt_path): """SRT 파일을 파싱하여 자막 리스트로 변환 - 인코딩 문제 해결""" subtitles = [] try: # 파일이 존재하는지 확인 if not os.path.exists(srt_path): print(f"SRT 파일이 존재하지 않습니다: {srt_path}") return subtitles # 인코딩 감지 시도 encoding = 'utf-8' # 기본값 try: with open(srt_path, 'rb') as f: raw_data = f.read() if raw_data: detected = chardet.detect(raw_data) if detected['encoding']: encoding = detected['encoding'] print(f"감지된 인코딩: {encoding}") except Exception as e: print(f"인코딩 감지 실패, UTF-8 사용: {e}") # 여러 인코딩으로 시도 encodings_to_try = [encoding, 'utf-8', 'cp949', 'euc-kr', 'latin1'] content = None for enc in encodings_to_try: try: with open(srt_path, 'r', encoding=enc, errors='replace') as f: content = f.read() print(f"파일 읽기 성공 - 인코딩: {enc}") break except Exception as e: print(f"인코딩 {enc} 실패: {e}") continue if content is None: print("모든 인코딩 시도 실패") return subtitles # SRT 형식 파싱 blocks = content.strip().split('\n\n') for block in blocks: lines = block.strip().split('\n') if len(lines) >= 3: # 시간 정보 파싱 time_line = lines[1] time_match = re.match(r'(\d+):(\d+):(\d+),(\d+) --> (\d+):(\d+):(\d+),(\d+)', time_line) if time_match: start_time = (int(time_match.group(1)) * 3600 + int(time_match.group(2)) * 60 + int(time_match.group(3)) + int(time_match.group(4)) / 1000) end_time = (int(time_match.group(5)) * 3600 + int(time_match.group(6)) * 60 + int(time_match.group(7)) + int(time_match.group(8)) / 1000) # 텍스트 (여러 줄일 수 있음) text = '\n'.join(lines[2:]) subtitles.append({ "start": start_time, "end": end_time, "text": text }) except Exception as e: print(f"SRT 파싱 오류: {e}") return subtitles
# VideoSubtitlePlayer 클래스는 그대로 유지... class VideoSubtitlePlayer(QMainWindow): def __init__(self): super().__init__() self.subtitles = [] self.current_subtitle_index = 0 self.video_path = "" self.initUI() self.setupMediaPlayer() self.setupTimer() def initUI(self): self.setWindowTitle("FFmpeg Whisper 영상 자막 생성기") self.setGeometry(100, 100, 1000, 700) central_widget = QWidget() self.setCentralWidget(central_widget) layout = QVBoxLayout(central_widget) # 파일 선택 버튼 file_layout = QHBoxLayout() self.file_button = QPushButton("영상 파일 선택") self.file_button.clicked.connect(self.select_video_file) self.file_label = QLabel("파일이 선택되지 않았습니다") file_layout.addWidget(self.file_button) file_layout.addWidget(self.file_label) layout.addLayout(file_layout) # 자막 생성 버튼과 옵션 subtitle_layout = QHBoxLayout() self.generate_button = QPushButton("자막 생성 (FFmpeg Whisper)") self.generate_button.clicked.connect(self.generate_subtitles) self.generate_button.setEnabled(False) subtitle_layout.addWidget(self.generate_button) layout.addLayout(subtitle_layout) # 진행률 표시 self.progress_bar = QProgressBar() self.progress_bar.setVisible(False) self.progress_label = QLabel("") self.progress_label.setVisible(False) layout.addWidget(self.progress_bar) layout.addWidget(self.progress_label) # 비디오 위젯 self.video_widget = QVideoWidget() layout.addWidget(self.video_widget) # 현재 자막 표시 self.subtitle_label = QLabel("") self.subtitle_label.setAlignment(Qt.AlignCenter) self.subtitle_label.setStyleSheet(""" QLabel { background-color: rgba(0, 0, 0, 180); color: white; font-size: 18px; font-weight: bold; padding: 15px; border-radius: 8px; margin: 5px; } """) self.subtitle_label.setMaximumHeight(100) self.subtitle_label.setWordWrap(True) layout.addWidget(self.subtitle_label) # 미디어 컨트롤 control_layout = QHBoxLayout() self.play_button = QPushButton("재생") self.play_button.clicked.connect(self.toggle_play) self.play_button.setEnabled(False) self.position_slider = QSlider(Qt.Horizontal) self.position_slider.sliderMoved.connect(self.set_position) self.position_slider.setEnabled(False) self.time_label = QLabel("00:00 / 00:00") control_layout.addWidget(self.play_button) control_layout.addWidget(self.position_slider) control_layout.addWidget(self.time_label) layout.addLayout(control_layout) # 자막 텍스트 에디터 self.subtitle_text = QTextEdit() self.subtitle_text.setMaximumHeight(150) self.subtitle_text.setPlaceholderText("FFmpeg Whisper로 생성된 자막이 여기에 표시됩니다...") layout.addWidget(self.subtitle_text) # 저장 버튼 self.save_button = QPushButton("자막 파일 저장 (SRT)") self.save_button.clicked.connect(self.save_subtitles) self.save_button.setEnabled(False) layout.addWidget(self.save_button) def setupMediaPlayer(self): # Windows에서 미디어 백엔드 설정 import os os.environ['QT_MULTIMEDIA_PREFERRED_PLUGINS'] = 'windowsmediafoundation' self.media_player = QMediaPlayer(None, QMediaPlayer.VideoSurface) self.media_player.setVideoOutput(self.video_widget) # 에러 처리 추가 self.media_player.error.connect(self.handle_media_error) self.media_player.mediaStatusChanged.connect(self.handle_media_status) self.media_player.stateChanged.connect(self.media_state_changed) self.media_player.positionChanged.connect(self.position_changed) self.media_player.durationChanged.connect(self.duration_changed)
def handle_media_status(self, status): """미디어 상태 처리""" status_messages = { QMediaPlayer.NoMedia: "미디어 없음", QMediaPlayer.LoadingMedia: "미디어 로딩 중", QMediaPlayer.LoadedMedia: "미디어 로드 완료", QMediaPlayer.StalledMedia: "미디어 정지됨", QMediaPlayer.BufferingMedia: "버퍼링 중", QMediaPlayer.BufferedMedia: "버퍼링 완료", QMediaPlayer.EndOfMedia: "미디어 끝", QMediaPlayer.InvalidMedia: "잘못된 미디어" } print(f"미디어 상태: {status_messages.get(status, str(status))}") if status == QMediaPlayer.InvalidMedia: QMessageBox.warning(self, "미디어 오류", "선택된 파일을 재생할 수 없습니다.\n다른 형식의 파일을 시도해보세요.")
def handle_media_error(self, error): """미디어 에러 처리""" error_messages = { QMediaPlayer.NoError: "에러 없음", QMediaPlayer.ResourceError: "리소스 에러 - 파일을 찾을 수 없거나 접근할 수 없습니다", QMediaPlayer.FormatError: "포맷 에러 - 지원되지 않는 형식입니다", QMediaPlayer.NetworkError: "네트워크 에러", QMediaPlayer.AccessDeniedError: "접근 거부 에러", QMediaPlayer.ServiceMissingError: "서비스 누락 에러 - 코덱이 없을 수 있습니다" } error_msg = error_messages.get(error, f"알 수 없는 에러: {error}") print(f"미디어 에러: {error_msg}") QMessageBox.critical(self, "미디어 재생 오류", f"비디오를 재생할 수 없습니다.\n\n{error_msg}") def setupTimer(self): self.timer = QTimer() self.timer.timeout.connect(self.update_subtitle) self.timer.start(100) # 100ms마다 자막 업데이트 def select_video_file(self): file_path, _ = QFileDialog.getOpenFileName( self, "영상 파일 선택", "", "Video Files (*.mp4 *.avi *.mov *.mkv *.wmv *.flv *.webm *.m4v)" ) if file_path: self.video_path = file_path self.file_label.setText(os.path.basename(file_path)) self.generate_button.setEnabled(True) # 비디오 로드 self.media_player.setMedia(QMediaContent(QUrl.fromLocalFile(file_path))) self.play_button.setEnabled(True) self.position_slider.setEnabled(True) def generate_subtitles(self): if not self.video_path: return self.generate_button.setEnabled(False) self.progress_bar.setVisible(True) self.progress_label.setVisible(True) self.progress_bar.setValue(0) self.progress_label.setText("FFmpeg Whisper로 음성을 인식하는 중...") # FFmpeg Whisper 스레드 시작 self.whisper_thread = WhisperTranscriptionThread(self.video_path) self.whisper_thread.progress_updated.connect(self.progress_bar.setValue) self.whisper_thread.transcription_ready.connect(self.on_transcription_ready) self.whisper_thread.error_signal.connect(self.show_error) self.whisper_thread.start() def on_transcription_ready(self, subtitles): self.subtitles = subtitles self.progress_bar.setVisible(False) self.progress_label.setVisible(False) self.generate_button.setEnabled(True) self.save_button.setEnabled(True) # 자막 텍스트 표시 subtitle_text = "" for i, subtitle in enumerate(subtitles): start_time = self.format_srt_time(subtitle["start"]) end_time = self.format_srt_time(subtitle["end"]) subtitle_text += f"{i+1}\n{start_time} --> {end_time}\n{subtitle['text']}\n\n" self.subtitle_text.setText(subtitle_text) QMessageBox.information(self, "완료", f"FFmpeg Whisper로 자막 생성이 완료되었습니다!\n총 {len(subtitles)}개의 자막이 생성되었습니다.") def show_error(self, error_message): self.progress_bar.setVisible(False) self.progress_label.setVisible(False) self.generate_button.setEnabled(True) QMessageBox.critical(self, "오류", error_message) def toggle_play(self): if self.media_player.state() == QMediaPlayer.PlayingState: self.media_player.pause() else: self.media_player.play() def media_state_changed(self, state): if state == QMediaPlayer.PlayingState: self.play_button.setText("일시정지") else: self.play_button.setText("재생") def position_changed(self, position): self.position_slider.setValue(position) # 시간 표시 업데이트 current_time = self.format_time(position / 1000) total_time = self.format_time(self.media_player.duration() / 1000) self.time_label.setText(f"{current_time} / {total_time}") def duration_changed(self, duration): self.position_slider.setRange(0, duration) def set_position(self, position): self.media_player.setPosition(position) def update_subtitle(self): if not self.subtitles or self.media_player.state() != QMediaPlayer.PlayingState: return current_time = self.media_player.position() / 1000 # 초 단위 # 현재 시간에 해당하는 자막 찾기 current_subtitle = "" for subtitle in self.subtitles: if subtitle["start"] <= current_time <= subtitle["end"]: current_subtitle = subtitle["text"] break self.subtitle_label.setText(current_subtitle) def format_time(self, seconds): if seconds < 0: seconds = 0 hours = int(seconds // 3600) minutes = int((seconds % 3600) // 60) secs = int(seconds % 60) if hours > 0: return f"{hours:02d}:{minutes:02d}:{secs:02d}" else: return f"{minutes:02d}:{secs:02d}" def save_subtitles(self): if not self.subtitles: return file_path, _ = QFileDialog.getSaveFileName( self, "자막 파일 저장", "", "SRT Files (*.srt)" ) if file_path: try: # UTF-8 BOM과 함께 저장하여 인코딩 문제 방지 with open(file_path, 'w', encoding='utf-8-sig') as f: for i, subtitle in enumerate(self.subtitles): start_time = self.format_srt_time(subtitle["start"]) end_time = self.format_srt_time(subtitle["end"]) f.write(f"{i+1}\n") f.write(f"{start_time} --> {end_time}\n") f.write(f"{subtitle['text']}\n\n") QMessageBox.information(self, "저장 완료", "자막 파일이 저장되었습니다!") except Exception as e: QMessageBox.critical(self, "저장 오류", f"파일 저장 중 오류가 발생했습니다: {str(e)}") def format_srt_time(self, seconds): if seconds < 0: seconds = 0 hours = int(seconds // 3600) minutes = int((seconds % 3600) // 60) secs = int(seconds % 60) milliseconds = int((seconds - int(seconds)) * 1000) return f"{hours:02d}:{minutes:02d}:{secs:02d},{milliseconds:03d}"
def check_ffmpeg_whisper(): """FFmpeg에 Whisper 지원이 있는지 확인""" try: result = subprocess.run(['ffmpeg', '-filters'], capture_output=True, text=True, check=True) return 'whisper' in result.stdout except: return False
def main():
app = QApplication(sys.argv) # FFmpeg 설치 확인 try: result = subprocess.run(['ffmpeg', '-version'], capture_output=True, text=True, check=True) # FFmpeg 버전 확인 if 'ffmpeg version' not in result.stdout: raise Exception("FFmpeg 버전 정보를 찾을 수 없습니다.") # Whisper 필터 지원 확인 if not check_ffmpeg_whisper(): QMessageBox.warning(None, "경고", "FFmpeg에 Whisper 필터가 포함되어 있지 않습니다.\n" "FFmpeg 8.0 이상 버전이 필요하며, Whisper 지원이 포함된 빌드여야 합니다.\n\n" "프로그램을 계속 실행하지만 자막 생성이 실패할 수 있습니다.") except (subprocess.CalledProcessError, FileNotFoundError): QMessageBox.critical(None, "오류", "FFmpeg가 설치되어 있지 않거나 PATH에 등록되어 있지 않습니다.\n" "FFmpeg 8.0 이상을 설치하고 PATH에 등록해주세요.") return window = VideoSubtitlePlayer() window.show() sys.exit(app.exec_())
if __name__ == '__main__': main() |
Comments ()