์ผ | ์ | ํ | ์ | ๋ชฉ | ๊ธ | ํ |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- db
- LIS
- LLM
- ์์ํ์
- ์๊ณ ๋ฆฌ์ฆ
- SK
- ๋์ ๊ณํ๋ฒ
- ๊ทธ๋ฆฌ๋
- ์ค๋ธ์
- ๊ทธ๋ํํ์
- ๋จธ์ง์ํธ
- ํ์ด์ฌ
- skala1๊ธฐ
- ๋ค์ด๋๋ฏนํ๋ก๊ทธ๋๋ฐ
- DFS
- ๋ฐฑ์ค
- SQL
- skala
- ๊ตฌํ
- ๋ณํฉ์ ๋ ฌ
- ๊ทธ๋ํ
- ๋๋น์ฐ์ ํ์
- ํฐ์คํ ๋ฆฌ์ฑ๋ฆฐ์ง
- ๊น์ด์ฐ์ ํ์
- ์ํ
- ๋ฐ์ดํฐ๋ฒ ์ด์ค
- BFS
- ํ๋ก๊ทธ๋๋จธ์ค
- DP
- ์ ๋ ฌ
- Today
- Total
๐๐ญ๐ฐ๐ธ ๐ฃ๐ถ๐ต ๐ด๐ต๐ฆ๐ข๐ฅ๐บ
[LLM] ํ์ํ์ ์ค STT to TTS ์ํํ๋ ์์คํ ์ค๊ณ - 2. ์ค์๊ฐ STT์ ๋ฒ์ญ์ด ๊ฐ๋ฅํ ์์คํ ๊ตฌํ(+ FastAPI ๋ชจ๋ธ์๋น) ๋ณธ๋ฌธ
[LLM] ํ์ํ์ ์ค STT to TTS ์ํํ๋ ์์คํ ์ค๊ณ - 2. ์ค์๊ฐ STT์ ๋ฒ์ญ์ด ๊ฐ๋ฅํ ์์คํ ๊ตฌํ(+ FastAPI ๋ชจ๋ธ์๋น)
.23 2025. 4. 19. 15:06Special thanks to. ๋ค๊ฑด๐
์ด์ ํฌ์คํ : [LLM] ํ์ํ์ ์ค STT to TTS ์ํํ๋ ์์คํ ์ค๊ณ - 1. OpenAI API 'Whisper-1' ํ์ฉํ์ฌ ์ค์๊ฐ STT ๊ตฌํ
์ด์ ์ ๋ง๋ค์๋ ์์ฑ ๋ น์ + ์ค์๊ฐ STT ์ฝ๋๋ฅผ ๊ฐ์ง๊ณ
๋ น์ ์์ ๋ฒํผ์ ๋๋ฅด๋ฉด ์ค๋จ ์์ด ํ๋ก๊ทธ๋จ์ด ์์ฒด์ ์ผ๋ก ๋ฌธ์ฅ์ ๋์ ํ๋จํ์ฌ ํ ๋ฌธ์ฅ์ฉ ์ ์ฌ์ ๋ฒ์ญ์ ์ํํ๋ ํ๋ก๊ทธ๋จ์ ์ค๊ณํ๋ค.
์ฌ์ค FE/BE ์ค์ ์ ๊ฑฐ์ ๋ค๊ฑด์ด๊ฐ ๋ค ํด์คฌ๊ณ ,
์์ฑ ๊ด๋ จ ํธ๋ฌ๋ธ์ํ ๋ถ๋ถ๋ง ๋ด๊ฐ ๋ฐ ์น์๋ค. ๊ฐ์ฌํฉ๋๋ค๐
์ฌ์ง์ 5์ด ์ ๋ ๊ธธ์ด์ ๋ฌธ์ฅ์ 3์ด๋ง์ ์ ์ฌํ๊ณ -> ๊ณง์ด์ด 1์ด ๋ด๋ก ๋ฒ์ญ์ด ์๋ฃ๋๋ ์คํ ๊ฒฐ๊ณผ ๋ชจ์ต์ด๋ค.
ํ๋ก ํธ ๋ชปํจ + ์ ๊ฒ ๋ฉ์ธ ํ๋ก์ ํธ๊ฐ ์๋์์ ์ด์๋ก ๋ณด์ฌ์ฃผ๋ ์์ฑ๋๋ฅผ ์๊ฐํ์ง ์๊ณ ๊ธฐ๋ฅ์๋ง ์ง์คํด์ ๊ตฌํํ๊ณ ์ ํ๊ธฐ ๋๋ฌธ์
์ด์ ์ฝ์ ํ๋ฉด์ฒ๋ผ ์ค์๊ฐ ์ ์ฌ๋๋ ๋ชจ์ต์ ํ์ธ์ด ์๋๋ค
๋ง๋ค. ๋ณ๋ช ์ด๋ค. ์ํผ ์ค์๊ฐ์ด๋ค.
?? : ์ค์๊ฐ์ ์๋ฏธ๋ ์์ ์ด ์ ์ํจ์ ๋ฐ๋ผ์ ๋ฌ๋ผ.
์ฌ์ฉ ๋ชจ๋ธ
- STT: whisper
- Translation: gpt-4o-mini
- TTS(์ฌ๊ธฐ์ ์ธ๊ธ ์ํจ): gpt-4o-mini-tts
ํ ํฐ์ ์ต๋ํ ์ ์ฝํ๋ฉด์ ์ค์๊ฐ์ฑ์ ํ๋ณดํ๊ธฐ ์ํด gpt-4o๋ tts-1๊ฐ์ ๋ชจ๋ธ๋ณด๋จ ์์ ๋ชจ๋ธ๋ค์ ์ฌ์ฉํ์๋ค.
์์ฒญ ๋ฐฉ๋ํ ์์ ๋ํ๋ฅผ ๋ น์ํ๊ฑฐ๋, ๋ณต์กํ ์ฒ๋ฆฌ๊ฐ ํ์ํ ๊ณผ์ ์ด ์์๊ธฐ ๋๋ฌธ์ mini ๋ชจ๋ธ๋ก๋ ์ถฉ๋ถํ ์ ๋์๊ฐ๋ค.๐
์ ์ฒด ์งํ ํ๋ก์ฐ
1. ์ฒ์ ์น์ ๋ค์ด์์ ๋, '๋ น์ ์์'์ ๋๋ฅด๋ฉด ์น์์ผ์ด ์ฐ๊ฒฐ๋์ด 2์ด ๋จ์ ์ฒญํฌ๋ก webm blob์ fastAPI๋ก ์ ์กํ๋ค.
2. fastAPI๋ ์คํ ์ ์ค๋ ๋๋ฅผ ๊ฐ๋์์ผ ๊ฐ ์ ์ฌ ํ / ๋ฒ์ญ ํ์ ๋ด์ฉ์ด ๋ค์ด์ค๋ฉด ๋ฐ๋ก ์์ ์ ์ํํ ์ ์๋๋ก ํ์๊ณ ,
websocket์ด ์ฐ๊ฒฐ๋๋ฉด ์ ๋ฌ๋ฐ์ .webm ํ์ผ์ .wav๋ก ๋ณํํ๊ณ , audio_queue์ ๊ฐ์ ๋ฃ์ด์ค๋ค.
3. stt_processing_thread()์์๋ audio_queue์ ๊ฐ์ด ๋ค์ด์ค๋ฉด ์ ์ฌ ์์ ์ ์ํํด์ ์ ์ฌ๋ ํ ์คํธ๋ฅผ ์ ์ฌ ๊ฒฐ๊ณผ ํ์ ๋ฃ์ด์ฃผ๊ณ
translation_thread()์์๋ ์ ์ฌ ๊ฒฐ๊ณผ ํ์ ๊ฐ์ด ๋ค์ด์ค๋ฉด ๋ฒ์ญ์ ํด์ ๋ฒ์ญ ๊ฒฐ๊ณผ ํ์ ๊ฒฐ๊ณผ๋ฅผ ๋ฃ์ด์ค๋ค.
4. ๋์ผํ๊ฒ fastAPI์ ๋ฉ์ธ ๋ผ์ฐํฐ์์ ๋น๋๊ธฐ์ ์ผ๋ก ๊ณ์ ์ ์ฌ ๊ฒฐ๊ณผ ํ์ ๋ฒ์ญ ๊ฒฐ๊ณผ ํ๋ฅผ ํ์ธํ๋ฉฐ
๊ฐ์ด ๋ค์ด์ฌ๋๋ง๋ค websocket์ผ๋ก ์น์ ๊ฐ ๊ฒฐ๊ณผ๋ฅผ ์ ์กํ๋ฉด, ๊ฒฐ๊ณผ๊ฐ ํ๋ฉด์ ๋ณด์ฌ์ง๋ค.
5. ์ฌ์ฉ์๊ฐ ๊ทธ๋ง ๊ฐ๊ณ ๋๊ณ ์ถ์ด์ '๋ น์ ์ค๋จ'์ ๋๋ฌ ์น์์ผ์ด ๋ซํ๋๊น์ง ๋ฐ๋ณตํด์ ์งํ๋๋ค.
ํ์ผ ๊ตฌ์กฐ
project/
โโโ config.py # ์ ์ญ ์ค์ (API ํค, ์ค๋์ค ์ค์ , ์ ์ญ ํด๋ผ์ด์ธํธ ๋ฑ)
โโโ main.py # ํ๋ก๊ทธ๋จ์ ์ง์
์ : ๊ฐ ๋ชจ๋์ ๋ถ๋ฌ์ ์ค๋ ๋ ์คํ
โโโ modules/
| โโโ __init__.py
| โโโ stt.py # STT ์ฒ๋ฆฌ (Whisper-1, VAD, ์ธ์ด ์๋ ๊ฐ์ง)
| โโโ translation.py # ๋ฒ์ญ ์ฒ๋ฆฌ (GPT-4o-mini ์ฌ์ฉ)
| โโโ tts.py # TTS ์ฒ๋ฆฌ (GPT-4o-mini-tts ์ฌ์ฉ)
| โโโ utils.py # ๊ณตํต ์ ํธ๋ฆฌํฐ ํจ์ (์ธ์ด ๋ณด์ , ๋ก๊ทธ ํ์ผ๋ช
์์ฑ ๋ฑ)
โโโ templates/
โโโ index.html # ํ
์คํธ์ฉ ๊ฐ๋จํ ํ๋ก ํธ ์ฝ๋
์ฝ๋
ํ๋ก ํธ์์ ๊ผญ ํด์ค์ผ ํ ์ผ!!!
์ฌ์ค ์ด ํ๋ก์ ํธ ํ๋ฉด์ ๋๋๊ฑด ํ๋ก ํธ๊ฐ ์ ์ผ ์ค์ํ๋ค.... ์ ๋ง ๋๋ฌดํ๋ค์๋ค๐ญ
ํ๋ก ํธ๋จ์์ ์ ๋๋ก ๋ ๋ฐ์ดํฐ๋ฅผ ๋ณด๋ด์ฃผ์ง ์์ผ๋ฉด, ์๋ฌด๋ฆฌ ํ์ด์ฌ ์ชฝ์์ ์ฝ๋๋ฅผ ๊ธฐ๊น๋๊ฒ ๊ตฌ์ฑํ์ด๋ ์๋ฌด๋ฐ ์๋ ๊ฒฐ๊ณผ๋ฅผ ํ์ธํ ์ ์๋ค.
ํด๋น ํ๋ก์ ํธ์์ ์ฃผ์ํ ๊ฒ์ webmํ์ผ์ ์ฒญํฌ๋จ์๋ก ๋ณด๋ผ ๋ 'ํค๋ ์ ๋ณด'๋ฅผ ๋ฐ๋์ ํฌํจํด์ ๋ณด๋ด์ผ ํ๋ ๊ฒ์ด๋ค.
๊ทธ๋ฌ๋, ์ต์ด 1ํ ์ ์ก๋๋ ํ์ผ์๋ง ํค๋ ์ ๋ณด๊ฐ ๋ถ๊ณ ๊ทธ ์ดํ์๋ ๋ น์๋ ๋ฐ์ดํฐ๋ง ์ ์ก๋๋๋ฐ, ๊ทธ๋ผ ffmpeg ๋ชจ๋์ด ์ ๋๋ก ๋ ๋ฐ์ดํฐ๊ฐ ์๋๋ผ๊ณ ๋ ์ด๊ฑฐ ๋ณํ ๋ชปํ๋ค๋ฉฐ ๋ฏธ์ณ ๋ ๋ด๋ค.
์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํ ๋ฐฉ๋ฒ์
1. ํค๋ ์ ๋ณด๋ฅผ ๋ณ์์ ์ง์ ํด๋๊ณ , ๋งค blob์ ํค๋ ์ ๋ณด๋ฅผ ๋ถ์ฌ ๋ณด๋ด์ค๋ค.
2. ๊ทธ๋ฅ ๋ น์์ ์์ ์ฒญํฌ๋ง๋ค ์๋ก ํ๋ค.
์๋๋ฐ, webm์ ํค๋์ ๋ณด๋ ๊ณ ์ ๊ธธ์ด๊ฐ ์๋๋ผ ๋งค๋ฒ ํฌ๊ธฐ๊ฐ ๋ฌ๋ผ์ง ์ ์์ผ๋ฏ๋ก 2๋ฒ์ด ๋ฌด์ํด๋ณด์ด์ง๋ง ์๊ฐ๋ณด๋ค ์๋จนํ๋ค.
๊ทผ๋ฐ ๊ทธ๋ผ
์ด? ๊ทธ๋ผ ์ค๊ฐ์ค๊ฐ ๋ น์์ด ์๋ ์ ์๋๊ฑฐ ์๋๊ฐ?
์ถ๊ธฐ๋ ํ์ง๋ง, ์ค์ ์คํ ๊ฒฐ๊ณผ ๋ค์ ๋ น์๊ธฐ๋ฅผ ๊ป๋ค ํค๋ ๊ณผ์ ์ ๊ต์ฅํ ๋น ๋ฅธ ์๊ฐ์์ ์ด๋ฃจ์ด์ก๊ณ ,
๊ทธ ๊ณผ์ ์์ ๋๋จํ ๋ด์ฉ ์์ค์ด ๋ฐ์ํ์ง๋ ์์๊ธฐ ๋๋ฌธ์
2์ด๋ง๋ค ๋ น์ํ์ผ์ ํ๋ฒ์ฉ ๋ณด๋ด์ฃผ๋ ๋ฐฉ์์ ์ ํํ๊ฒ ๋์๋ค.
function startRecording() {
const chunks = [];
mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm' });
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
chunks.push(event.data);
}
};
mediaRecorder.onstop = () => {
if (chunks.length > 0 && ws && ws.readyState === WebSocket.OPEN) {
const blob = new Blob(chunks, { type: 'audio/webm' });
ws.send(blob);
console.log('๋
น์ ๋ฐ์ดํฐ ์ ์ก๋จ, ํฌ๊ธฐ:', blob.size);
}
};
mediaRecorder.start();
console.log('์ ๋
น์ ์ธ์
์์');
}
startRecording()์์๋ audio/webm ์ง์ ํด์ค์ MediaRecorder ์ฌ์ฉํ์ฌ ๋ น์์ ์ํํ๊ณ ,
์๋์ ์ค์ ํด์ฃผ๋ Interval ์๊ฐ๋ง๋ค blob์ ๋ฐ์ดํฐ๋ฅผ ๋ด์ ์น์์ผ์ผ๋ก ์ ์กํด์ค๋ค.
// ๋ง์ดํฌ ์คํธ๋ฆผ ์ป๊ธฐ
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
// WebSocket ์ฐ๊ฒฐ (HTTP ํ๊ฒฝ์ด๋ฉด "ws://", HTTPS์ด๋ฉด "wss://")
ws = new WebSocket(`ws://${window.location.host}/ws/stt`);
ws.onopen = () => {
console.log('WebSocket ์ฐ๊ฒฐ๋จ');
status.textContent = '์ํ: WebSocket ์ฐ๊ฒฐ๋จ';
// ๋
น์ ์์
startRecording();
// 3์ด๋ง๋ค ์๋ก์ด ๋
น์ ์ธ์
์์
recordingInterval = setInterval(() => {
if (isRecording) {
stopRecording();
startRecording();
}
}, 2000);
// ์ดํ ์ฝ๋
main
app = FastAPI()
@app.on_event("startup")
async def startup_event():
# stt, ๋ฒ์ญ ์ค๋ ๋ ์์
threading.Thread(
target=stt_processing_thread,
args=(audio_queue, sentence_queue, transcription_queue, recording_active, "ko"),
daemon=True
).start()
threading.Thread(
target=translation_thread,
args=(sentence_queue, translation_queue, translated_queue, "en"),
daemon=True
).start()
asyncio.create_task(result_sender_task())
on_event ๋ถ๋ถ์์ fastAPI ์คํ ์ ์ด๋ค ๊ฒ๋ค์ด ์คํ๋์ด์ผ ํ๋์ง ์ ์ํด์ฃผ์๋ค.
"startup" ์ด๋ฒคํธ ์คํ์ด ์์๋๋ฉด,
๋ ๊ฐ์ ์ค๋ ๋ ์คํ
- stt_processing_thread ํธ์ถ
- translation_thread ํธ์ถ
๊ฒฐ๊ณผ ์ ์กํด์ฃผ๋ asyncio ์ฝ๋ฃจํด ์ ์
๊ฐ ์ํ๋๋ค.
์ฐธ๊ณ ๋ก on_event๋ deprecated ๋ ๊ธฐ๋ฅ์ด๊ธฐ ๋๋ฌธ์, FastAPI 0.95+ ๋ฒ์ ๋ถํฐ๋ lifespan ์ด๋ฒคํธ ์ฒ๋ฆฌ ๋ฐฉ์์ ์ฌ์ฉํ๋๊ฑธ ๊ถ์ฅํ๋ค. ๋ณดํต์ lifespan ํจ์๋ฅผ ์ ์ํด์ ์ฐ์ง๋ง, ๊ทธ๋ฅ if name=='__main__' ์์ ์ ์ํด์ค๋ ์๊ด์ ์๋ค. ๊ทผ๋ฐ ๋๋ ๊ทธ๋ฅ ์คํ์ ๋ฌธ์ ์์ด์ ์ ๋๋ก ์ผ๋ค.
# STT WebSocket ์๋ํฌ์ธํธ
@app.websocket("/ws/stt")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
print("[DEBUG] WebSocket ์ฐ๊ฒฐ๋จ")
websocket_clients.append(websocket)
try:
while True:
data = await websocket.receive_bytes()
# ์์ ํ์ผ๋ก ์ ์ฅ
with tempfile.NamedTemporaryFile(suffix=".webm", delete=False) as temp_file:
temp_file_path = temp_file.name
temp_file.write(data)
print(f"[DEBUG] ์์ ํ์ผ ์์ฑ๋จ: {temp_file_path}")
try:
# ffmpeg๋ก .webm → .wav ๋ณํ (BytesIO ํํ)
wav_buffer = convert_webm_to_wav_bytes(temp_file_path)
os.unlink(temp_file_path) # ์์ ํ์ผ ์ญ์
if wav_buffer is None:
print("[DEBUG] ๋ณํ ์คํจํ ์ฒญํฌ ๊ฑด๋๋")
continue
# BytesIO ๋ฒํผ์์ ์ค๋์ค ๋ฐ์ดํฐ ์ฝ๊ธฐ
wav_buffer.seek(0)
audio_data, sample_rate = sf.read(wav_buffer, dtype='float32')
print(f"[DEBUG] ์์ ๋ ์ค๋์ค ๋ฐ์ดํฐ: shape {audio_data.shape}, sample_rate {sample_rate}")
# ์คํ
๋ ์ค๋ฉด ๋ชจ๋
ธ๋ก ๋ณํ
if len(audio_data.shape) > 1:
audio_data = audio_data.mean(axis=1).reshape(-1, 1)
else:
audio_data = audio_data.reshape(-1, 1)
# audio_queue์ ์ถ๊ฐ (STT ์ฒ๋ฆฌ ์ค๋ ๋๋ก ์ ๋ฌ)
audio_queue.put(audio_data)
print(f"[DEBUG] audio_queue์ ๋ฐ์ดํฐ ์ถ๊ฐ๋จ. ํ์ฌ queue ํฌ๊ธฐ: {audio_queue.qsize()}")
except Exception as e:
print(f"[DEBUG] ์ค๋์ค ์ฒ๋ฆฌ ์ค๋ฅ: {e}")
if os.path.exists(temp_file_path):
os.unlink(temp_file_path)
except WebSocketDisconnect:
print("[DEBUG] WebSocket ์ฐ๊ฒฐ ์ข
๋ฃ๋จ")
if websocket in websocket_clients:
websocket_clients.remove(websocket)
๊ฐ์ฅ ์ค์ํ ๋ฐ์ดํฐ ๋ฐ์์ค๋ ๋ถ๋ถ..
์น์์ผ ์ฌ์ฉ + ๋์๋ค๋ฐ์ ์ผ๋ก ์์ฑ์ ๋ฐ์์ค๋ฉด ๋ฐ์์ค๋ ๋๋ก ํ์ ๋ฃ๋ ์์ ์ ์ํํ๊ธฐ ์ํด ๋น๋๊ธฐ ํจ์๋ก ์์ ๊ฐ์ด ์ฝ๋๋ฅผ ์งฐ๋ค.
ํ๋ก ํธ๋จ์์ ์์ฑ ๋ฐ์ดํฐ๋ฅผ ์น์์ผ์ผ๋ก FastAPI์ ์ด์ฃผ๋ฉด, ์ด๋ฅผ stt๋ชจ๋ธ์ธ whisper์ ์ ๋ฌํด์ฃผ๊ธฐ ์ํด ๋ณํ์ ์ํํ๋ค.
whisper ๋ชจ๋ธ์ .wav๋ฅผ ์ ๋ ฅ์ผ๋ก ๋ฐ๊ธฐ ๋๋ฌธ์ webm์ผ๋ก ๋ น์๋ ํ์ผ์ ํ๋ฒ ๋ณํํด์ฃผ๋ ์์ ์ด ํ์ํ๋ค.
๋๋ฌธ์ ffmpeg ์ค์น๊ฐ ํ์์ด๋ฉฐ, ๋ฐ๋์ ํ์ด์ฌ๊ณผ ๋ก์ปฌ์ ๊ฐ๊ฐ ์ค์น๋ฅผ ํด์ค์ผ ํ๋ค.
pip install ffmpeg-python
brew install ffmpeg
stt
def is_speech(buffer, sample_rate=16000, frame_duration_ms=30, speech_threshold=0.3):
audio_int16 = np.int16(buffer * 32767)
audio_bytes = audio_int16.tobytes()
frame_size = int(sample_rate * (frame_duration_ms / 1000.0))
num_frames = len(audio_int16) // frame_size
if num_frames == 0:
return False
speech_frames = 0
for i in range(num_frames):
start = i * frame_size * 2 # 2๋ฐ์ดํธ per ์ํ
frame = audio_bytes[start: start + frame_size * 2]
if len(frame) < frame_size * 2:
break
if vad.is_speech(frame, sample_rate):
speech_frames += 1
fraction = speech_frames / num_frames
return fraction >= speech_threshold
stt ํจ์์์๋ ์ฐ์ ํ์ฌ ๋ค์ด์ค๋ ์ฒญํฌ ์ ๋ณด๊ฐ ์ ๋๋ก ๋ ์์ฑ ์ ๋ณด์ธ์ง, ๋๋ฌด ์งง์ ๋จ์์ ์ฒญํฌ๊ฐ ๋ค์ด์จ ๊ฒ์ ์๋์ง๋ฅผ ๊ตฌ๋ณํ๊ธฐ ์ํด is_speech๋ผ๋ ํจ์๋ฅผ ์ ์ํ๋ค.
๋ฒํผ์ ํฌํจ๋ ์์ฑ์ ๋ณด๊ฐ ๋ฐํ๋ก ์ธ์ํ๊ธฐ ํ๋ ์ง๋์น๊ฒ ์งง์(0.3์ด ์ดํ)์ง, ๋งํ๊ณ ์๊ธด ํ๊ฒ์ธ์ง๋ฅผ ๊ตฌ๋ถํ๋ค.
def detect_language(audio_path):
try:
with open(audio_path, "rb") as audio_file:
response = CLIENT.audio.transcriptions.create(
model="whisper-1",
file=audio_file,
response_format="verbose_json"
)
detected_lang = response.language
sanitized = sanitize_language_code(detected_lang)
print(f"[DEBUG] ๊ฐ์ง๋ ์ธ์ด (๋ณด์ ๋จ): {sanitized}")
return sanitized
except Exception as e:
print(f"์ธ์ด ๊ฐ์ง ์ค๋ฅ: {e}", file=sys.stderr)
return DEFAULT_LANGUAGE
๋์๊ฐ๋ ํ๋ก๊ทธ๋จ์ด ๋จ์ํ ํ๊ตญ์ด -> ์์ด๋ก ๋ฒ์ญํ๋ ๊ฒ์ด ์๋๊ณ , ์ดํ ํ์ํ์์์ ๋ค๊ตญ์ด ์์ญ์ ํ์ฉ๋ ๊ฒ์ ๊ณ ๋ คํ์ฌ ์ธ์ด๋ฅผ ์ง์ ๊ฐ์งํ๊ฒ ํด๋ณด๊ณ ์ถ์๋ค. ๊ทธ๋์ detect_language๋ฅผ ๋ง๋ค์ด ๋ฒํผ์ ์ ์ฅ๋ ์ค๋์ค ํ์ผ์ ๊ณต์ ํ๋ฉฐ ์ธ์ด๋ฅผ ์๋์ผ๋ก ํ์งํ๋ ํจ์๋ฅผ ๋ง๋ค์ด์คฌ๋ค.
๊ทธ๋์ ๋น๋ก UI๋ ์์ ์ด ๊ท์ฐฎ์ ๊ด๊ณ๋ก ์๋ณธ ํ ์คํธ(ํ๊ตญ์ด) ๋ก ์ ํ์๊ธด ํ์ง๋ง..
์ด๋ ๊ฒ ์ผ๋ณธ์ด๋ ํ๋์ค์ด๋ ์ ๋๋ค๐
์๋ ๊ฐ๋จ์ด๋ผ๊ณ ๋ฐ์ํ๊ณ ์ถ์๋๋ฐ ๋ฐ์์ด์๋ก ์ฝ๋๋๊ฑฐ๋ง ๋นผ๊ณ
def stt_processing_thread(audio_queue, sentence_queue, transcription_queue, recording_active, target_language):
global detected_language
buffer = np.zeros((0, 1), dtype=np.float32)
silence_threshold = 0.02
silence_duration_threshold = 0.5
silence_start = None
language_detected_once = False
while True:
try:
data = audio_queue.get(timeout=1)
buffer = np.concatenate((buffer, data), axis=0)
current_time = time.time()
amplitude = np.mean(np.abs(data))
if amplitude < silence_threshold:
if silence_start is None:
silence_start = current_time
elif current_time - silence_start >= silence_duration_threshold:
if len(buffer) > int(SAMPLE_RATE * 0.5):
if not is_speech(buffer):
buffer = np.zeros((0, 1), dtype=np.float32)
silence_start = None
audio_queue.task_done()
continue
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
sf.write(f.name, buffer, SAMPLE_RATE, format='WAV', subtype='PCM_16')
if not language_detected_once:
lang_code = detect_language(f.name)
if lang_code:
with language_lock:
detected_language = lang_code
language_detected_once = True
with language_lock:
current_lang = detected_language if detected_language is not None else DEFAULT_LANGUAGE
with open(f.name, "rb") as audio_file:
response = CLIENT.audio.transcriptions.create(
model="whisper-1",
file=audio_file,
language=current_lang,
prompt="We're now on meeting. Please transcribe exactly what you hear."
)
os.unlink(f.name)
text = response.text.strip()
if text:
print(f"[DEBUG] STT ๊ฒฐ๊ณผ: {text}")
source_log, _ = get_log_filenames(detected_language, target_language)
with open(source_log, "a", encoding="utf-8") as f:
f.write(text + "\n")
with language_lock:
src_lang = detected_language if detected_language is not None else DEFAULT_LANGUAGE
sentence_queue.put((text, src_lang))
transcription_queue.put((text, src_lang))
buffer = np.zeros((0, 1), dtype=np.float32)
silence_start = None
else:
silence_start = None
audio_queue.task_done()
except queue.Empty:
continue
stt๋ฅผ ์ค์ ๋ก ์ํํ๋ ํจ์์์๋ ์ด์ ๊ณผ ํฌ๊ฒ ๋ฌ๋ผ์ง ๋ถ๋ถ์ ์๊ณ ,
๋ ธ์ด์ฆ๋ฅผ ์ต๋ํ ๋ฐ์ํ์ง ์๊ธฐ ์ํด ์งํญ์ ๊ฐ์งํ์ฌ silence_threshold(0.02)๊ฐ์ ์ถ๊ฐํ์ฌ ํน์ ์งํญ๋ณด๋ค ๋ฎ์ ๊ฐ์ด ๋ค์ด์ค๋ฉด ์นจ๋ฌต์ด๋ผ๊ณ ๊ฐ์ ํ๋๋ก ํ๊ณ ,
์ด๋ฌํ ์นจ๋ฌต์๊ฐ์ด silence_duration_threshold(0.5์ด)๋ณด๋ค ๊ธธ์ด์ง๋ฉด ํ ๋ฌธ์ฅ์ด ๋๋ฌ๋ค๊ณ ๊ฐ์ฃผํ์ฌ data์ ๋์ ํด๋์ ๋ฒํผ๋ค์ ์ ์ฌ๋ฅผ ์งํํ๋ค.
silence_threshold๊ฐ์ ๋๋ฌด ๋ฎ์ผ๋ฉด ์นจ๋ฌต์ ์์ธ ๋ ธ์ด์ฆ๋ ๋ฐํ๋ผ๊ณ ๊ฐ์ฃผ๋์ด ๊ตฌ๋ ํด์ฃผ์ ์ ๊ฐ์ฌํ๋ค๋ ๋ฌธ๊ตฌ๋ก ์ธ์ํ๊ณ ,
๋๋ฌด ๋์ผ๋ฉด ๋ด๊ฐ ์ค์ ๋ก ๋งํ ๊ฒ๋ ์ธ์์ด ๋์ง ์์ผ๋ ์ ๋นํ ๊ฐ์ ๊ฒฝํ์ ์ผ๋ก ์ค์ ํด์ฃผ๋ ๊ฒ์ด ์ข์ ๊ฒ ๊ฐ๋น.
์ ์ฌ๊ฐ ์๋ฃ๋ ๋ฌธ์ฅ์ sentence_queue์ transciption_queue๋ก ์ ๋ฌ๋๋๋ฐ,
๊ฐ๊ฐ translation_thread์ ์น์์ผ์ผ๋ก ์ ์ก๋๋ ์ญํ ์ ํ๋ค.
ํ์์ ๋ฐ์ดํฐ ์ฒ๋ฆฌ ์ get(pop)์ ํตํด ๊ฐ์ ๊บผ๋ด์ค๊ธฐ ๋๋ฌธ์, ์ด์ค์ผ๋ก ๊บผ๋ด์ค๋ ๊ฒ์ ๋ฐฉ์งํ๊ธฐ ์ํด ๋๊ฐ์ ํ๋ฅผ ์ ์ํ์ฌ ์ฒ๋ฆฌ๋ฅผ ์งํํ๊ฒ ํ๋ค.
translation
def translation_thread(sentence_queue, translation_queue, translated_queue, target_language):
while True:
try:
sentence_data = sentence_queue.get(timeout=1)
if isinstance(sentence_data, tuple):
sentence, source_lang = sentence_data
else:
sentence = sentence_data
source_lang = "ko"
print(f"[DEBUG] ๋ฒ์ญํ ๋ฌธ์ฅ: {sentence} (์์ค ์ธ์ด: {source_lang})")
if source_lang == target_language:
print("[DEBUG] ์์ค ์ธ์ด์ ํ๊ฒ ์ธ์ด ๋์ผ, ๋ฒ์ญ ๊ฑด๋๋")
translation_queue.put(sentence)
sentence_queue.task_done()
continue
try:
source_name = language_map.get(source_lang, "๊ฐ์ง๋ ์ธ์ด")
target_name = language_map.get(target_language, "์์ด")
response = CLIENT.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": f"Translate the following text from {source_name} to {target_name}. Only provide the translation without any additional explanation."},
{"role": "user", "content": sentence}
]
)
translation = response.choices[0].message.content.strip()
print(f"[DEBUG] ๋ฒ์ญ ๊ฒฐ๊ณผ: {translation}")
except Exception as e:
print(f"๋ฒ์ญ ์ค๋ฅ: {e}", file=sys.stderr)
translation = ""
if translation:
_, target_log = get_log_filenames(source_lang, target_language)
with open(target_log, "a", encoding="utf-8") as f:
f.write(translation + "\n")
translation_queue.put(translation)
translated_queue.put(translation)
sentence_queue.task_done()
except queue.Empty:
continue
์ด์ ํ๋ก์ ํธ์ ๋ฌ๋ผ์ง ๊ฒ์ ์ฌ๊ธฐ์๋ ๋ฒ์ญ์ด ์ถ๊ฐ๋์๋ค๋ ์ !
๊ทธ๋ฌ๋ ๋ฒ์ญ์ ์ฌ์ค ์ฝ๋ค..
์ ์ถ๋ ฅ ๋ฐ์ดํฐ๊ฐ ๋ชจ๋ ํ ์คํธ์ด๊ธฐ ๋๋ฌธ์ ์ค๊ฐ ๊ฒฐ๊ณผ๋ค ๋ก๊ทธ ์ฐ์ด์ ๋ณด๊ธฐ๋ ๋๋ฌด ์ข๊ณ
๋ฒ์ญ๋ GPT-4o-mini ๋ชจ๋ธ์ ์ผ๊ธฐ ๋๋ฌธ์ stt๋ก ๋ค์ด๊ฐ๋ ๋ฌธ์ฅ์ค ๋งฅ๋ฝ์ ๋ถ์์ฐ์ค๋ฌ์ด ๋จ์ด๊ฐ ํฌํจ๋์ด์์ด๋ ์์์ ๊ฐ์ ๋ ๋ฒ์ญ์ ๋ด๋๊ธฐ ๋๋ฌธ์ ๊ฒฐ๊ณผ๋ฌผ์ ๋ํ ๊ฑฑ์ ๋ ์๋ค.
์์ ํ์ค๋ ๋ชฐ๋ ํ ์คํธํด๋ณธ์ ์ด ์๋๋ฐ ์ฌ๋ฌ๊ฐ์ ๊ฝค ๊ธด ๋ฌธ์ฅ์ ๋ฐํ๋ ์๋๋ ๊ฑธ ๋ณผ ์ ์๋ค.
์์ ์ ์ฌ-๋ฒ์ญ ๊ฒฐ๊ณผ ์ค ์ฒซ๋ฒ์งธ ๋ฌธ์ฅ์์
์๋ฌธ์ด
๋ฐ์ดํฐ ์จ์ด ํ์ฐ์ค๊ฐ ๊ฐ๊ณ ์๋ ๋ฐ์ดํฐ ์ฌ์ ์ ๋ง๊ฒ ๋ฐ์ดํฐ๋ฅผ ๋ณํ์ํค๊ณ ๊ทธ ๋ค์์ ์ด์ ๋ฐ์ดํฐ ์จ์ด ํ์ฐ์ค์์ ์ธ๋ ค๋ฃ๋๊ฑฐ๋ค.
๋ก ์ธ์๋์์ผ๋,
๋ฒ์ญ์์๋
The data is transformed to match the data dictionary of the data warehouse, and then it is loaded into the data warehouse.
์ด๋ ๊ฒ ์์์ ์ฌ๋ ค ๋ฃ๋ค / ์ ์ฌ์ ์๋ฏธ์ธ load๋ก ์ ์์ ๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
sentence_data์์ ๊บผ๋ด์จ ๋ฐ์ดํฐ์์๋ถํฐ ๋ฒ์ญ์ ์งํํ๋๋ฐ, ์ฌ๊ธฐ์๋ถํด 'ํ ๋ฌธ์ฅ ๋จ์'์ ์ ์ฌ ๊ฒฐ๊ณผ๊ฐ ์ ๋ฌ๋์์๊ฒ์ด๋ฏ๋ก, ๋ฌธ์ฅ์ ๊ธธ์ด๊ฐ ์งง๊ณ ๋ง๊ณ ๋ฅผ ํ๋จํด ๋ฒ์ญ ์งํ์ ๊ฒฐ์ ํ์ง ์๋๋ค. ๊ทธ๋์ ์์ธ์ฒ๋ฆฌ ํด์ค ๊ฒ๋ ์ ์๋ค.
์๋์ stt์ ๋ง์ฐฌ๊ฐ์ง๋ก ๋ฒ์ญ๋ ํ ์คํธ ๊ฒฐ๊ณผ๊ฐ ๋ฐํ๋์์ ๊ฒฝ์ฐ tts ์ ์ก ๋จ๊ณ๋ฅผ ๊ณ ๋ คํ์ฌ translation_queue์ translated_queue์ ๊ฐ๊ฐ ๋ฒ์ญ ๊ฒฐ๊ณผ๋ฅผ ๋ฃ์ด์ค๋ค.
์คํ ๊ฒฐ๊ณผ