Jeongsoo1975 commited on
Commit
ae9ec05
·
0 Parent(s):

Initial commit: Gradio text-based speaker separation app for Hugging Face Spaces

Browse files
Files changed (14) hide show
  1. .gitignore +122 -0
  2. README.md +60 -0
  3. README_backup.md +152 -0
  4. app.py +251 -0
  5. audio_summarizer.py +491 -0
  6. check_models.py +33 -0
  7. data/.gitkeep +2 -0
  8. deployment_guide.md +154 -0
  9. env_example.txt +17 -0
  10. output/.gitkeep +2 -0
  11. requirements.txt +4 -0
  12. stt_processor.py +294 -0
  13. test_gradio.py +135 -0
  14. test_stt.py +208 -0
.gitignore ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 대용량 파일들
2
+ data/*.wav
3
+ data/*.mp3
4
+ data/*.mp4
5
+ docs/
6
+ *.pdf
7
+
8
+ # 환경 파일들
9
+ .env
10
+ .env.local
11
+ .env.production
12
+
13
+ # Python
14
+ __pycache__/
15
+ *.py[cod]
16
+ *$py.class
17
+ *.so
18
+ .Python
19
+ build/
20
+ develop-eggs/
21
+ dist/
22
+ downloads/
23
+ eggs/
24
+ .eggs/
25
+ lib/
26
+ lib64/
27
+ parts/
28
+ sdist/
29
+ var/
30
+ wheels/
31
+ share/python-wheels/
32
+ *.egg-info/
33
+ .installed.cfg
34
+ *.egg
35
+ MANIFEST
36
+
37
+ # PyTorch
38
+ *.pth
39
+ *.pt
40
+
41
+ # Jupyter Notebook
42
+ .ipynb_checkpoints
43
+
44
+ # IPython
45
+ profile_default/
46
+ ipython_config.py
47
+
48
+ # pyenv
49
+ .python-version
50
+
51
+ # pipenv
52
+ Pipfile.lock
53
+
54
+ # poetry
55
+ poetry.lock
56
+
57
+ # celery beat schedule file
58
+ celerybeat-schedule
59
+ celerybeat.pid
60
+
61
+ # SageMath parsed files
62
+ *.sage.py
63
+
64
+ # Environments
65
+ .venv
66
+ env/
67
+ venv/
68
+ ENV/
69
+ env.bak/
70
+ venv.bak/
71
+
72
+ # Spyder project settings
73
+ .spyderproject
74
+ .spyproject
75
+
76
+ # Rope project settings
77
+ .ropeproject
78
+
79
+ # mkdocs documentation
80
+ /site
81
+
82
+ # mypy
83
+ .mypy_cache/
84
+ .dmypy.json
85
+ dmypy.json
86
+
87
+ # Pyre type checker
88
+ .pyre/
89
+
90
+ # pytype static type analyzer
91
+ .pytype/
92
+
93
+ # Cython debug symbols
94
+ cython_debug/
95
+
96
+ # IDE
97
+ .vscode/
98
+ .idea/
99
+ *.swp
100
+ *.swo
101
+ *~
102
+
103
+ # OS
104
+ .DS_Store
105
+ Thumbs.db
106
+
107
+ # Environment variables
108
+ .env
109
+
110
+ # Log files
111
+ *.log
112
+
113
+ # Temporary files
114
+ temp_segment_*.wav
115
+
116
+ # Data and Output folders (keep structure but ignore contents)
117
+ output/*.txt
118
+ output/*.json
119
+
120
+ # Keep folder structure
121
+ !data/.gitkeep
122
+ !output/.gitkeep
README.md ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: 2인 대화 화자 분리기 (AI)
3
+ emoji: 💬
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: gradio
7
+ sdk_version: 4.44.0
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
+ ---
12
+
13
+ # 2인 대화 화자 분리기 (AI)
14
+
15
+ Gemini 2.0 Flash AI를 사용하여 텍스트 대화를 화자별로 자동 분리하고 맞춤법을 교정하는 웹 애플리케이션입니다.
16
+
17
+ ## 🎯 주요 기능
18
+
19
+ 1. **WAV 파일 업로드**: 웹 인터페이스를 통한 간편한 파일 업로드
20
+ 2. **고정밀 음성 인식**: OpenAI Whisper를 사용한 음성-텍스트 변환
21
+ 3. **AI 화자 분리**: Google Gemini를 사용한 텍스트 기반 2인 대화 분리
22
+ 4. **실시간 결과**: 웹에서 즉시 결과 확인 및 다운로드
23
+
24
+ ## 🛠 기술 스택
25
+
26
+ - **UI Framework**: Gradio (웹 인터페이스)
27
+ - **음성 인식**: OpenAI Whisper
28
+ - **AI 화자 분리**: Google Gemini Pro
29
+ - **호스팅**: Hugging Face Spaces
30
+
31
+ ## 📝 사용 방법
32
+
33
+ 1. WAV 파일을 업로드하세요
34
+ 2. "처리 시작" 버튼을 클릭하세요
35
+ 3. 처리 완료 후 결과를 확인하세요:
36
+ - 원본 텍스트
37
+ - 화자별 분리 결과
38
+ - 맞춤법 교정 결과
39
+
40
+ ## ⚙️ API 설정
41
+
42
+ 이 애플리케이션은 Google AI API를 사용합니다. Hugging Face Spaces의 Settings에서 다음 환경 변수를 설정해야 합니다:
43
+
44
+ - `GOOGLE_API_KEY`: Google AI Studio에서 발급받은 API 키
45
+
46
+ ## 🎤 화자 분리 정확도
47
+
48
+ Gemini AI의 텍스트 기반 화자 분리는 다음 요소들을 분석합니다:
49
+
50
+ - **대화 맥락**: 질문과 답변의 패턴
51
+ - **말투 변화**: 존댓말/반말, 어조 변화
52
+ - **주제 전환**: 화자별 관심사나 역할
53
+ - **언어 패턴**: 개인별 표현 습관
54
+
55
+ ## ⚠️ 주의사항
56
+
57
+ - WAV 형식의 오디오 파일만 지원됩니다
58
+ - 2인 대화에 최적화되어 있습니다
59
+ - 처리 시간은 파일 길이에 따라 달라집니다
60
+ - Google AI API 사용량에 따라 제한이 있을 수 있습니다
README_backup.md ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 2인 대화 STT 처리기 (AI 화자 분리)
2
+
3
+ Whisper STT + Gemini AI를 결합하여 오디오 파일을 텍스트로 변환하고 화자별로 자동 분리하는 애플리케이션입니다.
4
+
5
+ ## 주요 기능
6
+
7
+ 1. **WAV 파일 자동 감지**: data 폴더의 모든 WAV 파일을 자동으로 감지
8
+ 2. **고정밀 음성 인식**: OpenAI Whisper를 사용한 음성-텍스트 변환
9
+ 3. **AI 화자 분리**: Google Gemini를 사용한 텍스트 기반 2인 대화 분리
10
+ 4. **결과 저장**:
11
+ - 전체 대화 (원본 + 화자 분리)
12
+ - 화자별 개별 대화
13
+ - JSON 형태 상세 데이터
14
+
15
+ ## 기술 스택
16
+
17
+ - **UI Framework**: tkinter (Python GUI)
18
+ - **음성 인식**: OpenAI Whisper
19
+ - **AI 화자 분리**: Google Gemini Pro
20
+ - **로깅**: Python logging
21
+
22
+ ## 폴더 구조
23
+
24
+ ```
25
+ sttUsingAPI/
26
+ ├── data/ # WAV 파일을 여기에 넣으세요
27
+ ├── output/ # 처리 결과가 저장됩니다
28
+ ├── logs/ # 로그 파일이 저장됩니다
29
+ ├── .env # API 키 설정
30
+ ├── audio_summarizer.py # 메인 애플리케이션
31
+ └── test_stt.py # 테스트 스크립트
32
+ ```
33
+
34
+ ## 설치 및 설정
35
+
36
+ ### 1. 의존성 설치
37
+
38
+ ```bash
39
+ pip install torch torchaudio python-dotenv google-generativeai
40
+ pip install git+https://github.com/openai/whisper.git
41
+ ```
42
+
43
+ ### 2. API 키 설정
44
+
45
+ `.env` 파일에 Google AI API 키를 설정하세요:
46
+
47
+ ```env
48
+ # Google AI API 키 (https://aistudio.google.com/app/apikey)
49
+ GOOGLE_API_KEY=your_google_api_key_here
50
+ ```
51
+
52
+ ### 3. WAV 파일 준비
53
+
54
+ 처리할 WAV 파일을 `data/` 폴더에 넣으세요.
55
+
56
+ ### 4. 실행
57
+
58
+ ```bash
59
+ # GUI 애플리케이션
60
+ python audio_summarizer.py
61
+
62
+ # 또는 테스트 스크립트
63
+ python test_stt.py
64
+ ```
65
+
66
+ ## 사용 방법
67
+
68
+ 1. 애플리케이션 실행
69
+ 2. `.env` 파일에 Google API 키 설정 확인
70
+ 3. WAV 파일이 `data/` 폴더에 있는지 확인
71
+ 4. "파일 목록 새로고침" 버튼으로 파일 목록 업데이트
72
+ 5. 개별 파일 처리: 파일 선택 후 "선택된 파일 처리" 클릭
73
+ 6. 전체 파일 처리: "모든 파일 처리" 클릭
74
+ 7. `output/` 폴더에서 결과 확인
75
+
76
+ ## 처리 과정
77
+
78
+ 1. **음성 인식**: Whisper가 WAV 파일을 텍스트로 변환
79
+ 2. **화자 분리**: Gemini가 텍스트 분석으로 화자별 발언 구분
80
+ 3. **결과 저장**: 다양한 형태로 결과 파일 생성
81
+
82
+ ## 출력 파일 형식
83
+
84
+ 각 WAV 파일에 대해 다음 파일들이 생성됩니다:
85
+
86
+ - `{파일명}_전체대화_{타임스탬프}.txt`: 원본 + 화자 분리 결과
87
+ - `{파일명}_화자1_{타임스탬프}.txt`: 화자1의 발언만
88
+ - `{파일명}_화자2_{타임스탬프}.txt`: 화자2의 발언만
89
+ - `{파일명}_data_{타임스탬프}.json`: JSON 형태 상세 데이터
90
+
91
+ ### 화자 분리 결과 예시
92
+
93
+ ```
94
+ 원본 텍스트:
95
+ 안녕하세요, 오늘 회의에 참석해주셔서 감사합니다. 네, 안녕하세요. 준비된 자료가 있나요? 네, 프레젠테이션 자료를 준비했습니다.
96
+
97
+ 화자별 분리 결과:
98
+ [화자1] 안녕하세요, 오늘 회의에 참석해주셔서 감사합니다.
99
+ [화자2] 네, 안녕하세요. 준비된 자료가 있나요?
100
+ [화자1] 네, 프레젠테이션 자료를 준비했습니다.
101
+ ```
102
+
103
+ ## API 키 발급
104
+
105
+ ### Google AI API 키
106
+ 1. [Google AI Studio](https://aistudio.google.com/app/apikey) 방문
107
+ 2. 구글 계정으로 로그인
108
+ 3. "Create API Key" 클릭
109
+ 4. 생성된 키를 `.env` 파일에 추가
110
+
111
+ ## 로그
112
+
113
+ - 모든 처리 과정과 오류는 `logs/stt_processor.log` 파일에 기록됩니다.
114
+
115
+ ## 화자 분리 정확도
116
+
117
+ Gemini AI의 텍스트 기반 화자 분리는 다음 요소들을 분석합니다:
118
+
119
+ - **대화 맥락**: 질문과 답변의 패턴
120
+ - **말투 변화**: 존댓말/반말, 어조 변화
121
+ - **주제 전환**: 화자별 관심사나 역할
122
+ - **언어 패턴**: 개인별 표현 습관
123
+
124
+ ## 주의사항
125
+
126
+ - WAV 형식의 오디오 파일만 지원됩니다.
127
+ - 최초 실행 시 Whisper 모델 다운로드로 인해 시간이 소요될 수 있습니다.
128
+ - 인터넷 연결이 필요합니다 (모델 다운로드 및 API 호출).
129
+ - Google AI API 사용량에 따라 비용이 발생할 수 있습니다.
130
+ - 2인 대화에 최적화되어 있습니다.
131
+
132
+ ## 화자 분리 정확도 향상 팁
133
+
134
+ 1. **명확한 역할 구분**: 인터뷰어-인터뷰이, 강사-학생 등
135
+ 2. **대화 흐름**: 자연스러운 질문과 답변 형태
136
+ 3. **말투 차이**: 존댓말/반말, 전문용어 사용 차이
137
+ 4. **음질**: 깨끗하고 명확한 음성
138
+ 5. **화자 간 중복 발언 최소화**: 동시에 말하는 구간 최소화
139
+
140
+ ## 문제 해결
141
+
142
+ ### API 키 오류
143
+ - `.env` 파일에 올바른 Google AI API 키가 설정되어 있는지 확인
144
+ - API 키의 권한 및 할당량 확인
145
+
146
+ ### 화자 분리 정확도 문제
147
+ - 대화 내용이 너무 짧거나 단조로운 경우 정확도 저하 가능
148
+ - 두 명 이상의 화자가 있는 경우 부정확할 수 있음
149
+
150
+ ### 모델 로딩 오류
151
+ - 인터넷 ���결 상태 확인
152
+ - 가상환경 및 패키지 설치 상태 확인
app.py ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import os
3
+ import logging
4
+ from datetime import datetime
5
+ from stt_processor import TextProcessor
6
+
7
+ # 로깅 설정
8
+ logging.basicConfig(
9
+ level=logging.INFO,
10
+ format='%(asctime)s - %(levelname)s - %(message)s',
11
+ handlers=[
12
+ logging.StreamHandler()
13
+ ]
14
+ )
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # 전역 변수
18
+ text_processor = None
19
+
20
+ def initialize_processor():
21
+ """텍스트 프로세서를 초기화합니다."""
22
+ global text_processor
23
+ try:
24
+ # 환경 변수 또는 Hugging Face Secrets에서 API 키 읽기
25
+ google_api_key = os.getenv("GOOGLE_API_KEY")
26
+
27
+ if not google_api_key:
28
+ return False, "❌ Google API 키가 설정되지 않았습니다. Hugging Face Spaces의 Settings에서 GOOGLE_API_KEY를 설정해주세요."
29
+
30
+ text_processor = TextProcessor(google_api_key)
31
+ return True, "✅ 텍스트 프로세서가 초기화되었습니다."
32
+
33
+ except Exception as e:
34
+ logger.error(f"텍스트 프로세서 초기화 실패: {e}")
35
+ return False, f"❌ 초기화 실패: {str(e)}"
36
+
37
+ def process_text_input(input_text, progress=gr.Progress()):
38
+ """
39
+ 입력된 텍스트를 처리합니다.
40
+
41
+ Args:
42
+ input_text: 처리할 텍스트
43
+ progress: Gradio 진행률 객체
44
+
45
+ Returns:
46
+ tuple: (처리 상태, 원본 텍스트, 화자 분리 결과, 교정 결과, 화자1 대화, 화자2 대화)
47
+ """
48
+ global text_processor
49
+
50
+ if not input_text or not input_text.strip():
51
+ return "❌ 처리할 텍스트를 입력해주세요.", "", "", "", "", ""
52
+
53
+ try:
54
+ # 텍스트 프로세서 초기화 (필요한 경우)
55
+ if text_processor is None:
56
+ progress(0.1, desc="텍스트 프로세서 초기화 중...")
57
+ success, message = initialize_processor()
58
+ if not success:
59
+ return message, "", "", "", "", ""
60
+
61
+ # 모델 로딩
62
+ progress(0.2, desc="AI 모델 로딩 중...")
63
+ if not text_processor.models_loaded:
64
+ text_processor.load_models()
65
+
66
+ # 진행 상황 콜백 함수
67
+ def progress_callback(status, current, total):
68
+ progress_value = 0.2 + (current / total) * 0.7 # 0.2~0.9 범위
69
+ progress(progress_value, desc=f"{status} ({current}/{total})")
70
+
71
+ # 텍스트 처리
72
+ progress(0.3, desc="텍스트 처리 시작...")
73
+ result = text_processor.process_text(input_text, progress_callback=progress_callback)
74
+
75
+ if not result.get("success", False):
76
+ return f"❌ 처리 실패: {result.get('error', 'Unknown error')}", "", "", "", "", ""
77
+
78
+ # 결과 추출
79
+ progress(0.95, desc="결과 정리 중...")
80
+ original_text = result["original_text"]
81
+ separated_text = result["separated_text"]
82
+ corrected_text = result["corrected_text"]
83
+
84
+ # 화자별 대화 추출
85
+ conversations = result["conversations_by_speaker_corrected"]
86
+ speaker1_text = "\n\n".join([f"{i+1}. {utterance}" for i, utterance in enumerate(conversations.get("화자1", []))])
87
+ speaker2_text = "\n\n".join([f"{i+1}. {utterance}" for i, utterance in enumerate(conversations.get("화자2", []))])
88
+
89
+ progress(1.0, desc="처리 완료!")
90
+
91
+ status_message = f"""
92
+ ✅ **처리 완료!**
93
+ - 텍스트명: {result['text_name']}
94
+ - 처리 시간: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
95
+ - 화자1 발언 수: {len(conversations.get('화자1', []))}개
96
+ - 화자2 발언 수: {len(conversations.get('화자2', []))}개
97
+ """
98
+
99
+ return status_message, original_text, separated_text, corrected_text, speaker1_text, speaker2_text
100
+
101
+ except Exception as e:
102
+ logger.error(f"텍스트 처리 중 오류: {e}")
103
+ return f"❌ 처리 중 오류가 발생했습니다: {str(e)}", "", "", "", "", ""
104
+
105
+ def create_interface():
106
+ """Gradio 인터페이스를 생성합니다."""
107
+
108
+ # CSS 스타일링
109
+ css = """
110
+ .gradio-container {
111
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
112
+ }
113
+ .status-box {
114
+ padding: 15px;
115
+ border-radius: 8px;
116
+ margin: 10px 0;
117
+ }
118
+ .main-header {
119
+ text-align: center;
120
+ color: #2c3e50;
121
+ margin-bottom: 20px;
122
+ }
123
+ """
124
+
125
+ with gr.Blocks(css=css, title="2인 대화 STT 처리기") as interface:
126
+
127
+ # 헤더
128
+ gr.HTML("""
129
+ <div class="main-header">
130
+ <h1>💬 2인 대화 화자 분리기 (AI)</h1>
131
+ <p>Gemini 2.0 Flash AI를 사용한 텍스트 화자 분리 및 맞춤법 교정</p>
132
+ </div>
133
+ """)
134
+
135
+ with gr.Row():
136
+ with gr.Column(scale=1):
137
+ # 텍스트 입력 섹션
138
+ gr.Markdown("### 📝 텍스트 입력")
139
+ text_input = gr.Textbox(
140
+ label="2인 대화 텍스트를 입력하세요",
141
+ placeholder="두 명이 나누는 대화 내용을 여기에 붙여넣기하세요...\n\n예시:\n안녕하세요, 오늘 회의에 참석해주셔서 감사합니다. 네, 안녕하세요. 준비된 자료가 있나요? 네, 프레젠테이션 자료를 준비했습니다.",
142
+ lines=8,
143
+ max_lines=15
144
+ )
145
+
146
+ process_btn = gr.Button(
147
+ "🚀 처리 시작",
148
+ variant="primary",
149
+ size="lg"
150
+ )
151
+
152
+ # 상태 표시
153
+ status_output = gr.Markdown(
154
+ "### 📊 처리 상태\n준비 완료. 2인 대화 텍스트를 입력하고 '처리 시작' 버튼을 클릭하세요.",
155
+ elem_classes=["status-box"]
156
+ )
157
+
158
+ with gr.Column(scale=2):
159
+ # 결과 표시 섹션
160
+ with gr.Tabs():
161
+ with gr.TabItem("📝 원본 텍스트"):
162
+ original_output = gr.Textbox(
163
+ label="입력된 원본 텍스트",
164
+ lines=10,
165
+ max_lines=20,
166
+ placeholder="처리 후 원본 텍스트가 여기에 표시됩니다..."
167
+ )
168
+
169
+ with gr.TabItem("👥 화자 분리 (원본)"):
170
+ separated_output = gr.Textbox(
171
+ label="AI 화자 분리 결과 (원본)",
172
+ lines=10,
173
+ max_lines=20,
174
+ placeholder="처리 후 화자별로 분리된 대화가 여기에 표시됩니다..."
175
+ )
176
+
177
+ with gr.TabItem("✏️ 화자 분리 (교정)"):
178
+ corrected_output = gr.Textbox(
179
+ label="AI 화자 분리 결과 (맞춤법 교정)",
180
+ lines=10,
181
+ max_lines=20,
182
+ placeholder="처리 후 맞춤법이 교정된 화자 분리 결과가 여기에 표시됩니다..."
183
+ )
184
+
185
+ with gr.TabItem("👤 화자1 대화"):
186
+ speaker1_output = gr.Textbox(
187
+ label="화자1의 모든 발언",
188
+ lines=10,
189
+ max_lines=20,
190
+ placeholder="처리 후 화자1의 발언들이 여기에 표시됩니다..."
191
+ )
192
+
193
+ with gr.TabItem("👤 화자2 대화"):
194
+ speaker2_output = gr.Textbox(
195
+ label="화자2의 모든 발언",
196
+ lines=10,
197
+ max_lines=20,
198
+ placeholder="처리 후 화자2의 발언들이 여기에 표시됩니다..."
199
+ )
200
+
201
+ # 사용법 안내
202
+ gr.Markdown("""
203
+ ### 📖 사용법
204
+ 1. **텍스트 입력**: 2인 대화 텍스트를 입력란에 붙여넣기하세요
205
+ 2. **처리 시작**: '🚀 처리 시작' 버튼을 클릭하여 화자 분리를 시작하세요
206
+ 3. **결과 확인**: 각 탭에서 원본 텍스트, 화자 분리 결과, 개별 화자 대화를 확인하세요
207
+
208
+ ### ⚙️ 기술 정보
209
+ - **화자 분리**: Google Gemini 2.0 Flash
210
+ - **맞춤법 교정**: 고급 AI 기반 한국어 교정
211
+ - **지원 언어**: 한국어 최적화
212
+ - **최적 환경**: 2인 대화, 명확한 문맥
213
+
214
+ ### ⚠️ 주의사항
215
+ - 처리 시간은 텍스트 길이에 따라 달라집니다 (보통 30초-2분)
216
+ - Google AI API 사용량 제한이 있을 수 있습니다
217
+ - 2인 대화에 최적화되어 있습니다
218
+ - 대화 맥락이 명확할수록 분리 정확도가 높아집니다
219
+ """)
220
+
221
+ # 이벤트 연결
222
+ process_btn.click(
223
+ fn=process_text_input,
224
+ inputs=[text_input],
225
+ outputs=[
226
+ status_output,
227
+ original_output,
228
+ separated_output,
229
+ corrected_output,
230
+ speaker1_output,
231
+ speaker2_output
232
+ ],
233
+ show_progress=True
234
+ )
235
+
236
+ return interface
237
+
238
+ # 메인 실행
239
+ if __name__ == "__main__":
240
+ logger.info("Gradio 앱을 시작합니다...")
241
+
242
+ # 인터페이스 생성
243
+ app = create_interface()
244
+
245
+ # 앱 실행
246
+ app.launch(
247
+ server_name="0.0.0.0",
248
+ server_port=7860,
249
+ share=True,
250
+ show_error=True
251
+ )
audio_summarizer.py ADDED
@@ -0,0 +1,491 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import tkinter as tk
2
+ from tkinter import scrolledtext, messagebox, ttk
3
+ import threading
4
+ import os
5
+ import torch
6
+ import whisper
7
+ import google.generativeai as genai
8
+ from dotenv import load_dotenv
9
+ import logging
10
+ import json
11
+ from datetime import datetime
12
+ import glob
13
+ import re
14
+
15
+ # 환경 변수 로드
16
+ load_dotenv()
17
+
18
+ # --- 설정: .env 파일에서 API 키를 읽어옵니다 ---
19
+ GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
20
+
21
+ # logs 폴더 생성
22
+ if not os.path.exists("logs"):
23
+ os.makedirs("logs")
24
+
25
+ # output 폴더 생성
26
+ if not os.path.exists("output"):
27
+ os.makedirs("output")
28
+
29
+ # data 폴더 생성
30
+ if not os.path.exists("data"):
31
+ os.makedirs("data")
32
+
33
+ # 로깅 설정
34
+ logging.basicConfig(
35
+ level=logging.INFO,
36
+ format='%(asctime)s - %(levelname)s - %(message)s',
37
+ handlers=[
38
+ logging.FileHandler('logs/stt_processor.log', encoding='utf-8'),
39
+ logging.StreamHandler()
40
+ ]
41
+ )
42
+ logger = logging.getLogger(__name__)
43
+
44
+ # -----------------------------------------
45
+
46
+ class STTProcessorApp:
47
+ def __init__(self, root):
48
+ self.root = root
49
+ self.root.title("2인 대화 STT 처리기 (AI 화자 분리)")
50
+ self.root.geometry("1000x750")
51
+
52
+ # 모델 로딩 상태 변수
53
+ self.models_loaded = False
54
+ self.whisper_model = None
55
+ self.gemini_model = None
56
+
57
+ # UI 요소 생성
58
+ self.main_frame = tk.Frame(root, padx=10, pady=10)
59
+ self.main_frame.pack(fill=tk.BOTH, expand=True)
60
+
61
+ # 제목
62
+ title_label = tk.Label(self.main_frame, text="2인 대화 STT 처리기 (AI 화자 분리)", font=("Arial", 16, "bold"))
63
+ title_label.pack(pady=5)
64
+
65
+ # 설명
66
+ desc_label = tk.Label(self.main_frame, text="Whisper STT + Gemini AI 화자 분리로 2명의 대화를 자동으로 구분합니다", font=("Arial", 10))
67
+ desc_label.pack(pady=2)
68
+
69
+ # WAV 파일 목록 프레임
70
+ files_frame = tk.LabelFrame(self.main_frame, text="data 폴더의 WAV 파일 목록", padx=5, pady=5)
71
+ files_frame.pack(fill=tk.BOTH, expand=True, pady=5)
72
+
73
+ # 파일 목록과 스크롤바
74
+ list_frame = tk.Frame(files_frame)
75
+ list_frame.pack(fill=tk.BOTH, expand=True)
76
+
77
+ scrollbar = tk.Scrollbar(list_frame)
78
+ scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
79
+
80
+ self.file_listbox = tk.Listbox(list_frame, yscrollcommand=scrollbar.set, selectmode=tk.SINGLE)
81
+ self.file_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
82
+ scrollbar.config(command=self.file_listbox.yview)
83
+
84
+ # 버튼 프레임
85
+ button_frame = tk.Frame(self.main_frame)
86
+ button_frame.pack(fill=tk.X, pady=5)
87
+
88
+ self.refresh_button = tk.Button(button_frame, text="파일 목록 새로고침", command=self.refresh_file_list)
89
+ self.refresh_button.pack(side=tk.LEFT, padx=5)
90
+
91
+ self.process_button = tk.Button(button_frame, text="선택된 파일 처리", command=self.start_processing,
92
+ state=tk.DISABLED)
93
+ self.process_button.pack(side=tk.LEFT, padx=5)
94
+
95
+ self.process_all_button = tk.Button(button_frame, text="모든 파일 처리", command=self.start_processing_all,
96
+ state=tk.DISABLED)
97
+ self.process_all_button.pack(side=tk.LEFT, padx=5)
98
+
99
+ # 진행률 표시
100
+ progress_frame = tk.Frame(self.main_frame)
101
+ progress_frame.pack(fill=tk.X, pady=5)
102
+
103
+ tk.Label(progress_frame, text="진행률:").pack(side=tk.LEFT)
104
+ self.progress_var = tk.StringVar(value="대기 중")
105
+ tk.Label(progress_frame, textvariable=self.progress_var).pack(side=tk.LEFT, padx=10)
106
+
107
+ self.progress_bar = ttk.Progressbar(progress_frame, mode='determinate')
108
+ self.progress_bar.pack(side=tk.RIGHT, fill=tk.X, expand=True, padx=10)
109
+
110
+ # 상태 표시줄
111
+ self.status_label = tk.Label(self.main_frame, text="준비 완료. Google API 키를 설정하고 '처리' 버튼을 누르세요.", bd=1,
112
+ relief=tk.SUNKEN, anchor=tk.W)
113
+ self.status_label.pack(side=tk.BOTTOM, fill=tk.X)
114
+
115
+ # 결과 출력 영역
116
+ result_frame = tk.LabelFrame(self.main_frame, text="처리 결과", padx=5, pady=5)
117
+ result_frame.pack(fill=tk.BOTH, expand=True, pady=5)
118
+
119
+ self.result_text = scrolledtext.ScrolledText(result_frame, wrap=tk.WORD, state=tk.DISABLED, height=15)
120
+ self.result_text.pack(fill=tk.BOTH, expand=True)
121
+
122
+ # 초기 파일 목록 로드
123
+ self.refresh_file_list()
124
+
125
+ def refresh_file_list(self):
126
+ """data 폴더의 WAV 파일 목록을 새로고침합니다."""
127
+ self.file_listbox.delete(0, tk.END)
128
+
129
+ wav_files = glob.glob("data/*.wav")
130
+ if wav_files:
131
+ for file_path in wav_files:
132
+ filename = os.path.basename(file_path)
133
+ self.file_listbox.insert(tk.END, filename)
134
+ self.process_button.config(state=tk.NORMAL)
135
+ self.process_all_button.config(state=tk.NORMAL)
136
+ logger.info(f"{len(wav_files)}개의 WAV 파일을 발견했습니다.")
137
+ else:
138
+ self.file_listbox.insert(tk.END, "WAV 파일이 없습니다. data 폴더에 WAV 파일을 넣어주세요.")
139
+ self.process_button.config(state=tk.DISABLED)
140
+ self.process_all_button.config(state=tk.DISABLED)
141
+ logger.warning("data 폴더에 WAV 파일이 없습니다.")
142
+
143
+ def update_status(self, message):
144
+ """UI의 상태 메시지를 업데이트합니다."""
145
+ self.status_label.config(text=message)
146
+ self.root.update_idletasks()
147
+
148
+ def update_progress(self, current, total, message=""):
149
+ """진행률을 업데이트합니다."""
150
+ if total > 0:
151
+ progress = (current / total) * 100
152
+ self.progress_bar.config(value=progress)
153
+ if message:
154
+ self.progress_var.set(f"{message} ({current}/{total})")
155
+ else:
156
+ self.progress_var.set(f"{current}/{total}")
157
+ self.root.update_idletasks()
158
+
159
+ def show_result(self, content):
160
+ """결과 텍스트 영역에 내용을 표시합니다."""
161
+ self.result_text.config(state=tk.NORMAL)
162
+ self.result_text.insert(tk.END, content + "\n\n")
163
+ self.result_text.see(tk.END)
164
+ self.result_text.config(state=tk.DISABLED)
165
+
166
+ def load_models(self):
167
+ """필요한 AI 모델들을 로드합니다."""
168
+ try:
169
+ if not GOOGLE_API_KEY or GOOGLE_API_KEY == "your_google_api_key_here":
170
+ messagebox.showerror("API 키 오류", ".env 파일에 올바른 Google AI API 키를 입력해주세요.")
171
+ logger.error("Google API 키가 설정되지 않았습니다.")
172
+ return False
173
+
174
+ logger.info("모델 로딩을 시작합니다.")
175
+ self.update_status("모델 로딩 중... (최초 실행 시 시간이 걸릴 수 있습니다)")
176
+
177
+ # Whisper 모델 로딩
178
+ self.update_status("음성 인식 모델(Whisper) 로딩 중...")
179
+ logger.info("Whisper 모델 로딩을 시작합니다.")
180
+ self.whisper_model = whisper.load_model("base") # "small", "medium", "large" 등으로 변경 가능
181
+ logger.info("Whisper 모델 로딩이 완료되었습니다.")
182
+
183
+ # Gemini 모델 설정
184
+ self.update_status("AI 화자 분리 모델(Gemini) 설정 중...")
185
+ logger.info("Gemini 모델 설정을 시작합니다.")
186
+ genai.configure(api_key=GOOGLE_API_KEY)
187
+ # gemini-2.0-flash: 최신 Gemini 2.0 모델, 빠르고 정확한 처리
188
+ self.gemini_model = genai.GenerativeModel('gemini-2.0-flash')
189
+ logger.info("Gemini 2.0 Flash 모델 설정이 완료되었습니다.")
190
+
191
+ self.models_loaded = True
192
+ self.update_status("모든 모델 로딩 완료. 처리 준비 완료.")
193
+ logger.info("모든 모델 로딩이 완료되었습니다.")
194
+ return True
195
+ except Exception as e:
196
+ error_msg = f"모델을 로딩하는 중 오류가 발생했습니다: {e}"
197
+ messagebox.showerror("모델 로딩 오류", error_msg)
198
+ logger.error(error_msg)
199
+ self.update_status("오류: 모델 로딩 실패")
200
+ return False
201
+
202
+ def start_processing(self):
203
+ """선택된 파일 처리 시작."""
204
+ selection = self.file_listbox.curselection()
205
+ if not selection:
206
+ messagebox.showwarning("파일 미선택", "처리할 파일을 선택해주세요.")
207
+ return
208
+
209
+ filename = self.file_listbox.get(selection[0])
210
+ if filename == "WAV 파일이 없습니다. data 폴더에 WAV 파일을 넣어주세요.":
211
+ return
212
+
213
+ self.process_files([filename])
214
+
215
+ def start_processing_all(self):
216
+ """모든 파일 처리 시작."""
217
+ wav_files = glob.glob("data/*.wav")
218
+ if not wav_files:
219
+ messagebox.showwarning("파일 없음", "data 폴더에 처리할 WAV 파일이 없습니다.")
220
+ return
221
+
222
+ filenames = [os.path.basename(f) for f in wav_files]
223
+ self.process_files(filenames)
224
+
225
+ def process_files(self, filenames):
226
+ """파일 처리 시작."""
227
+ # 모델이 로드되지 않았으면 먼저 로드
228
+ if not self.models_loaded:
229
+ if not self.load_models():
230
+ return # 모델 로딩 실패 시 중단
231
+
232
+ # UI 비활성화 및 처리 스레드 시작
233
+ self.refresh_button.config(state=tk.DISABLED)
234
+ self.process_button.config(state=tk.DISABLED)
235
+ self.process_all_button.config(state=tk.DISABLED)
236
+
237
+ processing_thread = threading.Thread(target=self.process_audio_files, args=(filenames,))
238
+ processing_thread.start()
239
+
240
+ def process_audio_files(self, filenames):
241
+ """백그라운드에서 여러 오디오 파일을 처리하는 메인 로직."""
242
+ try:
243
+ total_files = len(filenames)
244
+ logger.info(f"{total_files}개의 파일 처리를 시작합니다.")
245
+
246
+ for idx, filename in enumerate(filenames):
247
+ file_path = os.path.join("data", filename)
248
+ self.update_progress(idx, total_files, f"처리 중: {filename}")
249
+
250
+ result = self.process_single_audio_file(file_path, filename)
251
+ if result:
252
+ self.show_result(f"✅ {filename} 처리 완료")
253
+ else:
254
+ self.show_result(f"❌ {filename} 처리 실패")
255
+
256
+ self.update_progress(total_files, total_files, "완료")
257
+ self.update_status("모든 파일 처리 완료!")
258
+ logger.info("모든 파일 처리가 완료되었습니다.")
259
+
260
+ except Exception as e:
261
+ error_msg = f"파일 처리 중 오류가 발생했습니다: {e}"
262
+ logger.error(error_msg)
263
+ self.update_status(f"오류: {e}")
264
+ finally:
265
+ # UI 다시 활성화
266
+ self.refresh_button.config(state=tk.NORMAL)
267
+ self.process_button.config(state=tk.NORMAL)
268
+ self.process_all_button.config(state=tk.NORMAL)
269
+
270
+ def process_single_audio_file(self, file_path, filename):
271
+ """단일 오디오 파일을 처리합니다."""
272
+ try:
273
+ logger.info(f"파일 처리 시작: {file_path}")
274
+ base_name = os.path.splitext(filename)[0]
275
+
276
+ # 1단계: Whisper로 음성 인식
277
+ self.update_status(f"1/4: 음성 인식 진행 중: {filename}")
278
+ logger.info(f"음성 인식 시작: {filename}")
279
+
280
+ result = self.whisper_model.transcribe(file_path)
281
+ full_text = result['text'].strip()
282
+
283
+ if not full_text:
284
+ logger.warning(f"파일 {filename}에서 텍스트를 추출할 수 없습니다.")
285
+ return False
286
+
287
+ # 2단계: Gemini로 화자 분리
288
+ self.update_status(f"2/4: AI 화자 분리 진행 중: {filename}")
289
+ logger.info(f"AI 화자 분리 시작: {filename}")
290
+
291
+ speaker_separated_text = self.separate_speakers_with_gemini(full_text)
292
+
293
+ # 3단계: 맞춤법 교정
294
+ self.update_status(f"3/4: 맞춤법 교정 진행 중: {filename}")
295
+ logger.info(f"맞춤법 교정 시작: {filename}")
296
+
297
+ corrected_text = self.correct_spelling_with_gemini(speaker_separated_text)
298
+
299
+ # 4단계: 결과 저장
300
+ self.update_status(f"4/4: 결과 저장 중: {filename}")
301
+ self.save_separated_conversations(base_name, full_text, speaker_separated_text, corrected_text, result)
302
+
303
+ logger.info(f"파일 처리 완료: {filename}")
304
+ return True
305
+
306
+ except Exception as e:
307
+ logger.error(f"파일 {filename} 처리 중 오류: {e}")
308
+ return False
309
+
310
+ def separate_speakers_with_gemini(self, text):
311
+ """Gemini API를 사용하여 텍스트를 화자별로 분리합니다."""
312
+ try:
313
+ prompt = f"""
314
+ 당신은 2명의 화자가 나누는 대화를 분석하는 전문가입니다.
315
+ 주어진 텍스트를 분석하여 각 발언을 화자별로 구분해주세요.
316
+
317
+ 분석 지침:
318
+ 1. 대화의 맥락과 내용을 기반으로 화자를 구분하세요
319
+ 2. 말투, 주제 전환, 질문과 답변의 패턴을 활용하세요
320
+ 3. 화자1과 화자2로 구분하여 표시하세요
321
+ 4. 각 발언 앞에 [화자1] 또는 [화자2]를 붙여주세요
322
+ 5. 발언이 너무 길 경우 자연스러운 지점에서 나누어주세요
323
+
324
+ 출력 형식:
325
+ [화자1] 첫 번째 발언 내용
326
+ [화자2] 두 번째 발언 내용
327
+ [화자1] 세 번째 발언 내용
328
+ ...
329
+
330
+ 분석할 텍스트:
331
+ {text}
332
+ """
333
+
334
+ response = self.gemini_model.generate_content(prompt)
335
+ separated_text = response.text.strip()
336
+
337
+ logger.info("Gemini를 통한 화자 분리가 완료되었습니다.")
338
+ return separated_text
339
+
340
+ except Exception as e:
341
+ logger.error(f"Gemini 화자 분리 중 오류: {e}")
342
+ return f"[오류] 화자 분리 실패: {str(e)}"
343
+
344
+ def correct_spelling_with_gemini(self, separated_text):
345
+ """Gemini API를 사용하여 화자별 분리된 텍스트의 맞춤법을 교정합니다."""
346
+ try:
347
+ prompt = f"""
348
+ 당신은 한국어 맞춤법 교정 전문가입니다.
349
+ 주어진 텍스트에서 맞춤법 오류, 띄어쓰기 오류, 오타를 수정해주세요.
350
+
351
+ 교정 지침:
352
+ 1. 자연스러운 한국어 표현으로 수정하되, 원본의 의미와 말투는 유지하세요
353
+ 2. [화자1], [화자2] 태그는 그대로 유지하세요
354
+ 3. 전문 용어나 고유명사는 가능한 정확하게 수정하세요
355
+ 4. 구어체 특성은 유���하되, 명백한 오타만 수정하세요
356
+ 5. 문맥에 맞는 올바른 단어로 교체하세요
357
+
358
+ 수정이 필요한 예시:
359
+ - "치특기" → "치트키"
360
+ - "실점픽" → "실전 픽"
361
+ - "복사부천억" → "복사 붙여넣기"
362
+ - "핵심같이가" → "핵심 가치가"
363
+ - "재활" → "재활용"
364
+ - "저정할" → "저장할"
365
+ - "플레일" → "플레어"
366
+ - "서벌 수" → "서버리스"
367
+ - "커리" → "쿼리"
368
+ - "전력" → "전략"
369
+ - "클라클라" → "클라크"
370
+ - "가인만" → "가입만"
371
+ - "M5U" → "MAU"
372
+ - "나온 로도" → "다운로드"
373
+ - "무시무치" → "무시무시"
374
+ - "송신유금" → "송신 요금"
375
+ - "10지가" → "10GB"
376
+ - "유금" → "요금"
377
+ - "전 색을" → "전 세계"
378
+ - "도무원은" → "도구들은"
379
+ - "골차품데" → "골치 아픈데"
380
+ - "변원해" → "변환해"
381
+ - "f 운영" → "서비스 운영"
382
+ - "오류추저개" → "오류 추적기"
383
+ - "f 늘려질" → "서비스가 늘어날"
384
+ - "캐시칭" → "캐싱"
385
+ - "플레이어" → "플레어"
386
+ - "업스테시" → "업스태시"
387
+ - "원시근을" → "웬지슨"
388
+ - "부각이릉도" → "부각들도"
389
+ - "컴포넌트" → "컴포넌트"
390
+ - "본이터링" → "모니터링"
391
+ - "번뜨기는" → "번뜩이는"
392
+ - "사용적 경험" → "사용자 경험"
393
+
394
+ 교정할 텍스트:
395
+ {separated_text}
396
+ """
397
+
398
+ response = self.gemini_model.generate_content(prompt)
399
+ corrected_text = response.text.strip()
400
+
401
+ logger.info("Gemini를 통한 맞춤법 교정이 완료되었습니다.")
402
+ return corrected_text
403
+
404
+ except Exception as e:
405
+ logger.error(f"Gemini 맞춤법 교정 중 오류: {e}")
406
+ return separated_text # 오류 발생 시 원본 반환
407
+
408
+ def parse_separated_text(self, separated_text):
409
+ """화자별로 분리된 텍스트를 파싱하여 구조화합니다."""
410
+ conversations = {
411
+ "화자1": [],
412
+ "화자2": []
413
+ }
414
+
415
+ # 정규표현식으로 화자별 발언 추출
416
+ pattern = r'\[화자([12])\]\s*(.+?)(?=\[화자[12]\]|$)'
417
+ matches = re.findall(pattern, separated_text, re.DOTALL)
418
+
419
+ for speaker_num, content in matches:
420
+ speaker = f"화자{speaker_num}"
421
+ content = content.strip()
422
+ if content:
423
+ conversations[speaker].append(content)
424
+
425
+ return conversations
426
+
427
+ def save_separated_conversations(self, base_name, original_text, separated_text, corrected_text, whisper_result):
428
+ """화자별로 분리되고 맞춤법이 교정된 대화 내용을 파일로 저장합니다."""
429
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
430
+
431
+ # 교정된 텍스트에서 화자별 대화 파싱
432
+ corrected_conversations = self.parse_separated_text(corrected_text)
433
+
434
+ # 원본 화자별 대화 파싱 (비교용)
435
+ original_conversations = self.parse_separated_text(separated_text)
436
+
437
+ # 1. 전체 대화 저장 (원본, 화자 분리, 맞춤법 교정 포함)
438
+ all_txt_path = f"output/{base_name}_전체대화_{timestamp}.txt"
439
+ with open(all_txt_path, 'w', encoding='utf-8') as f:
440
+ f.write(f"파일명: {base_name}\n")
441
+ f.write(f"처리 시간: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
442
+ f.write(f"언어: {whisper_result.get('language', 'unknown')}\n")
443
+ f.write("="*50 + "\n\n")
444
+ f.write("원본 텍스트:\n")
445
+ f.write(original_text + "\n\n")
446
+ f.write("="*50 + "\n\n")
447
+ f.write("화자별 분리 결과 (원본):\n")
448
+ f.write(separated_text + "\n\n")
449
+ f.write("="*50 + "\n\n")
450
+ f.write("화자별 분리 결과 (맞춤법 교정):\n")
451
+ f.write(corrected_text + "\n")
452
+
453
+ # 2. 교정된 화자별 개별 파일 저장
454
+ for speaker, utterances in corrected_conversations.items():
455
+ if utterances:
456
+ speaker_txt_path = f"output/{base_name}_{speaker}_교정본_{timestamp}.txt"
457
+ with open(speaker_txt_path, 'w', encoding='utf-8') as f:
458
+ f.write(f"파일명: {base_name}\n")
459
+ f.write(f"화자: {speaker}\n")
460
+ f.write(f"처리 시간: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
461
+ f.write(f"발언 수: {len(utterances)}\n")
462
+ f.write("="*50 + "\n\n")
463
+
464
+ for idx, utterance in enumerate(utterances, 1):
465
+ f.write(f"{idx}. {utterance}\n\n")
466
+
467
+ # 3. JSON 형태로도 저장 (분석용)
468
+ json_path = f"output/{base_name}_data_{timestamp}.json"
469
+ json_data = {
470
+ "filename": base_name,
471
+ "processed_time": datetime.now().isoformat(),
472
+ "language": whisper_result.get("language", "unknown"),
473
+ "original_text": original_text,
474
+ "separated_text": separated_text,
475
+ "corrected_text": corrected_text,
476
+ "conversations_by_speaker_original": original_conversations,
477
+ "conversations_by_speaker_corrected": corrected_conversations,
478
+ "segments": whisper_result.get("segments", [])
479
+ }
480
+
481
+ with open(json_path, 'w', encoding='utf-8') as f:
482
+ json.dump(json_data, f, ensure_ascii=False, indent=2)
483
+
484
+ logger.info(f"결과 저장 완료: {all_txt_path}, {json_path}")
485
+ logger.info(f"교정된 화자별 파일도 저장되었습니다.")
486
+
487
+
488
+ if __name__ == "__main__":
489
+ root = tk.Tk()
490
+ app = STTProcessorApp(root)
491
+ root.mainloop()
check_models.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import google.generativeai as genai
2
+ from dotenv import load_dotenv
3
+ import os
4
+
5
+ def list_available_models():
6
+ """사용 가능한 Gemini 모델 목록을 확인합니다"""
7
+
8
+ load_dotenv()
9
+ GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
10
+
11
+ if not GOOGLE_API_KEY or GOOGLE_API_KEY == "your_google_api_key_here":
12
+ print("ERROR: Please set GOOGLE_API_KEY in .env file")
13
+ return
14
+
15
+ try:
16
+ genai.configure(api_key=GOOGLE_API_KEY)
17
+
18
+ print("Available Gemini models:")
19
+ print("=" * 50)
20
+
21
+ models = genai.list_models()
22
+ for model in models:
23
+ if 'generateContent' in model.supported_generation_methods:
24
+ print(f"[OK] {model.name}")
25
+ print(f" Display name: {model.display_name}")
26
+ print(f" Description: {model.description}")
27
+ print()
28
+
29
+ except Exception as e:
30
+ print(f"Error: {e}")
31
+
32
+ if __name__ == "__main__":
33
+ list_available_models()
data/.gitkeep ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # 이 파일은 data 폴더 구조를 유지하기 위한 파일입니다.
2
+ # WAV 파일을 이 폴더에 넣어주세요.
deployment_guide.md ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🚀 허깅페이스 Spaces 배포 가이드
2
+
3
+ ## 📋 배포 준비사항
4
+
5
+ ### 1. 필수 파일 확인
6
+ - `app.py` (메인 애플리케이션)
7
+ - `stt_processor.py` (STT 처리 모듈)
8
+ - `requirements.txt` (의존성)
9
+ - `README.md` (허깅페이스용 설명)
10
+
11
+ ### 2. Google AI API 키 준비
12
+ 1. [Google AI Studio](https://aistudio.google.com/app/apikey) 접속
13
+ 2. Google 계정으로 로그인
14
+ 3. "Create API Key" 클릭
15
+ 4. 생성된 API 키 복사 (나중에 Hugging Face에서 사용)
16
+
17
+ ## 🔧 허깅페이스 Spaces 배포 단계
18
+
19
+ ### 1단계: Hugging Face 계정 생성
20
+ 1. [Hugging Face](https://huggingface.co/) 접속
21
+ 2. 계정 생성 또는 로그인
22
+
23
+ ### 2단계: 새 Space 생성
24
+ 1. 프로필 페이지에서 "Spaces" 탭 클릭
25
+ 2. "Create new Space" 버튼 클릭
26
+ 3. 설정:
27
+ - **Space name**: `stt-speaker-separation` (또는 원하는 이름)
28
+ - **License**: MIT
29
+ - **SDK**: Gradio
30
+ - **Hardware**: CPU basic (무료)
31
+ - **Visibility**: Public
32
+
33
+ ### 3단계: 코드 업로드
34
+ 다음 방법 중 하나 선택:
35
+
36
+ #### 방법 A: 웹 인터페이스 사용
37
+ 1. Space 페이지에서 "Files" 탭 클릭
38
+ 2. "Upload files" 클릭
39
+ 3. 다음 파일들을 업로드:
40
+ - `app.py`
41
+ - `stt_processor.py`
42
+ - `requirements.txt`
43
+ - `README.md`
44
+
45
+ #### 방법 B: Git 사용
46
+ ```bash
47
+ # Space 복제
48
+ git clone https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME
49
+ cd YOUR_SPACE_NAME
50
+
51
+ # 파일 복사
52
+ copy app.py .
53
+ copy stt_processor.py .
54
+ copy requirements.txt .
55
+ copy README.md .
56
+
57
+ # 커밋 및 푸시
58
+ git add .
59
+ git commit -m "Initial upload of STT speaker separation app"
60
+ git push
61
+ ```
62
+
63
+ ### 4단계: 환경 변수 설정
64
+ 1. Space 페이지에서 "Settings" 탭 클릭
65
+ 2. "Repository secrets" 섹션 찾기
66
+ 3. "New secret" 클릭
67
+ 4. 다음 입력:
68
+ - **Name**: `GOOGLE_API_KEY`
69
+ - **Value**: 앞서 복사한 Google AI API 키
70
+ 5. "Add secret" 클릭
71
+
72
+ ### 5단계: 앱 빌드 및 실행 확인
73
+ 1. Space가 자동으로 빌드 시작됨
74
+ 2. 빌드 로그에서 오류 확인
75
+ 3. 빌드 완료 후 앱 인터페이스 확인
76
+
77
+ ## 🔍 빌드 로그 확인 포인트
78
+
79
+ ### 정상 빌드 시 나타나는 로그:
80
+ ```
81
+ Installing dependencies from requirements.txt...
82
+ ✓ torch
83
+ ✓ torchaudio
84
+ ✓ openai-whisper
85
+ ✓ google-generativeai
86
+ ✓ gradio
87
+ ✓ spaces
88
+ ```
89
+
90
+ ### 주의해야 할 오류:
91
+ - **ModuleNotFoundError**: requirements.txt 확인
92
+ - **API Key Error**: 환경 변수 설정 확인
93
+ - **CUDA/GPU 오류**: CPU 빌드 환경이므로 정상
94
+
95
+ ## 📊 성능 최적화
96
+
97
+ ### CPU 환경 최적화:
98
+ 1. Whisper 모델을 "base"로 유지 (더 작은 모델 사용)
99
+ 2. 배치 처리 대신 단일 파일 처리 사용
100
+ 3. 메모리 사용량 모니터링
101
+
102
+ ### 사용자 경험 개선:
103
+ 1. 파일 크기 제한 안내
104
+ 2. 처리 시간 예상 안내
105
+ 3. 에러 메시지 명확화
106
+
107
+ ## 🌐 배포 후 공유
108
+
109
+ ### Space URL:
110
+ `https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME`
111
+
112
+ ### 임베드 코드:
113
+ ```html
114
+ <iframe
115
+ src="https://your-username-your-space-name.hf.space"
116
+ frameborder="0"
117
+ width="850"
118
+ height="450"
119
+ ></iframe>
120
+ ```
121
+
122
+ ## 🛠 문제 해결
123
+
124
+ ### 자주 발생하는 문제:
125
+
126
+ #### 1. API 키 인식 불가
127
+ - Settings → Repository secrets에서 `GOOGLE_API_KEY` 확인
128
+ - 키에 특수문자나 공백이 없는지 확인
129
+
130
+ #### 2. 모델 로딩 시간 초과
131
+ - Whisper 모델 크기 조정 (`base` → `tiny`)
132
+ - 타임아웃 설정 증가
133
+
134
+ #### 3. 메모리 부족
135
+ - 동시 처리 요청 수 제한
136
+ - 파일 크기 제한 설정
137
+
138
+ #### 4. 빌드 실패
139
+ - requirements.txt의 패키지 버전 호환성 확인
140
+ - Python 버전 호환성 확인
141
+
142
+ ## 📞 지원
143
+
144
+ - [Hugging Face 포럼](https://discuss.huggingface.co/)
145
+ - [Gradio 문서](https://gradio.app/docs/)
146
+ - [OpenAI Whisper GitHub](https://github.com/openai/whisper)
147
+
148
+ ## 🎉 배포 완료!
149
+
150
+ 배포가 성공적으로 완료되면:
151
+ 1. 공개 URL을 통해 누구나 접근 가능
152
+ 2. 자동으로 SSL 인증서 적용
153
+ 3. CDN을 통한 전세계 접근 가능
154
+ 4. 사용량 통계 확인 가능
env_example.txt ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 환경 변수 설정 가이드
2
+
3
+ ## 로컬 개발 시 (.env 파일 생성)
4
+ GOOGLE_API_KEY=your_google_api_key_here
5
+
6
+ ## 허깅페이스 Spaces 배포 시
7
+ 1. Hugging Face Space 페이지 접속
8
+ 2. Settings 탭 클릭
9
+ 3. Repository secrets 섹션에서 추가:
10
+ - Name: GOOGLE_API_KEY
11
+ - Value: 실제 API 키 값
12
+
13
+ ## Google AI API 키 발급
14
+ 1. https://aistudio.google.com/app/apikey 접속
15
+ 2. Google 계정으로 로그인
16
+ 3. "Create API Key" 클릭
17
+ 4. 생성된 키를 복사하여 사용
output/.gitkeep ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # 이 파일은 output 폴더 구조를 유지하기 위한 파일입니다.
2
+ # 처리 결과 파일들이 이 폴더에 저장됩니다.
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ python-dotenv
2
+ google-generativeai
3
+ gradio
4
+ spaces
stt_processor.py ADDED
@@ -0,0 +1,294 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import google.generativeai as genai
3
+ from dotenv import load_dotenv
4
+ import logging
5
+ import json
6
+ from datetime import datetime
7
+ import re
8
+
9
+ # 환경 변수 로드
10
+ load_dotenv()
11
+
12
+ # 로깅 설정
13
+ logger = logging.getLogger(__name__)
14
+
15
+ class TextProcessor:
16
+ """
17
+ 텍스트를 AI를 통한 화자 분리 및 맞춤법 교정을 수행하는 클래스
18
+ """
19
+
20
+ def __init__(self, google_api_key=None):
21
+ """
22
+ TextProcessor 초기화
23
+
24
+ Args:
25
+ google_api_key (str): Google AI API 키. None인 경우 환경 변수에서 읽음
26
+ """
27
+ self.google_api_key = google_api_key or os.getenv("GOOGLE_API_KEY")
28
+ self.gemini_model = None
29
+ self.models_loaded = False
30
+
31
+ if not self.google_api_key or self.google_api_key == "your_google_api_key_here":
32
+ raise ValueError("Google AI API 키가 설정되지 않았습니다. 환경 변수 GOOGLE_API_KEY를 설정하거나 매개변수로 전달하세요.")
33
+
34
+ def load_models(self):
35
+ """Gemini AI 모델을 로드합니다."""
36
+ try:
37
+ logger.info("Gemini 모델 로딩을 시작합니다.")
38
+
39
+ # Gemini 모델 설정
40
+ genai.configure(api_key=self.google_api_key)
41
+ self.gemini_model = genai.GenerativeModel('gemini-2.0-flash')
42
+ logger.info("Gemini 2.0 Flash 모델 설정이 완료되었습니다.")
43
+
44
+ self.models_loaded = True
45
+ logger.info("Gemini 모델 로딩이 완료되었습니다.")
46
+ return True
47
+
48
+ except Exception as e:
49
+ error_msg = f"Gemini 모델을 로딩하는 중 오류가 발생했습니다: {e}"
50
+ logger.error(error_msg)
51
+ raise Exception(error_msg)
52
+
53
+ def process_text(self, input_text, text_name=None, progress_callback=None):
54
+ """
55
+ 텍스트를 처리하여 화자 분리 및 맞춤법 교정을 수행합니다.
56
+
57
+ Args:
58
+ input_text (str): 처리할 텍스트
59
+ text_name (str): 텍스트 이름 (선택사항)
60
+ progress_callback (function): 진행 상황을 알려주는 콜백 함수
61
+
62
+ Returns:
63
+ dict: 처리 결과 딕셔너리
64
+ """
65
+ if not self.models_loaded:
66
+ self.load_models()
67
+
68
+ try:
69
+ text_name = text_name or f"text_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
70
+ logger.info(f"텍스트 처리 시작: {text_name}")
71
+
72
+ # 입력 텍스트 검증
73
+ if not input_text or not input_text.strip():
74
+ raise ValueError("처리할 텍스트가 비어 있습니다.")
75
+
76
+ full_text = input_text.strip()
77
+
78
+ # 1단계: Gemini로 화자 분리
79
+ if progress_callback:
80
+ progress_callback("AI 화자 분리 중...", 1, 3)
81
+ logger.info(f"AI 화자 분리 시작: {text_name}")
82
+
83
+ speaker_separated_text = self.separate_speakers_with_gemini(full_text)
84
+
85
+ # 2단계: 맞춤법 교정
86
+ if progress_callback:
87
+ progress_callback("맞춤법 교정 중...", 2, 3)
88
+ logger.info(f"맞춤법 교정 시작: {text_name}")
89
+
90
+ corrected_text = self.correct_spelling_with_gemini(speaker_separated_text)
91
+
92
+ # 3단계: 결과 파싱
93
+ if progress_callback:
94
+ progress_callback("결과 정리 중...", 3, 3)
95
+
96
+ # 교정된 텍스트에서 화자별 대화 파싱
97
+ corrected_conversations = self.parse_separated_text(corrected_text)
98
+ original_conversations = self.parse_separated_text(speaker_separated_text)
99
+
100
+ # 결과 딕셔너리 생성
101
+ processing_result = {
102
+ "text_name": text_name,
103
+ "processed_time": datetime.now().isoformat(),
104
+ "original_text": full_text,
105
+ "separated_text": speaker_separated_text,
106
+ "corrected_text": corrected_text,
107
+ "conversations_by_speaker_original": original_conversations,
108
+ "conversations_by_speaker_corrected": corrected_conversations,
109
+ "success": True
110
+ }
111
+
112
+ logger.info(f"텍스트 처리 완료: {text_name}")
113
+ return processing_result
114
+
115
+ except Exception as e:
116
+ logger.error(f"텍스트 {text_name} 처리 중 오류: {e}")
117
+ return {
118
+ "text_name": text_name or "unknown",
119
+ "success": False,
120
+ "error": str(e)
121
+ }
122
+
123
+ def separate_speakers_with_gemini(self, text):
124
+ """Gemini API를 사용하여 텍스트를 화자별로 분리합니다."""
125
+ try:
126
+ prompt = f"""
127
+ 당신은 2명의 화자가 나누는 대화를 분석하는 전문��입니다.
128
+ 주어진 텍스트를 분석하여 각 발언을 화자별로 구분해주세요.
129
+
130
+ 분석 지침:
131
+ 1. 대화의 맥락과 내용을 기반으로 화자를 구분하세요
132
+ 2. 말투, 주제 전환, 질문과 답변의 패턴을 활용하세요
133
+ 3. 화자1과 화자2로 구분하여 표시하세요
134
+ 4. 각 발언 앞에 [화자1] 또는 [화자2]를 붙여주세요
135
+ 5. 발언이 너무 길 경우 자연스러운 지점에서 나누어주세요
136
+
137
+ 출력 형식:
138
+ [화자1] 첫 번째 발언 내용
139
+ [화자2] 두 번째 발언 내용
140
+ [화자1] 세 번째 발언 내용
141
+ ...
142
+
143
+ 분석할 텍스트:
144
+ {text}
145
+ """
146
+
147
+ response = self.gemini_model.generate_content(prompt)
148
+ separated_text = response.text.strip()
149
+
150
+ logger.info("Gemini를 통한 화자 분리가 완료되었습니다.")
151
+ return separated_text
152
+
153
+ except Exception as e:
154
+ logger.error(f"Gemini 화자 분리 중 오류: {e}")
155
+ return f"[오류] 화자 분리 실패: {str(e)}"
156
+
157
+ def correct_spelling_with_gemini(self, separated_text):
158
+ """Gemini API를 사용하여 화자별 분리된 텍스트의 맞춤법을 교정합니다."""
159
+ try:
160
+ prompt = f"""
161
+ 당신은 한국어 맞춤법 교정 전문가입니다.
162
+ 주어진 텍스트에서 맞춤법 오류, 띄어쓰기 오류, 오타를 수정해주세요.
163
+
164
+ 교정 지침:
165
+ 1. 자연스러운 한국어 표현으로 수정하되, 원본의 의미와 말투는 유지하세요
166
+ 2. [화자1], [화자2] 태그는 그대로 유지하세요
167
+ 3. 전문 용어나 고유명사는 가능한 정확하게 수정하세요
168
+ 4. 구어체 특성은 유지하되, 명백한 오타만 수정하세요
169
+ 5. 문맥에 맞는 올바른 단어로 교체하세요
170
+
171
+ 수정이 필요한 예시:
172
+ - "치특기" → "치트키"
173
+ - "실점픽" → "실전 픽"
174
+ - "복사부천억" → "복사 붙여넣기"
175
+ - "핵심같이가" → "핵심 가치가"
176
+ - "재활" → "재활용"
177
+ - "저정할" → "저장할"
178
+ - "플레일" → "플레어"
179
+ - "서벌 수" → "서버리스"
180
+ - "커리" → "쿼리"
181
+ - "전력" → "전략"
182
+ - "클라클라" → "클라크"
183
+ - "가인만" → "가입만"
184
+ - "M5U" → "MAU"
185
+ - "나온 로도" → "다운로드"
186
+ - "무시무치" → "무시무시"
187
+ - "송신유금" → "송신 요금"
188
+ - "10지가" → "10GB"
189
+ - "유금" → "요금"
190
+ - "전 색을" → "전 세계"
191
+ - "도무원은" → "도구들은"
192
+ - "골차품데" → "골치 아픈데"
193
+ - "변원해" → "변환해"
194
+ - "f 운영" → "서비스 운영"
195
+ - "오류추저개" → "오류 추적기"
196
+ - "f 늘려질" → "서비스가 늘어날"
197
+ - "캐시칭" → "캐싱"
198
+ - "플레이어" → "플레어"
199
+ - "업스테시" → "업스태시"
200
+ - "원시근을" → "웬지슨"
201
+ - "부각이릉도" → "부각들도"
202
+ - "컴포넌트" → "컴포넌트"
203
+ - "본이터링" → "모니터링"
204
+ - "번뜨기는" → "번뜩이는"
205
+ - "사용적 경험" → "사용자 경험"
206
+
207
+ 교정할 텍스트:
208
+ {separated_text}
209
+ """
210
+
211
+ response = self.gemini_model.generate_content(prompt)
212
+ corrected_text = response.text.strip()
213
+
214
+ logger.info("Gemini를 통한 맞춤법 교정이 완료되었습니다.")
215
+ return corrected_text
216
+
217
+ except Exception as e:
218
+ logger.error(f"Gemini 맞춤법 교정 중 오류: {e}")
219
+ return separated_text # 오류 발생 시 원본 반환
220
+
221
+ def parse_separated_text(self, separated_text):
222
+ """화자별로 분리된 텍스트를 파싱하여 구조화합니다."""
223
+ conversations = {
224
+ "화자1": [],
225
+ "화자2": []
226
+ }
227
+
228
+ # 정규표현식으로 화자별 발언 추출
229
+ pattern = r'\[화자([12])\]\s*(.+?)(?=\[화자[12]\]|$)'
230
+ matches = re.findall(pattern, separated_text, re.DOTALL)
231
+
232
+ for speaker_num, content in matches:
233
+ speaker = f"화자{speaker_num}"
234
+ content = content.strip()
235
+ if content:
236
+ conversations[speaker].append(content)
237
+
238
+ return conversations
239
+
240
+ def save_results_to_files(self, result, output_dir="output"):
241
+ """처리 결과를 파일로 저장합니다."""
242
+ if not result.get("success", False):
243
+ logger.error(f"결과 저장 실패: {result.get('error', 'Unknown error')}")
244
+ return False
245
+
246
+ try:
247
+ # output 폴더 생성
248
+ if not os.path.exists(output_dir):
249
+ os.makedirs(output_dir)
250
+
251
+ base_name = result["base_name"]
252
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
253
+
254
+ # 1. 전체 대화 저장 (원본, 화자 분리, 맞춤법 교정 포함)
255
+ all_txt_path = f"{output_dir}/{base_name}_전체대화_{timestamp}.txt"
256
+ with open(all_txt_path, 'w', encoding='utf-8') as f:
257
+ f.write(f"파일명: {base_name}\n")
258
+ f.write(f"처리 ��간: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
259
+ f.write(f"언어: {result['language']}\n")
260
+ f.write("="*50 + "\n\n")
261
+ f.write("원본 텍스트:\n")
262
+ f.write(result['original_text'] + "\n\n")
263
+ f.write("="*50 + "\n\n")
264
+ f.write("화자별 분리 결과 (원본):\n")
265
+ f.write(result['separated_text'] + "\n\n")
266
+ f.write("="*50 + "\n\n")
267
+ f.write("화자별 분리 결과 (맞춤법 교정):\n")
268
+ f.write(result['corrected_text'] + "\n")
269
+
270
+ # 2. 교정된 화자별 개별 파일 저장
271
+ for speaker, utterances in result['conversations_by_speaker_corrected'].items():
272
+ if utterances:
273
+ speaker_txt_path = f"{output_dir}/{base_name}_{speaker}_교정본_{timestamp}.txt"
274
+ with open(speaker_txt_path, 'w', encoding='utf-8') as f:
275
+ f.write(f"파일명: {base_name}\n")
276
+ f.write(f"화자: {speaker}\n")
277
+ f.write(f"처리 시간: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
278
+ f.write(f"발언 수: {len(utterances)}\n")
279
+ f.write("="*50 + "\n\n")
280
+
281
+ for idx, utterance in enumerate(utterances, 1):
282
+ f.write(f"{idx}. {utterance}\n\n")
283
+
284
+ # 3. JSON 형태로도 저장 (분석용)
285
+ json_path = f"{output_dir}/{base_name}_data_{timestamp}.json"
286
+ with open(json_path, 'w', encoding='utf-8') as f:
287
+ json.dump(result, f, ensure_ascii=False, indent=2)
288
+
289
+ logger.info(f"결과 파일 저장 완료: {output_dir}")
290
+ return True
291
+
292
+ except Exception as e:
293
+ logger.error(f"결과 파일 저장 중 오류: {e}")
294
+ return False
test_gradio.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Gradio 앱 기본 기능 테스트 스크립트
4
+ """
5
+
6
+ import os
7
+ import sys
8
+ import logging
9
+
10
+ # 로깅 설정
11
+ logging.basicConfig(level=logging.INFO)
12
+ logger = logging.getLogger(__name__)
13
+
14
+ def test_imports():
15
+ """필수 모듈 import 테스트"""
16
+ try:
17
+ import gradio as gr
18
+ logger.info(f"✓ Gradio 버전: {gr.__version__}")
19
+
20
+ import torch
21
+ logger.info(f"✓ PyTorch 버전: {torch.__version__}")
22
+
23
+ import whisper
24
+ logger.info("✓ OpenAI Whisper 가져오기 성공")
25
+
26
+ import google.generativeai as genai
27
+ logger.info("✓ Google Generative AI 가져오기 성공")
28
+
29
+ from stt_processor import STTProcessor
30
+ logger.info("✓ STTProcessor 모듈 가져오기 성공")
31
+
32
+ return True
33
+
34
+ except ImportError as e:
35
+ logger.error(f"❌ 모듈 import 실패: {e}")
36
+ return False
37
+
38
+ def test_stt_processor_init():
39
+ """STTProcessor 초기화 테스트 (API 키 없이)"""
40
+ try:
41
+ from stt_processor import STTProcessor
42
+
43
+ # API 키 없이 초기화 시도 (예상되는 오류)
44
+ try:
45
+ processor = STTProcessor()
46
+ logger.error("❌ API 키 없이 초기화 성공 (예상되지 않음)")
47
+ return False
48
+ except ValueError as e:
49
+ logger.info(f"✓ API 키 검증 로직 정상 작동: {e}")
50
+ return True
51
+
52
+ except Exception as e:
53
+ logger.error(f"❌ STTProcessor 테스트 실패: {e}")
54
+ return False
55
+
56
+ def test_gradio_interface():
57
+ """Gradio 인터페이스 생성 테스트"""
58
+ try:
59
+ from app import create_interface
60
+
61
+ # 인터페이스 생성 테스트
62
+ interface = create_interface()
63
+ logger.info("✓ Gradio 인터페이스 생성 성공")
64
+
65
+ # 인터페이스 구성 요소 확인
66
+ if hasattr(interface, 'blocks'):
67
+ logger.info("✓ Gradio Blocks 구조 확인")
68
+
69
+ return True
70
+
71
+ except Exception as e:
72
+ logger.error(f"❌ Gradio 인터페이스 테스트 실패: {e}")
73
+ return False
74
+
75
+ def test_file_structure():
76
+ """필수 파일 구조 확인"""
77
+ required_files = [
78
+ 'app.py',
79
+ 'stt_processor.py',
80
+ 'requirements.txt',
81
+ 'README.md',
82
+ 'deployment_guide.md'
83
+ ]
84
+
85
+ missing_files = []
86
+ for file in required_files:
87
+ if not os.path.exists(file):
88
+ missing_files.append(file)
89
+
90
+ if missing_files:
91
+ logger.error(f"❌ 누락된 파일: {missing_files}")
92
+ return False
93
+ else:
94
+ logger.info("✓ 모든 필수 파일 존재 확인")
95
+ return True
96
+
97
+ def main():
98
+ """테스트 실행"""
99
+ logger.info("🧪 Gradio STT 앱 테스트 시작")
100
+ logger.info("=" * 50)
101
+
102
+ tests = [
103
+ ("필수 모듈 import", test_imports),
104
+ ("파일 구조 확인", test_file_structure),
105
+ ("STTProcessor 초기화", test_stt_processor_init),
106
+ ("Gradio 인터페이스", test_gradio_interface)
107
+ ]
108
+
109
+ passed = 0
110
+ total = len(tests)
111
+
112
+ for test_name, test_func in tests:
113
+ logger.info(f"\n🔍 {test_name} 테스트...")
114
+ try:
115
+ if test_func():
116
+ passed += 1
117
+ logger.info(f"✅ {test_name} 통과")
118
+ else:
119
+ logger.error(f"❌ {test_name} 실패")
120
+ except Exception as e:
121
+ logger.error(f"❌ {test_name} 오류: {e}")
122
+
123
+ logger.info("\n" + "=" * 50)
124
+ logger.info(f"📊 테스트 결과: {passed}/{total} 통과")
125
+
126
+ if passed == total:
127
+ logger.info("🎉 모든 테스트 통과! 앱이 배포 준비되었습니다.")
128
+ return True
129
+ else:
130
+ logger.warning("⚠️ 일부 테스트가 실패했습니다. 문제를 확인해주세요.")
131
+ return False
132
+
133
+ if __name__ == "__main__":
134
+ success = main()
135
+ sys.exit(0 if success else 1)
test_stt.py ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import whisper
2
+ import google.generativeai as genai
3
+ import os
4
+ import json
5
+ from datetime import datetime
6
+ import re
7
+
8
+ def test_speaker_separation():
9
+ """Gemini를 사용한 화자 분리 테스트"""
10
+
11
+ # API 키 로드
12
+ from dotenv import load_dotenv
13
+ load_dotenv()
14
+
15
+ GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
16
+
17
+ if not GOOGLE_API_KEY or GOOGLE_API_KEY == "your_google_api_key_here":
18
+ print("ERROR: Please set GOOGLE_API_KEY in .env file")
19
+ return
20
+
21
+ print("Loading models...")
22
+
23
+ try:
24
+ # Whisper 모델 로드
25
+ whisper_model = whisper.load_model("base")
26
+ print("Whisper model loaded!")
27
+
28
+ # Gemini 모델 설정
29
+ genai.configure(api_key=GOOGLE_API_KEY)
30
+ # gemini-2.0-flash: 최신 Gemini 2.0 모델, 빠르고 정확한 화자 분리
31
+ gemini_model = genai.GenerativeModel('gemini-2.0-flash')
32
+ print("Gemini 2.0 Flash model configured!")
33
+
34
+ # WAV 파일 찾기
35
+ wav_files = []
36
+ if os.path.exists("data"):
37
+ for file in os.listdir("data"):
38
+ if file.endswith(".wav"):
39
+ wav_files.append(os.path.join("data", file))
40
+
41
+ if not wav_files:
42
+ print("No WAV files found in data folder.")
43
+ return
44
+
45
+ print(f"Found {len(wav_files)} WAV file(s)")
46
+
47
+ for wav_file in wav_files[:1]: # 첫 번째 파일만 테스트
48
+ print(f"\nProcessing: {os.path.basename(wav_file)}")
49
+
50
+ # 1단계: 음성 인식
51
+ print("Step 1: Speech recognition...")
52
+ result = whisper_model.transcribe(wav_file)
53
+ full_text = result['text'].strip()
54
+
55
+ print(f"Language detected: {result['language']}")
56
+ print(f"Text length: {len(full_text)} characters")
57
+ print(f"Text preview: {full_text[:200]}...")
58
+
59
+ # 2단계: 화자 분리
60
+ print("\nStep 2: Speaker separation with Gemini...")
61
+
62
+ prompt = f"""
63
+ 당신은 2명의 화자가 나누는 대화를 분석하는 전문가입니다.
64
+ 주어진 텍스트를 분석하여 각 발언을 화자별로 구분해주세요.
65
+
66
+ 분석 지침:
67
+ 1. 대화의 맥락과 내용을 기반으로 화자를 구분하세요
68
+ 2. 말투, 주제 전환, 질문과 답변의 패턴을 활용하세요
69
+ 3. 화자1과 화자2로 구분하여 표시하세요
70
+ 4. 각 발언 앞에 [화자1] 또는 [화자2]를 붙여주세요
71
+ 5. 발언이 너무 길 경우 자연스러운 지점에서 나누어주세요
72
+
73
+ 출력 형식:
74
+ [화자1] 첫 번째 발언 내용
75
+ [화자2] 두 번째 발언 내용
76
+ [화자1] 세 번째 발언 내용
77
+ ...
78
+
79
+ 분석할 텍스트:
80
+ {full_text}
81
+ """
82
+
83
+ response = gemini_model.generate_content(prompt)
84
+ separated_text = response.text.strip()
85
+
86
+ print("Speaker separation completed!")
87
+
88
+ # 3단계: 맞춤법 교정
89
+ print("\nStep 3: Spell checking with Gemini...")
90
+
91
+ spelling_prompt = f"""
92
+ 당신은 한국어 맞춤법 교정 전문가입니다.
93
+ 주어진 텍스트에서 맞춤법 오류, 띄어쓰기 오류, 오타를 수정해주세요.
94
+
95
+ 교정 지침:
96
+ 1. 자연스러운 한국어 표현으로 수정하되, 원본의 의미와 말투는 유지하세요
97
+ 2. [화자1], [화자2] 태그는 그대로 유지하세요
98
+ 3. 전문 용어나 고유명사는 가능한 정확하게 수정하세요
99
+ 4. 구어체 특성은 유지하되, 명백한 오타만 수정하세요
100
+ 5. 문맥에 맞는 올바른 단어로 교체하세요
101
+
102
+ 교정할 텍스트:
103
+ {separated_text}
104
+ """
105
+
106
+ corrected_response = gemini_model.generate_content(spelling_prompt)
107
+ corrected_text = corrected_response.text.strip()
108
+
109
+ print("Spell checking completed!")
110
+
111
+ # 4단계: 결과 저장
112
+ print("\nStep 4: Saving results...")
113
+
114
+ base_name = os.path.splitext(os.path.basename(wav_file))[0]
115
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
116
+
117
+ # output 폴더 생성
118
+ if not os.path.exists("output"):
119
+ os.makedirs("output")
120
+
121
+ # 전체 결과 저장 (원본 + 분리 + 교정)
122
+ result_path = f"output/{base_name}_complete_result_{timestamp}.txt"
123
+ with open(result_path, 'w', encoding='utf-8') as f:
124
+ f.write(f"Filename: {base_name}\n")
125
+ f.write(f"Processing time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
126
+ f.write(f"Language: {result['language']}\n")
127
+ f.write("="*50 + "\n\n")
128
+ f.write("Original text:\n")
129
+ f.write(full_text + "\n\n")
130
+ f.write("="*50 + "\n\n")
131
+ f.write("Speaker separated text (original):\n")
132
+ f.write(separated_text + "\n\n")
133
+ f.write("="*50 + "\n\n")
134
+ f.write("Speaker separated text (spell corrected):\n")
135
+ f.write(corrected_text + "\n")
136
+
137
+ # 교정된 텍스트에서 화자별 분리 결과 파싱
138
+ corrected_conversations = {"화자1": [], "화자2": []}
139
+ pattern = r'\[화자([12])\]\s*(.+?)(?=\[화자[12]\]|$)'
140
+ matches = re.findall(pattern, corrected_text, re.DOTALL)
141
+
142
+ for speaker_num, content in matches:
143
+ speaker = f"화자{speaker_num}"
144
+ content = content.strip()
145
+ if content:
146
+ corrected_conversations[speaker].append(content)
147
+
148
+ # 원본 화자별 분리 결과도 파싱 (비교용)
149
+ original_conversations = {"화자1": [], "화자2": []}
150
+ matches = re.findall(pattern, separated_text, re.DOTALL)
151
+
152
+ for speaker_num, content in matches:
153
+ speaker = f"화자{speaker_num}"
154
+ content = content.strip()
155
+ if content:
156
+ original_conversations[speaker].append(content)
157
+
158
+ # 교정된 화자별 개별 파일 저장
159
+ for speaker, utterances in corrected_conversations.items():
160
+ if utterances:
161
+ speaker_path = f"output/{base_name}_{speaker}_교정본_{timestamp}.txt"
162
+ with open(speaker_path, 'w', encoding='utf-8') as f:
163
+ f.write(f"Filename: {base_name}\n")
164
+ f.write(f"Speaker: {speaker}\n")
165
+ f.write(f"Processing time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
166
+ f.write(f"Number of utterances: {len(utterances)}\n")
167
+ f.write("="*50 + "\n\n")
168
+
169
+ for idx, utterance in enumerate(utterances, 1):
170
+ f.write(f"{idx}. {utterance}\n\n")
171
+
172
+ # JSON 저장 (원본과 교정본 모두 포함)
173
+ json_path = f"output/{base_name}_complete_data_{timestamp}.json"
174
+ json_data = {
175
+ "filename": base_name,
176
+ "processed_time": datetime.now().isoformat(),
177
+ "language": result['language'],
178
+ "original_text": full_text,
179
+ "separated_text": separated_text,
180
+ "corrected_text": corrected_text,
181
+ "conversations_by_speaker_original": original_conversations,
182
+ "conversations_by_speaker_corrected": corrected_conversations,
183
+ "segments": result.get("segments", [])
184
+ }
185
+
186
+ with open(json_path, 'w', encoding='utf-8') as f:
187
+ json.dump(json_data, f, ensure_ascii=False, indent=2)
188
+
189
+ print(f"Results saved:")
190
+ print(f" - Complete result: {result_path}")
191
+ print(f" - JSON data: {json_path}")
192
+ for speaker in corrected_conversations:
193
+ if corrected_conversations[speaker]:
194
+ print(f" - {speaker} (교정본): {len(corrected_conversations[speaker])} utterances")
195
+
196
+ print("\nProcessing completed successfully!")
197
+ print("✓ Speech recognition with Whisper")
198
+ print("✓ Speaker separation with Gemini 2.0")
199
+ print("✓ Spell checking with Gemini 2.0")
200
+ print("✓ Results saved (original + corrected versions)")
201
+
202
+ except Exception as e:
203
+ print(f"Error occurred: {e}")
204
+ import traceback
205
+ traceback.print_exc()
206
+
207
+ if __name__ == "__main__":
208
+ test_speaker_separation()