์ผ | ์ | ํ | ์ | ๋ชฉ | ๊ธ | ํ |
---|---|---|---|---|---|---|
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 |
- ํฐ์คํ ๋ฆฌ์ฑ๋ฆฐ์ง
- ๋ฐ์ดํฐ๋ฒ ์ด์ค
- skala
- ๊ทธ๋ฆฌ๋
- SQL
- db
- ๋ค์ด๋๋ฏนํ๋ก๊ทธ๋๋ฐ
- ์๊ณ ๋ฆฌ์ฆ
- ๊ทธ๋ํ
- ๊น์ด์ฐ์ ํ์
- ๊ทธ๋ํํ์
- ์ค๋ธ์
- ๋์ ๊ณํ๋ฒ
- DFS
- ๊ตฌํ
- ๋จธ์ง์ํธ
- BFS
- SK
- ์ ๋ ฌ
- ํ๋ก๊ทธ๋๋จธ์ค
- ํ์ด์ฌ
- ๋๋น์ฐ์ ํ์
- LLM
- DP
- ์์ํ์
- ์ํ
- skala1๊ธฐ
- LIS
- ๋ณํฉ์ ๋ ฌ
- ๋ฐฑ์ค
- Today
- Total
๐๐ญ๐ฐ๐ธ ๐ฃ๐ถ๐ต ๐ด๐ต๐ฆ๐ข๐ฅ๐บ
[LLM] ํ์ํ์ ์ค STT to TTS ์ํํ๋ ์์คํ ์ค๊ณ - 3. OpenVidu ๊ธฐ๋ฐ ํ์ํ์ ์์คํ ์์ ์ค์๊ฐ STS(STT-๋ฒ์ญ-TTS) ์๋น์ค ๊ตฌํ ๋ณธ๋ฌธ
[LLM] ํ์ํ์ ์ค STT to TTS ์ํํ๋ ์์คํ ์ค๊ณ - 3. OpenVidu ๊ธฐ๋ฐ ํ์ํ์ ์์คํ ์์ ์ค์๊ฐ STS(STT-๋ฒ์ญ-TTS) ์๋น์ค ๊ตฌํ
.23 2025. 4. 20. 18:12๋๋์ด ๋ง์ง๋ง..
์ด์ ํฌ์คํ : [LLM] ํ์ํ์ ์ค STT to TTS ์ํํ๋ ์์คํ ์ค๊ณ - 2. ์ค์๊ฐ STT์ ๋ฒ์ญ์ด ๊ฐ๋ฅํ ์์คํ ๊ตฌํ(+ FastAPI ๋ชจ๋ธ์๋น)
ํ๋ก์ ํธ ๋ชฉ์
์ฝ๋ ์๊ฐ์ ์์, ํด๋น ๊ธฐ๋ฅ์ด ํ์ํ๋ ํ๋ก์ ํธ์ ๋ชฉ์ ์ ๋ํด ๋จผ์ ์ค๋ช ํ๊ณ ์ ํ๋ค. ํ์ง ๊ณต์ฅ์ ๋ฌธ์ ํด๊ฒฐ์ ๋์ฑ ์ฉ์ดํ๊ฒ ๋ง๋ค๊ณ , ๋ณธ์ฌ์ ํ์ง ๊ณต์ฅ ๊ฐ ์์ฌ์ํต ์ฐจ์ด๋ฅผ ์ํ์ํฌ ์ ์๋ ์์คํ ์ ์ค๊ณํ์๋ค.
ํ์ง ๊ณต์ฅ์์๋ ๋ง์ฝ ๊ณต์ฅ ๋ด ์ด์๊ฐ ๋ฐ์ํ๋ฉด
1๋จ๊ณ: ์ด์ํ์ ๋ชจ๋ํฐ๋ง ๋ฐ ๊ฐ์ง
ํ์ฅ์์๋ ์์ฒด์ ์ผ๋ก ๋ฌธ์ ๊ฐ์ง ์์คํ ์ ๊ตฌ์ถํ์ฌ, ์ด์ํ์ ๋ฐ์ ์ ์ธ์ ๋ฐ์ํ ์ด๋ ํ ์ข ๋ฅ์ ๋ฌธ์ ์ธ์ง ํ์ง ๊ณต์ฅ์์ ํ์ธํ ์ ์๋๋ก ์๋ฆผ์ ์ ์กํ๋ค.
2๋จ๊ณ: ์ฑ๋ด์ ํตํ ๋ฌธ์ ํด๊ฒฐ
๋ฐ์ํ ๋ฌธ์ ์ํฉ๊ณผ ์ดฌ์ ์ด๋ฏธ์ง๋ฅผ ์ฑ๋ด์ ์ ๋ก๋ํ๋ฉด ์์คํ ์ด ๋ฌธ์ ์ํฉ์ ๋ถ์ํ๊ณ , RAG๋ฅผ ํตํด ์ด์ ์ ์ถ์ ํด๋ ์ ์ฌ ํด๊ฒฐ์ฌ๋ก, ํธ๋ฌ๋ธ์ํ ๋ชฉ๋ก, ๊ฐ์ข ๊ธฐ์ ์ง์นจ ๊ฐ์ด๋ ๋ฑ์ ์ฐธ๊ณ ํ์ฌ ๋ฌธ์ ํด๊ฒฐ ๋ฐฉ์์ ์ ๊ณตํ๋ค.
3๋จ๊ณ: ๋ณธ์ฌ์ ์๋ฆผ ์ ์ก
์ฑ๋ด์ผ๋ก๊น์ง ๋ฌธ์ ํด๊ฒฐ์ด ์ด๋ ต๋ค๋ฉด, ์ง๊ธ๊น์ง์ ๋ฌธ์ ๋ฐ์ ์ํฉ ๋ฐ ์ฑ๋ด ์๋ด ์ ๋ณด๋ฅผ ์ข ํฉํ์ฌ ์๋์ผ๋ก ๋ณธ์ฌ ๋ฐ ํ๋ ฅ์ ์ฒด์ ๋ฉ์ผ์ ์ ์กํ๋ค.
4๋จ๊ณ: ๋ณธ์ฌ์ ํ์ํ์
์ค์๊ฐ ํต์ญ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ ํ์ํ์๋ฅผ ํตํด ๋ณธ์ฌ์ ๋ฌธ์ ํด๊ฒฐ์ ์งํํ๋ค.
5๋จ๊ณ: ํ์๋ก ์๋์์ฑ
์ ์ฌ ๊ธฐ๋ฅ์ ํตํด ์๋์ผ๋ก ๊ธฐ๋ก๋๋ ์คํฌ๋ฆฝํธ๋ฅผ ์ข ํฉํ์ฌ ํ์๋ก์ ์์ฝ ์์ฑํ๋ค. ์ด๋ฅผ ํตํด ์ดํ์ ๋ฐ์ํ ์ ์๋ ๋ฌธ์ ์ ๋ํด ๋์ํ ์ ์๋ ๋๋ค๋ฅธ ๋ฐ์ดํฐ๋ฅผ ์ถ๊ฐํ๋ค.
์ด 5๋จ๊ณ ํ์ดํ๋ผ์ธ์ ํตํด ํ์ง ๊ณต์ฅ์ ์ธ์ด์ ์ฅ๋ฒฝ ์์ด ๋ณด๋ค ์ฝ๊ฒ ๋ฌธ์ ํด๊ฒฐ ๊ฐ์ด๋๋ฅผ ์ ๊ณต๋ฐ๊ณ , ๋ณธ์ฌ๋ ๋ถ๊ฐํผํ ํ๊ฒฌ ๊ทผ๋ฌด๋ฅผ ์ต์ํํ๋ฉด์ ํ์ฅ ๊ด๋ฆฌ๋ฅผ ํธํ๊ฒ ํ ์ ์๋ ํ๊ฒฝ์ ๋ง๋ค์ด์ฃผ๋ ๊ฒ์ด ๋ณธ ํ๋ก์ ํธ์ ๋ชฉ์ ์ด์๋ค.
๋๋ ์ด ์ค 4๋จ๊ณ์ธ ๋ณธ์ฌ์ ํ์ํ์ ์งํ์ ํต์ฌ ๊ธฐ๋ฅ์ ๋ด๋นํ์ฌ ๊ฐ๋ฐ์ ์งํํ๋ค.
์ ์ฒด ์งํ ํ๋ก์ฐ
๋ณธ ํ๋ก์ ํธ์ ์ ์ฒด ๊ตฌ์กฐ์ ๊ธฐ์ ์ ํ๋ฆ์ ๋ค์๊ณผ ๊ฐ๋ค.
GitHub Action์ ํ์ฉํ์ฌ ์ฝ๋ ๋ณ๋์ฌํญ์ด ์์ ๋ ์๋์ผ๋ก EC2 ์๋ฒ์ ๋ฐ์๋๋๋ก ํ๊ณ , ํฌ๊ฒ 7๊ฐ์ง์ ์๋ฒ๋ก ๊ตฌ์ฑํ์๋ค.
๋ณธ์ฌ์ ํ์ง๊ณต์ฅ์ ์ฃผ์ ๊ธฐ๋ฅ(ํ์ํ์ ๋ด ๋ค์ด๊ฐ๋ ๋ชจ๋ธ๋ค, ์ฑ๋ด, ์ด์ํ์ ํ์ง ๋ฑ๋ฑ)๋ค์ ์ ๋ถ fastAPI์ ๊ตฌ์ฑํ๊ณ ,
vue๋ ํ๋ก ํธ
MariaDB๋ ํ์๋ก ๋ฐ ํ์ํ์์ ์ ์์ ๋ณด๋ฅผ ์ ์ฅํด๋๋ ์ญํ ์ ํ๊ณ ,
Spring ์ญ์ ํ์ํ์ ์ ๋ณด๋ฅผ ์ ์ฅํ๋ ์ญํ ์ ํ๋๋ก ๊ตฌ์ฑํ๋ค.
๋ณธ ๊ธฐ๋ฅ ํ๋ฆ
๊ทธ ์ค ๋ด๊ฐ ์ค๊ณํ ํ๋ฆ์ ์์ ๊ฐ๋ค..
๊ตฌ์กฐ๊ฐ ์ฒ์์ ํ๋ก์ ํธ๋ฅผ ์ฌ์ ์ค๊ณํ๊ณ ๊ทธ์ ๋ง์ถฐ ๋ฏธ๋ํ๋ก์ ํธ๋ฅผ ์งํํ์ ๋์ ๋น๊ตํด์ ๋ง์ด ๋ฐ๋์๋ค.
1. ์์ฑ ์ ๋ฌ ๋ฐฉ์: ์ค์๊ฐ → ๋ น์ํ ๋ถ๋ถ๋ง
OpenVidu ๊ตฌ์กฐ ์ ๋ฌด๋ฃ ์ธํ๋ผ์์ ๋ฐ์ํ ์ ์๋ ํต์ ๋ถ์์ , ๋ น์ ์ค TTS ์์ฑ์ ๊ฐ์ ๊ณผ ๊ฐ์ ์์ธ์ผ๋ก ์ธํด ํ์ค์ ์ผ๋ก ์ข์ ํ์ง์ STT ๊ฒฐ๊ณผ๋ฅผ ๊ฐ์ ธ์ค๊ธฐ ์ํด์๋ ๋ น์ํ ๋ถ๋ถ๋ง ์ ๋ฌํด์ ์งํํ๋ ๊ฒ์ด ์ณ๋ค๊ณ ์๊ฐํ๋ค. ๊ทธ๋์ ๊ทธ๋ฅ ๋ น์๋ฒํผ์ ๋๋ฅด๊ณ ์ ์งํ ๋ ๊น์ง ๋ น์ํ ๋ฌธ์ฅ์ ๋๊ธฐ๋ ๋ฐฉ์์ผ๋ก ๋ณ๊ฒฝํ๋ค.
ํนํ ํ๋ก์ ํธ๋ฅผ ๋ด์ฃผ์ จ๋ ๊ต์๋๊ป์ ์์ฑ→์์ฑ ํต์ ์ ๊ฐ์กฐํ์ จ๋ค. ํ์ํ์ ํ์ฅ์์ ์๋ํ STS๋ฅผ ์ ์ฉํ๋ ค๋ฉด ์์ฑ์ ์ ๋ ฅํ๋ ๋ถ๋ถ์ด๋ ์ถ๋ ฅํ๋ ๋ถ๋ถ ๋ ์ค ํ๋๋ ๋ฐ๋์ ์ธ๊ฐ์ ๊ฐ์ ์ด ํ์ํ๋ฐ, ์๋ํ๋ฉด ์คํผ์ปค ํต์ ์ ์ถ๋ ฅ ์์ฑ์ด ๋ฐ๋์ ์ ๋ ฅ ์์ฑ์ ์์ผ๊ฒ์ด๋ผ ์๊ฐํ๋ค. ๋ฐ๋ผ์ ํ์๋ค๊ณผ ๋ ผ์ํด๋ณธ ๊ฒฐ๊ณผ ์์ฑ '์ถ๋ ฅ'์ด ์๋ํ๋์ด์ผ ํ๋ค๋ ๊ฒ์ด ์ข ๋ ๋ง๋ค๋ ๋ฐฉํฅ์ผ๋ก ์๊ฒฌ์ด ์ ๋ ธ๊ธฐ ๋๋ฌธ์, ๋ น์์ ์ธ๊ฐ์ ๊ฐ์ ์ด ๋ค์ด๊ฐ๋ ๋ฐฉํฅ์ผ๋ก ์์ ํ๋ค.
2. ํต์ ๋ฐฉ์: websocket → REST API
1๋ฒ์ ์ํด ์ ๋ฌ๋ฐฉ์๋ ์น์์ผ์ ํ์ฉํ ๋ฌด์ค๋จ ํต์ ๋ฐฉ์ ๋ณด๋จ ๋ฒํผ์ ๋๋ฌ ํธ๋ฆฌ๊ฑฐ๊ฐ ๋ฐ์ํด์ผ์ง ์์ฑ์ ์ ๋ฌํ๊ณ , ๊ทธ์ ๋ํ ์๋ต์ผ๋ก ์ ์ฌ์ ๋ฒ์ญ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ์ ธ์ค๋ RESTful๋ฐฉ์์ผ๋ก ๋ฐ๊ฟจ๋ค.
3. ๋น๋๊ธฐ์ ๊ตฌ์ฑ: ์์ฑ์ ๋ฐ์์ค๋ ๋ถ๋ถ์๋ง
REST API๋ก ํต์ ๋ฐฉ์์ ๋ฐ๊พธ์๊ธฐ ๋๋ฌธ์, ์น์์ผ ๊ตฌํ์์์ฒ๋ผ ์ ์ฌ/๋ฒ์ญ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ๋ณ์ ์ผ๋ก ์ ๋ฌํ ํ์์ฑ์ด ์์ด์ก๋ค. ๋ฐ๋ผ์ ํ ๋ฒ์ ์์ฒญ์ผ๋ก ์ ์ฌ๋๋ ํ ์คํธ์ ๋ฒ์ญ ์ ๋ณด ๋ฐ TTS ์์ฑ์ ๋ณด๋ฅผ ์์ ์ ์ผ๋ก ๋ฌถ์ด ๋ฐํํ ์ ์๋ ๊ตฌ์กฐ๋ก ์ ํํ๋ค.
๊ทธ๋ฌ๋ ์ฌ์ ํ ๋ค์ํ ์ฌ์ฉ์๋ค์ด ๋์์ ๋ง์ดํฌ๋ฅผ ๋๋ฅด๊ณ ์์ฑ์ ๋ ฅ์ ํ ์ ์๋ ํ์ ์ํฉ์ ๊ณ ๋ คํด ์์ฑ ์ ๋ ฅ๋งํผ์ ์ค๋ ๋๋ฅผ ํ์ฉํ ๋น๋๊ธฐ ์ฒ๋ฆฌ ๋ฐฉ์์ ์ ์งํ๋ค.
ํ์ผ ๊ตฌ์กฐ
headquater_system/
โโโ config.py # ์ ์ญ ์ค์ (API ํค, ์ค๋์ค ์ค์ , ์ ์ญ ํด๋ผ์ด์ธํธ ๋ฑ)
โโโ main.py # ํ๋ก๊ทธ๋จ์ ์ง์
์ : ๊ฐ ๋ชจ๋์ ๋ถ๋ฌ์ ์ค๋ ๋ ์คํ
โโโ modules/
| โโโ __init__.py
| โโโ stt.py # STT ์ฒ๋ฆฌ (Whisper API, VAD, ์ธ์ด ๊ฐ์ง)
| โโโ translation.py # ๋ฒ์ญ ์ฒ๋ฆฌ (GPT-4o-mini๋ฅผ ์ฌ์ฉ)
| โโโ tts.py # TTS ์ฒ๋ฆฌ (GPT-4o-mini-tts๋ฅผ ์ฌ์ฉ)
| โโโ users.py # ์ฌ์ฉ์ ์ ๋ณด ๊ฐ์ฒด, ์
๋ฐ์ดํธ ํจ์ ์ ์
| โโโ utils.py # ๊ณตํต ์ ํธ๋ฆฌํฐ ํจ์ (์ธ์ด ๋ณด์ , ๋ก๊ทธ ํ์ผ๋ช
์์ฑ, ๋์คํ๋ ์ด ์
๋ฐ์ดํธ ๋ฑ)
โโโ routers/
โโโ __init__.py
โโโ hq.py # ๋ผ์ฐํฐ ์ ๋ณด ์ค์
๋๋ถ๋ถ์ ๊ตฌ์กฐ์ ํจ์๋ ์ด์ ๊ณผ ๋์ผํ๋ค. ๋ค๋ง, ์ฌ๊ธฐ์๋ ์ฌ์ฉ์ ์ ๋ณด(์ด๋ฆ), ์ฌ์ฉ ์ธ์ด, ๋ฒ์ญ ์ธ์ด ๊ทธ๋ฆฌ๊ณ ์ฌ์ฉ์ ๋ณ ๋ฐํ ์ ๋ณด๋ฅผ ๊ฐ๋ณ์ ์ผ๋ก ๊ด๋ฆฌํ๊ธฐ ์ํด users.py๋ฅผ ์๋ก ์ ์ํด์ฃผ์๋ค.
์ฝ๋
Router ๋ณ ์ก์ ์ ๋ฆฌ
ํ๋ก ํธ๋ก๋ถํฐ ์ ๋ฌ๋ฐ๋ ๊ฐ์ฒด๋ STTPayload, ์ ๋ฌํด์ฃผ๋ ๊ฐ์ฒด๋ CombineResult์ list ํ์์ผ๋ก ์ ์ํ๋ค.
class STTPayload(BaseModel):
type: str
speakerInfo: dict
audioData: str
sampleRate: int
timestamp: int = None
class CombinedResult(BaseModel):
speaker: str
transcription: str
translation: str
tts_voice: str
class CombinedResultsResponse(BaseModel):
results: list[CombinedResult]
์ด๋ ๊ฒ ์ ์๋ ๊ฐ์ฒด ํํ์ POST ์์ฒญ์ด ๋ค์ด์ค๋ฉด ์ด๋ค ๊ฐ์ฒด๋ฅผ ๋ฐ๊ณ , ์ด๋ค ๊ฐ์ฒด๋ฅผ return ํด์ค์ง ๊น์ง์ ๋ชจ๋ ์ก์ ์ ์๋์ ๊ฐ์ด ์์ฑํด์ฃผ์๋ค. ํจ์๊ฐ ๊ธธ์ด์ ์กฐ๊ธ์ฉ ์๋ผ๋ณด์๋ค........
# 1. STT ๊ด๋ฆฌ ํจ์
@hq_router.post("/stt/audio", response_model=CombinedResultsResponse)
async def stt_audio_endpoint(payload: STTPayload):
print("[DEBUG] POST ์์ฒญ")
global date_log
# payload์ type ํ์ธ
if payload.type != "live_audio_chunk":
raise HTTPException(status_code=400, detail="Invalid payload type")
์ฌ๊ธฐ๊น์ง๊ฐ ๊ธฐ๋ณธ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ค๋ ๋ถ๋ถ์ด๊ณ , ๋ง์ฝ payload์ 'type' ์ ๋ณด๊ฐ ์๊ฑฐ๋ live_audio_chunk๊ฐ ์๋๋ผ๋ฉด ๋ ์ด์ ์งํํ์ง ๋ชปํ๊ฒ 404 exception์ ๋ฐ์์ํค๊ฒ ํ์๋ค.
# ์ฌ์ฉ์ ์ ๋ณด ์ถ์ถ
speaker_info = payload.speakerInfo
speaker_name = speaker_info.get("name", "Unknown")
source_lang = speaker_info.get("speakerLang", "ko")
target_lang = speaker_info.get("targetLang", "en")
session_id = speaker_info.get("sessionId", None) # Extract sessionId
print(f"{speaker_name}: src {source_lang}, tar {target_lang}, sessionId: {session_id}")
# ํ์์คํฌํ ์ฒ๋ฆฌ
timestamp = payload.timestamp if payload.timestamp is not None else int(time.time() * 1000)
if not date_log:
dt = datetime.fromtimestamp(timestamp / 1000)
date_log = dt.strftime("%Y%m%d_%H%M%S")
# REST ๋ฐฉ์์ด๋ฏ๋ก websocket์ None ์ฒ๋ฆฌ
user = get_or_create_user(speaker_name, source_lang, target_lang, websocket=None)
# ์ฌ์ฉ์๋ณ๋ก STT ์ฒ๋ฆฌ ์ค๋ ๋ ์คํ (์ต์ด ์ฐ๊ฒฐ ํ ์ฒ์ ๋ฐ์ดํฐ๋ฅผ ์์ ํ ๋ ์คํ)
if not user.processing_started:
threading.Thread(
target=stt_processing_thread,
args=(user,), # user ๋ด๋ถ์ audio_queue ๋ฑ ์ฌ์ฉ
daemon=True
).start()
user.processing_started = True
# audioData ๋์ฝ๋ฉ ๋ฐ PCM ๋ฐ์ดํฐ ๋ณํ (Int16 -> float32, ์ ๊ทํ, ๋ชจ๋
ธ ์ฌ๋ฐฐ์ด)
try:
raw_bytes = base64.b64decode(payload.audioData)
print(f"raw_bytes: {len(raw_bytes)}")
audio_np = np.frombuffer(raw_bytes, dtype=np.int16).astype(np.float32) / 32768.0
audio_np = audio_np.reshape(-1, 1)
except Exception as e:
raise HTTPException(status_code=400, detail=f"Audio data decoding error: {e}")
# ์ฌ์ฉ์ ๊ฐ์ฒด์ ์์ฑ ํ์ ๋ฐ์ดํฐ๋ฅผ ๋ฃ์
user.audio_queue.put((audio_np, payload.sampleRate))
์ดํ payload ๋ด ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ ์ฌ์ฉ์๋ฅผ ๋ฑ๋กํ๊ฑฐ๋ ๋ฉํ์ ๋ณด(์ฌ์ฉ ์ธ์ด/๋ฒ์ญํ ์ธ์ด)๋ฅผ ์ ๋ฐ์ดํธ ํด์ฃผ๊ณ , ์ฌ์ฉ์ ๋ณ STS ํ๋ก์ธ์ค ์ค๋ ๋๋ฅผ ๋ฐ์์ํจ ํ, ์ฌ์ฉ์ ๋ณ audio_queue์ ์์ฑ ์ ๋ณด๋ฅผ ๋ฃ์ด์ค๋ค.
# ๋ฐฑ๊ทธ๋ผ์ด๋ STT ๋ฐ ๋ฒ์ญ ์ฒ๋ฆฌ ์ค๋ ๋๊ฐ ์คํ ์ค์ด๋ผ๊ณ ๊ฐ์ ํ๊ณ ,
# ๊ฒฐ๊ณผ๊ฐ ์ค๋น๋์ด ์๋ค๋ฉด transcription_queue์ translated_queue์์ ๊บผ๋ด ๊ฒฐํฉ ๋ฉ์์ง๋ก ์์ฑ
combined_results = []
# ์ต๋ timeout์ด๋์ ๊ฒฐ๊ณผ๊ฐ ์์ฑ๋ ๋๊น์ง ๊ธฐ๋ค๋ฆฌ๋ ์์ (polling)
timeout = 120.0 # ์ต๋ ๋๊ธฐ ์๊ฐ
poll_interval = 0.2 # 200ms ๊ฐ๊ฒฉ์ผ๋ก ํด๋ง
waited = 0.0
while timeout > waited:
try:
# (stt ๊ฒฐ๊ณผ, ๋ฒ์ญ ๊ฒฐ๊ณผ, tts ์์ฑ(ogg -> base64๋ก ์ธ์ฝ๋ฉ))
transcription, translation, tts_voice = user.final_results_queue.get_nowait()
print(f"์ ์ฌ๊ฒฐ๊ณผ: {transcription}\n๋ฒ์ญ๊ฒฐ๊ณผ:{translation}")
combined_results.append({
"speaker": speaker_name,
"transcription": transcription,
"translation": translation,
"tts_voice": tts_voice
})
user.final_results_queue.task_done()
break # ๊ฒฐ๊ณผ๋ฅผ ๋ฐ์์ผ๋ฏ๋ก ์ข
๋ฃ
except queue.Empty:
# ๊ฒฐ๊ณผ๊ฐ ์์ง ์๋ค๋ฉด ์ ์ ๋๊ธฐ
await asyncio.sleep(poll_interval)
waited += poll_interval
return CombinedResultsResponse(results=combined_results)
๋ง์ง๋ง์ผ๋ก ๋ฌดํ๋ฃจํ๊ฐ ๋ฐ์๋๋ ๊ฒ์ ๋ฐฉ์งํ๊ธฐ ์ํด timeout(120์ด)์ ์ค์ ํ๊ณ ๊ทธ ์๊ฐ ์ด๋ด์ ์ ์ฌ/๋ฒ์ญ์ด ์๋ฃ๋๋ฉด ์ต์ข final_results_queue์์ ์ ์ฒด ๊ฐ(stt ๊ฒฐ๊ณผ, ๋ฒ์ญ ๊ฒฐ๊ณผ, ์ธ์ฝ๋ฉ๋ tts ์์ฑ)์ ๊ฐ์ ธ์ ํ๋ก ํธ๋ก ๋ค์ ์ ์กํด์ค๋ค.
์ ์ฒด ์คํ ํ๋ฉด์ ์ด๋ ๊ฒ ๋๋ค๐
User
# modules/user.py
import threading
import time
import queue
# ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ด๋ ํด๋์ค ์ ์
class User:
def __init__(self, name: str, source_lang: str = "ko", target_lang: str = "en", session_id: str = None):
self.name = name
self.source_lang = source_lang # ์ฌ์ฉ์๊ฐ ๋งํ๋ ์ธ์ด
self.target_lang = target_lang # ๋ฒ์ญํ ๋์ ์ธ์ด
self.last_update = time.time() # ๋ง์ง๋ง ์
๋ฐ์ดํธ ์๊ฐ ๋ฑ ์ถ๊ฐ ์ ๋ณด ๊ธฐ๋ก ๊ฐ๋ฅ
self.session_id = session_id # ์ธ์
ID ์ถ๊ฐ
self.detected_language = source_lang # ๊ฐ๋ณ ๊ฐ์ง ์ธ์ด
self.processing_started = False # ์ฒ๋ฆฌ ์ค๋ ๋ ์คํ ์ฌ๋ถ
# ์ฌ์ฉ์ ์ ์ฉ ํ๋ค
self.audio_queue = queue.Queue()
self.final_results_queue = queue.Queue()
self.websocket = None
def update(self, name: str = None, source_lang: str = None, target_lang: str = None, websocket=None, session_id: str = None):
if name:
self.name = name
if source_lang:
self.source_lang = source_lang
if target_lang:
self.target_lang = target_lang
if session_id:
self.session_id = session_id
# ์ WebSocket ๊ฐ์ฒด๊ฐ ์ ๊ณต๋๋ฉด ์
๋ฐ์ดํธ
if websocket is not None:
self.websocket = websocket
self.last_update = time.time()
# ์ ์ญ ์ฌ์ฉ์ ์ ์ฅ์ (๋์ ์ ๊ทผ์ ์ํด lock ์ฌ์ฉ)
users_lock = threading.Lock()
users = {}
def get_or_create_user(name: str, default_source: str = "ko", default_target: str = "en", websocket=None, session_id: str = None) -> User:
with users_lock:
if name in users:
user = users[name]
# ์ฌ์ฉ์ ์ ๋ณด ์
๋ฐ์ดํธ (WebSocket๋ ํจ๊ป ์
๋ฐ์ดํธ)
user.update(name=name, source_lang=default_source, target_lang=default_target, websocket=websocket, session_id=session_id)
return user
else:
user = User(name, default_source, default_target, session_id)
user.websocket = websocket
users[name] = user
print(f"[DEBUG] ์๋ก์ด ์ฌ์ฉ์๊ฐ ์ถ๊ฐ๋์์ต๋๋ค: {user.name}, ์ธ์
ID: {session_id}, ์ฐธ์ฌ์ธ์: {len(users)}")
return user
def get_user_by_connection(connection_id: str) -> User:
with users_lock:
return users.get(connection_id)
๋ง์ง๋ง๊น์ง ์ฌ์ฉ์ ๊ด๋ จ ํจ์๋ฅผ ๊ณ์ ๊ฑด๋๋ ค์ ์ฝ๋ ์ ๋ฆฌ๊ฐ ์ ์๋ ์ํ์ง๋ง..๐
์ด์ ์ on_event("startup")๊ณผ ๊ธฐํ ์ ์ญ ๋ณ์๋ก ์ ์ธํด์ฃผ์๋ ๋ถ๋ถ์ ์ฌ์ฉ์ ๋ณ๋ก ๊ตฌ์ฑํ๊ธฐ ์ํด class User์ ์ ๋ถ ์ ์ํด๋ ์ฝ๋์ด๋ค.
ํ ์คํธ๋ฅผ ์ํด ์ฌ์ฉ์ ๊ณ์ 5๊ฐ๋ฅผ ๋ฏธ๋ฆฌ ์ ์ํ์๊ณ , ์ฌ๋๋ง๋ค ์ฌ์ฉ ์ธ์ด๋ฅผ ๋ค๋ฅด๊ฒ ์ ์ํด์ฃผ์๋ค.
์ฐ์ ์ฐธ์ฌ ๊ณ์ ์ ๋๊ฐ๋ผ๋ ์ ์ ํ์ ํ๋ก ํธ ๋ ๋ฒจ์์ ๋ฏธํ ์ฐธ์ฌ์๋ค์ ๊ฐ์งํ๊ณ , ๋ด๊ฐ ์๋ ์๋ ์ฐธ์ฌ์์ ์ฌ์ฉ ์ธ์ด๋ฅผ ์ฐธ์กฐํ์ฌ ๋ฒ์ญํ ์ธ์ด๋ฅผ payload์ ๊ฐ์ด ๋ณด๋ด์ฃผ๊ธฐ ๋๋ฌธ์,
get_or_create_user ํจ์์์ ์ด๋ฏธ ์กด์ฌํ๋ ์ฌ์ฉ์๋ผ๋ฉด ๋ฒ์ญ ์ธ์ด(target language)๋ฅผ updateํ ์ ์๊ฒ ์ฝ๋๋ฅผ ๊ตฌ์ฑํด์ฃผ์๋ค.
๊ทธ๋์ !!๋ณ๋์ ์ธ์ด ์ ํ ๊ณผ์ ์์ด!!(๋๋ฆ ์๋๐) ์ด๋ ๊ฒ ์ฐธ์ฌํ๋ ์ฐธ๊ฐ์ ๋ณ๋ก ์๋๋ฅผ ์ธ์ํ์ฌ ๋ฒ์ญ๋๋ ์ธ์ด๊ฐ ๋ฌ๋ผ์ง๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
์ฌ์ค ์ ์ฌ์ ๊ฒฝ์ฐ whisper ์์ฒด๊ฐ ์ด๋์ ๋ ์ธ์ด๋ฅผ ์๋์ผ๋ก ๊ฐ์งํ๋ ๊ธฐ๋ฅ์ด ์กด์ฌํ๊ธฐ๋ ํ๊ณ , ์ด์ ์ ์ฌ์ฉํ๋ ์ธ์ด ์๋ ๊ฐ์ง ์ฝ๋๋ฅผ ํ์ฉํ๋ฉด ์ฌ์ฉ์์ ์ฌ์ฉ ์ธ์ด(source language)๋ ๊ตณ์ด ํ๋ก ํธ๋จ์์ ์ ๋ฌํด์ฃผ์ง ์์๋ ๋๋คใ
๋ฒ์ญํ ์ธ์ด๋ฅผ ์ค์ ํ๋๊ฒ ๋ ์ด๋ ค์ด ์์ ์ด๊ธฐ ๋๋ฌธ์, ์ถํ 3๋ช ์ด์์ด ํ์์ ์ฐธ์ฌํ ์ ์๋ค๊ณ ๊ฐ์ ํ๊ณ , ํ์ ์ฐธ์ฌ์๋ค์ ์ธ์ด๊ถ์ด 3๊ฐ ์ด์์ผ ๊ฒฝ์ฐ๋ฅผ ๊ณ ๋ คํ ์์ ๋ฐฉ์์ ์๊ฐ์ค์ด๋ค.
STS pipeline
def stt_processing_thread(user):
while True:
try:
# user.audio_queue์ (audio_np, sample_rate) ํํ์ ๋ฐ์ดํฐ๋ฅผ ๋ฃ์๋ค๊ณ ๊ฐ์
data_tuple = user.audio_queue.get(timeout=1)
try:
if isinstance(data_tuple, tuple):
data, sample_rate = data_tuple
else:
data = data_tuple
sample_rate = 16000 # ๊ธฐ๋ณธ๊ฐ์ 16000
# Optional: ์์ฑ ์ฒดํฌ (is_speech) – ํ์ผ ์ ์ฒด์ ๋ํด์ ์์ฑ์ ์ ๋ฌด๋ฅผ ํ๋จ
if len(data) < int(sample_rate * 0.5) or not is_speech(data):
print(f"[DEBUG] {user.name} - ์์ฑ ์์ ๋๋ ๋๋ฌด ์งง์ ๋ฐํ")
continue
text = stt_processing(user, data, sample_rate)
if not text:
print(f"[DEBUG] {user.name} - STT ๊ฒฐ๊ณผ ์์")
continue
print(f"[DEBUG] STT ๊ฒฐ๊ณผ: {text}")
# “Please transcribe exactly what you hear.” ์ ์๋ฌ ์ ๋ ๋ฉ์์ง์ด๋ฏ๋ก ์คํต
if text.strip().lower().startswith("please transcribe exactly what you hear"):
print(f"[DEBUG] {user.name} - ์๋ฌ ํ๋กฌํํธ ๊ฐ์ง, ์คํต")
continue
try:
translation = translation_process(user, text)
except Exception as te:
print(f"[DEBUG] {user.name} ๋ฒ์ญ ํธ์ถ ์ค ์ค๋ฅ: {te}", file=sys.stderr)
translation = ""
try:
tts_voice = tts_process(translation)
except Exception as te:
print(f"[DEBUG] {user.name} tts ํธ์ถ ์ค ์ค๋ฅ: {te}", file=sys.stderr)
tts_voice = ""
user.final_results_queue.put((text, translation, tts_voice))
finally:
user.audio_queue.task_done()
except queue.Empty:
continue
except Exception as e:
print(f"STT ์ฒ๋ฆฌ ์ค ์ค๋ฅ ๋ฐ์: {e}", file=sys.stderr)
์ฝ๋ ๊ตฌ์กฐ๊ฐ ๋ค์ ๋จ์ํด์ก๋ค.
๊ธฐ์กด์ thread์ฒ๋ผ ๊ตฌ์ฑ๋์ด ์๋ ๊ฐ ํ๋ก์ธ์ค๋ฅผ ์ผ๋ฐ ํจ์๋ก ๋ฐ๊ฟ์ฃผ๊ณ ,
์์ฑ ๋ฐ์ดํฐ๊ฐ ๋ค์ด์ค๋ฉด ๊ฐ ๋จ๊ณ๋ณ๋ก ํจ์๋ฅผ ์คํํ๊ณ , ํ ์คํธ ๊ฐ์ด ๋ณํ๋๋ฉด ํ๋๋ก ๋ฌถ์ด final_results_queue์ ๋ฐ์ดํฐ๋ฅผ ๋ฃ์ด์ฃผ๋ฉด ๋.
STT
def stt_processing(user, data, sample_rate):
try:
# ์์ ํ์ผ์ ์ ์ฅํ์ฌ STT ์ฒ๋ฆฌ (Whisper API ํธ์ถ)
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
sf.write(f.name, data, sample_rate, format='WAV', subtype='PCM_16')
# Whisper API ํธ์ถ
with open(f.name, "rb") as audio_file:
response = CLIENT.audio.transcriptions.create(
model="whisper-1",
file=audio_file,
language=user.source_lang,
prompt="We're now on meeting. Please transcribe exactly what you hear."
)
text = response.text.strip()
return text
except Exception as e:
print(f"STT ์ฒ๋ฆฌ ์ค ์ค๋ฅ ๋ฐ์: {e}", file=sys.stderr)
return ""
finally:
# ์์ธ ๋ฐ์ ์ฌ๋ถ์ ์๊ด์์ด ์์ ํ์ผ์ด ์๋ค๋ฉด ์ญ์ ํฉ๋๋ค.
if f.name and os.path.exists(f.name):
try:
os.unlink(f.name)
except Exception as del_e:
print(f"์์ ํ์ผ ์ญ์ ์ค๋ฅ: {del_e}", file=sys.stderr)
์ด์ ๋ณด๋ค ํจ์ฌ ๊ฐ๋จํ๊ฒ ์ฌ์ฉ์ ์ ๋ณด, ์ ๋ ฅ ์์ฑ, ์์ฑ์ rate(!!๋งค์ฐ ์ค์!!)๋ง ์ ๋ฌํ์ฌ ์ ์ฌ๋ ํ ์คํธ๋ฅผ ๋ฐํํ๋ ์ฝ๋๋ก ๋ฐ๊พธ์ด์ฃผ์๋ค.
๊ธฐ์กด์๋ openvidu์์ ์์ฑ์ ๋ฐ์์ค๊ณ ์ ํ์์ผ๋, ํ๋ฆฌํฐ์ด ํ๊ฒฝ์์๋ ์๋ฌด๋๋ 8GB ์ด์์ ์ต์ ์ฌ์์ ์๊ตฌํ๋ OpenVidu์ ํน์ฑ์ ์๋๋ฐฉ์ ์์ฑ/์์ ์คํธ๋ฆฌ๋ฐ์ด ์ ๋๋ก ์งํ์ด ๋์ง ์๋ ๋ฌธ์ ๊ฐ ์กด์ฌํ๋ค.
๋ฐ๋ผ์ ์ข ๋ ์์ ๋ ํ๊ฒฝ์์ ๋ณด์ฅ๋ ํ์ง์ ์์ฑ ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด ๋ก์ปฌ ๋ง์ดํฌ๋ฅผ ์ฌ์ฉํ์ฌ ๋ น์์ ์งํํ๋๋ก ๋์ด์๋๋ฐ,
๊ทธ๋ ๊ฒ ๋ ๊ฒฝ์ฐ ๋ก์ปฌ ๊ธฐ๊ธฐ๋ง๋ค ๋ง์ดํฌ์ ์ ๋ณด๊ฐ ๋ค๋ฅผ ์ ์๊ธฐ ๋๋ฌธ์ ์ค๋์ค์ ์ํ๋ ์ดํธ๋ฅผ ์ ๋ฌํด์ฃผ๋ ๊ฒ์ด ๋งค์ฐ ์ค์ํ๋ค.. ์ด๊ฑธ ์ฌ์ฉํด์ ์ธ์ฝ๋ฉ ๋์ฝ๋ฉ์ ์งํํ๋๋ฐ ์ด ๊ฐ์ด ๋ง์ง ์์ผ๋ฉด ์ ๋๋ก ๋ ์ค๋์ค๊ฐ ์ ๋ฌ๋์ง ์๋๋ค..๐ฅฒ
Translation
def translation_process(user, text):
try:
# ๋ง์ฝ ๋ฉ์์ง์ ์๋ณธ ์ธ์ด์ ์์ ์ ๋์ ์ธ์ด๊ฐ ๊ฐ๋ค๋ฉด ๋ฒ์ญํ์ง ์๊ณ ๊ทธ๋๋ก ์ ๋ฌ
if user.source_lang == user.target_lang:
print(f"[DEBUG] {user.name} ์์ค ์ธ์ด์ ํ๊ฒ ์ธ์ด๊ฐ ๋์ผํ์ฌ ๋ฒ์ญ ์์ด ์ ์ก")
return text
# ๋ฒ์ญ API ํธ์ถ (์์: GPT-4o-mini ๋ฒ์ญ ์์ฒญ)
try:
source_name = language_map.get(user.source_lang, "๊ฐ์ง๋ ์ธ์ด")
target_name = language_map.get(user.target_lang)
response = CLIENT.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": f"""You are a professional interpreter. When translating from {source_name} to {target_name},
follow these rules:
1. ์ ๋งคํ๊ฑฐ๋ ์คํด ์์ง๊ฐ ์์ผ๋ฉด → ๋ช
ํํ ์ฉ์ด๋ก ๊ณ ์ณ ๋ฒ์ญํ๋ค.
2. ์๋ชป๋ ๊ทผ๊ฑฐ ์ ๋ณด์ผ ๊ฒฝ์ฐ → ๋ถ๋๋ฝ๊ฒ ์ฌํ์ธํ๋๋ก ๋ฒ์ญํ๋ค.
3. ํธํ·๊ณต๊ฒฉ์ ๋ฐ์ธ์ผ ๊ฒฝ์ฐ → ๊ฑด์ค์ ์ธ ๋
ผ์ ๊ธฐํ๋ก ์ ํ๋๋๋ก ํค์ ์กฐ์ ํ๋ค.
4. ๋ฐ๋ณต·๋๋ ์ด ์ค์ธ ๋
ผ์์ผ ๊ฒฝ์ฐ → ํต์ฌ์ ์์ฝํด ์ฃผ์ ์ ์ด๋์ด๋ธ๋ค.
5. ๊ฐ์ ์ด ๊ฒฉํด์ง ๋ฐ์ธ์ผ ๊ฒฝ์ฐ → ์ค๋ฆฝ์ ์์ถฉ ์ญํ ์ ํ๋ฉฐ ๋ฒ์ญํ๋ค.
Translate exactly what they say, without any extra commentary."""},
{"role": "user", "content": text}
]
)
translation = response.choices[0].message.content.strip()
print(f"[DEBUG] {user.name} ๋ฒ์ญ ๊ฒฐ๊ณผ: {translation}")
except Exception as e:
print(f"[DEBUG] {user.name} ๋ฒ์ญ ์ค๋ฅ: {e}", file=sys.stderr)
translation = text
return translation
except Exception as e:
print(f"[DEBUG] {user.name} ๋ฒ์ญ ์ฒ๋ฆฌ ์ค ์ค๋ฅ ๋ฐ์: {e}", file=sys.stderr)
return text
๋ณธ ํจ์์ ํ๋กฌํํธ๊ฐ ์กฐ๊ธ ๋ ๊ธธ์ด์ก๋ค.
์ด์ ์๋ ๋จ์ํ ๋ฒ์ญ๋ง ์ํํ๋ผ๊ณ ์์ฒญํ์์ผ๋ ํด๋น ํ๋ก๊ทธ๋จ์ ์ค์ ์ ๋ฌด์์ ์ฌ์ฉํ๋ค๊ณ ๊ฐ์ ํ๊ณ , ๊ทธ ์ํฉ์์ ๋น์ฆ๋์ค ํต์ญ๊ฐ๊ฐ ์ง์ผ์ผ ํ๋ ๋น์ฆ๋์ค ๋งค๋์ ๋ฃฐ์ ๊ณ ๋ คํ์ฌ ํ๋กฌํํธ๋ฅผ ์์ ๊ฐ์ด ์์ ํด์ฃผ์๋ค.
์ฟ ํก์ ๋ ๋ ๋ค๋ฅธ ํ๊ตญ ์คํํธ์ ๋ค์์ ์ผํ๋ฉด์ ๋ ํ๊ณ ์๋ ๊ถ๊ธ์ฆ์ด ํ๋ ์์๋ค. "ํ์ฌ๊ฐ
์ฟ ํก์ ๋ ๋ ๋ค๋ฅธ ํ๊ตญ ์คํํธ์ ๋ค์์ ์ผํ๋ฉด์ ๋ ํ๊ณ ์๋ ๊ถ๊ธ์ฆ์ด ํ๋ ์์๋ค. "ํ์ฌ๊ฐ ์ปค์ง๋ฉด ์ํ ์๊ณ ๊ฐ์ ๋ฌผ๋ก ์ด๊ณ ๊ฐ ์กฐ์ง ๊ฐ์ ํฌ๊ณ ์์ ์ดํด๊ด๊ณ์ ์๋ ฅ๋คํผ์ด ๋ง์ฐํ๋ฉฐ ์ฑ์ฅ
kr.linkedin.com
TTS
def tts_process(translation):
print(f"[TTS] ํฉ์ฑํ ํ
์คํธ: {translation}")
try:
with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as temp_file:
temp_audio_path = Path(temp_file.name)
print(f"[DEBUG] TTS ์์ ํ์ผ ๊ฒฝ๋ก: {temp_audio_path}")
# TTS API ํธ์ถ (model="tts-1") – CLIENT.audio.speech.with_streaming_response.create ์ฌ์ฉ
with CLIENT.audio.speech.with_streaming_response.create(
model="tts-1", # TTS ์ฒ๋ฆฌ ๋ชจ๋ธ: tts-1
voice="nova", # ์ ํ ์ต์
(์ํ๋ ๋ชฉ์๋ฆฌ๋ก ์ค์ )
input=translation,
response_format="opus"
# instructions="Optional additional instructions" # ํ์ ์ ์ถ๊ฐ ์ง์นจ
) as response:
response.stream_to_file(temp_audio_path)
# ์์ฑ๋ ์์ฑ ํ์ผ์ binary ๋ชจ๋๋ก ์ฝ์ ํ base64๋ก ์ธ์ฝ๋ฉ
with open(str(temp_audio_path), "rb") as audio_file:
audio_bytes = audio_file.read()
base64_audio = base64.b64encode(audio_bytes).decode('utf-8')
print(f"[DEBUG] TTS ์์ฑ์ด base64๋ก ์ธ์ฝ๋ฉ๋จ (๊ธธ์ด: {len(base64_audio)} ๋ฌธ์)")
# ์์ ํ์ผ ์ญ์
os.unlink(str(temp_audio_path))
# tts_result_queue์ base64 ์ธ์ฝ๋ฉ ์์ฑ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅ
return base64_audio
except Exception as e:
print(f"TTS ์ค๋ฅ: {e}", file=sys.stderr)
return ""
์ ์ก ์์ฑ ํ์ ์ ์ง์ ํ ์๊ฐ ์๋ค. ๊ทผ๋ฐ ์ฌ๊ธฐ์ ์ค์ํ ๊ฒ์ mp3๋ base64 ์ธ์ฝ๋ฉ ์ 5์ด ๊ฐ๋์ ๋ฐํ๊ฐ ๋ฌด๋ ค 70KB๋ฅผ ๋๋๋ค .. ๊ทธ๋์ ์ฌ์ค ๋ฐ์ดํฐ๊ฐ ์ ์ก๋ ๋ ์๊พธ ๋ก๋ฉ์ด ๊ฑธ๋ฆฐ ํ ๋ฉ์์ง ์์ฒด๊ฐ ์นํ๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค ใ ใ
์ฐพ์๋ณด๋ ๋คํธ์ํฌ ์ฑ๋ฅ ๋ฌธ์ ์ธ๊ฑฐ๊ฐ์.. ๋ ์ ์ ์ฉ๋์ ์์ฑ ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํ๊ณ ์ ์น์์ผ ์คํธ๋ฆฌ๋ฐ ๋ฐ ํต์ ์ฉ์ ์ต์ ํ๋ ์ ์ง์ฐ·๊ณ ์์ถ ์ฝ๋ฑ์ธ opus ํฌ๋งท์ ์ฌ์ฉํด์ค๋ค.
์ด๋ฅผ openAI API์์ ์ค์ ํด์ค๋ response_format์ ์ง์ ํด์ฃผ๋ฉด ๋๋ค. ๊ธฐ๋ณธ ์ค์ ์ด mp3๋๊น, ์ ์ธํ๊ณ ์ง์ ํด์ฃผ๋ฉด ๋๋ค.
์ต์ ์ ๋ค์๊ณผ ๊ฐ๋ค.:
- MP3: The default response format for general use cases.
- Opus: For internet streaming and communication, low latency.
- AAC: For digital audio compression, preferred by YouTube, Android, iOS.
- FLAC: For lossless audio compression, favored by audio enthusiasts for archiving.
- WAV: Uncompressed WAV audio, suitable for low-latency applications to avoid decoding overhead.
- PCM: Similar to WAV but contains the raw samples in 24kHz (16-bit signed, low-endian), without the header.
๐ ์ฐธ๊ณ : https://platform.openai.com/docs/guides/text-to-speech
Opus(.ogg)๋ฅผ ์ฌ์ฉํ๋ฉด mp3 ๋๋น ํ ํฐ์ด 90% ๊ฐ๋ ์์ถ๋๋ ๊ฒ์ ํ์ธํ ์ ์์๋ค.
๊ทธ๋ฆฌ๊ณ ์ค์ ๋ก ํ์ํ์์์ ๋ค์ ๋ ํฌ๊ฒ ์์ง ์ฐจ์ด๋ ๋๋ผ์ง ๋ชปํ๋ค. ์ผํธ๐
๊ทธ๋ฌ๋ ๋ง์ฝ์ ํ์ฅ์ฑ์ ์๊ฐํ๋ค๋ฉด ์์ ํ์ผ์ ์ ๋ก๋ํ๋ค๊ฐ ๋ฏธํ ์ข ๋ฃ ํ ์ญ์ ํ๋ ๋ฐฉ์์ ๊ณ ๋ คํด๋ ๊ด์ฐฎ์ ๊ฒ ๊ฐ๋ค.
๊ฒฐ๊ณผ