Spaces:
Runtime error
Runtime error
Jeongsoo1975
commited on
Commit
·
ae9ec05
0
Parent(s):
Initial commit: Gradio text-based speaker separation app for Hugging Face Spaces
Browse files- .gitignore +122 -0
- README.md +60 -0
- README_backup.md +152 -0
- app.py +251 -0
- audio_summarizer.py +491 -0
- check_models.py +33 -0
- data/.gitkeep +2 -0
- deployment_guide.md +154 -0
- env_example.txt +17 -0
- output/.gitkeep +2 -0
- requirements.txt +4 -0
- stt_processor.py +294 -0
- test_gradio.py +135 -0
- 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()
|