์ด ๋ฌธ์๋ ์ค์๊ฐ ํ์ํ์์ ๋๋ณธ/์์ฝ ์์ฑ์ ์ด๊ดํ๋ OnMeet AI ๋ฐฑ์๋์ ์ํคํ ์ฒ ์งํ ๊ณผ์ , ๋น์ฉ ์ต์ ํ ์ค๊ณ, ๊ทธ๋ฆฌ๊ณ ๋ชจ๋ธ ์ ์ ๋ฐฐ๊ฒฝ์ ๋ํ ๋ฐํ/๋ฆฌ๋ทฐ์ฉ ๋ฐฑ์(Whitepaper)์ ๋๋ค.
AI ์๋น์ค ๊ตฌ์ถ์ ์ค์์ฉํ ์ ๋ฐ์ํ๋ ๊ณผ๊ธ ํญํ๊ณผ ํ์ ๋ถ๋ฆฌ ํ๊ณ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ค์๊ณผ ๊ฐ์ ๊ฑฐ๋ํ ํ์ดํ๋ผ์ธ ์งํ๋ฅผ ๊ฑฐ์ณค์ต๋๋ค.
flowchart TD
A["[์ด๊ธฐ โ] ํผํฉ ์์ฑ(Mix) ํต์ง STT ๋ณํ ์๋"]
B{"์น๋ช
์ ๋ฌธ์ : ๋๊ฐ ๋งํ๋์ง ์๋ณ ๋ถ๊ฐ (ํ์ ๋ถ๋ฆฌ ์ง์ ํ๊ณ)"}
C["[Phase 1] ์๋ฒฝํ ํ์ ๋ถ๋ฆฌ๋ฅผ ์ํด ๊ฐ ์ฐธ๊ฐ์๋ณ ํธ๋ ๊ฐ๋ณ ๊ตฌ๋
๋ฐ ๋ถํ STT"]
D{"์น๋ช
์ ๋ฌธ์ : '์นจ๋ฌต'๊น์ง STT๋์ด ํธ๋ ์(N)์ ๋น๋กํด ๊ณผ๊ธ N๋ฐฐ ํญ์ฆ"}
E["[Phase 2] ์ค๋์ค ๋น์ฉ ๊ทน์ฐ์ํฅ ๋ฐฉ์ด๋ฅผ ์ํ VAD (์์ฑ ๊ฐ์ง) ๋ฅ๋ฌ๋ ํํฐ ๋์
"]
F{"์ํคํ
์ฒ ์ค์ผ์ผ๋ง ๊ณ ๋ฏผ: VAD ๋ชจ๋ธ์ ๋น๋์ค ์๋ฒ์ ๋ ๊ฒ์ธ๊ฐ, AI ์๋ฒ์ ๋ ๊ฒ์ธ๊ฐ?"}
G["[์ต์ข
/Phase 3 โ
] AI ์๋ฒ ๊ฒ์ดํธ์จ์ด์ VAD ๋ด์ฅ ๋ฐฐ์น (MSA ๊ด์ฌ์ฌ ๋ถ๋ฆฌ/๋ฉ๋ชจ๋ฆฌ ๋ฐ ์ฝ๋ ๊ฐ๋
์ฑ ํ๋ณด)"]
A --> B
B -->|ํ์ ๋ถ๋ฆฌ์ ๋น์์ฑ ํ๋ณด| C
C --> D
D -->|์นจ๋ฌต ๊ตฌ๊ฐ ๊ณผ๊ธ ๋ฐฉ์ด| E
E --> F
F -->|์ต์ ์ ์ํคํ
์ฒ ์์ฌ๊ฒฐ์ | G
%% ๊ฐ์์ฑ์ ๊ทน๋ํํ๊ธฐ ์ํด ํ
๋ง๋ณ ๋ฐฑ๊ทธ๋ผ์ด๋ ๋์ , ๊ฐ๋ ฌํ ํ
๋๋ฆฌ ์์(Stroke)๋ง ์ง์
style A stroke:#ff4757,stroke-width:2px
style B stroke:#ff4757,stroke-dasharray: 5 5
style C stroke:#2ed573,stroke-width:2px
style D stroke:#ff4757,stroke-dasharray: 5 5
style E stroke:#1e90ff,stroke-width:2px
style F stroke:#ff4757,stroke-dasharray: 5 5
style G stroke:#5352ed,stroke-width:4px
1. ๋๋ณธ์ ๋ณธ์ง (ํต์ง ๋ฏน์ค STT์ ํฌ๊ธฐ)
- As-Is: ํธ๋ํฝ ๋น์ฉ ํ๋ฝ์ ์ํด ์นRTC์ ๋ชจ๋ ์ฌ์ฉ์ ์ค๋์ค๊ฐ ํฉ์ณ์ง(Mix) ์คํธ๋ฆผ 1๊ฐ๋ง์ STT๋ก ๋๊ธฐ๋ ค ํ์ต๋๋ค.
- Problem: ํ ๋ฒ ์์ฌ๋ฒ๋ฆฐ ์ค๋์ค๋ ํด๋ผ์ฐ๋ ๋ชจ๋ธ์ด ์๋ฌด๋ฆฌ ๋ฐ์ด๋๋ "์ ํํ ๋๊ฐ ์ด ๋ง์ ํ๋์ง(ํ์ ๋ถ๋ฆฌ)" ์๋ฒฝํ๊ฒ ์ชผ๊ฐ์ฃผ์ง ๋ชปํ๋ ์น๋ช ์ ์ธ ํ๊ณ์ ๋ถ๋ชํ์ต๋๋ค.
- ํด๊ฒฐ์ฑ : ํ์ ๋ถ๋ฆฌ์ ๋ํ ๋น์์ฑ๊ณผ ์ ํ์ฑ์ ์ต์ฐ์ ์ผ๋ก ํ๋ณดํ๊ธฐ ์ํด, ์ฐธ๊ฐ์ ๊ฐ์์ ์ค๋์ค ํธ๋์ ๋ฐ๋ก๋ฐ๋ก ๋ถ๋ฆฌ ๊ตฌ๋ ํ์ฌ ๊ฐ๋ณ STT๋ฅผ ํ๊ฒ ๊ตฌ์กฐ๋ฅผ ์ ๋ฉด ์์ ํ์ต๋๋ค.
2. ํธ๋ ํญ์ฆ์ ๋ฐ๋ฅธ ๋น์ฉ ์ฌ๋๊ณผ VAD ๋ฐฉ์ด๋ง
- Problem: ๊ฐ๋ณ ํธ๋ ๊ตฌ๋ ์ '2๋ช ์ด ๋งํ ๋ ์นจ๋ฌตํ๋ 3~4๋ช ์ ๋น ์ค๋์ค'๊น์ง ๋ชจ๋ ํด๋ผ์ฐ๋๋ก ์ ์ก๋๋ฉด์ STT API ๊ณผ๊ธ์ด ๋ถ๋น $0.006 ๋ฐฐ์๋ก ๋ฌดํ์ ํญ๋ฐํ๋ ํ๋ฉธ์ ๋น์ฉ ๊ตฌ์กฐ๊ฐ ๋๋ฌ๋ฌ์ต๋๋ค.
- ๋ฐฉ์ด ๋ก์ง: ๋น์ฉ ํญํ์ ๋๊ณ ์ ๋ฅ๋ฌ๋ ๊ธฐ๋ฐ์ **VAD ์ ์ฒ๋ฆฌ ํํฐ(
Silero ONNX)**๋ฅผ ์ฑํํด ์นจ๋ฌต์ ๊ณผ๊ฐํ ๋ฒ๋ฆฌ๊ณ "์๋ฏธ ์๋ ์ ์(Voice)" ์ฒญํฌ๋ง ์์๋ด๊ฒ ํจ์ผ๋ก์จ STT ๋น์ฉ์ ์ต์ 30%~70% ์ด์ ๋ฐฉ์ดํ์ต๋๋ค.
3. ์ํคํ ์ฒ ๋ฐฐ์น ์์ฌ๊ฒฐ์ (Why AI Service?)
- ์ด๋์ ๊ตฌํํ ๊ฒ์ธ๊ฐ?: VAD ํํฐ๋ฅผ ์์/์์ฑ ๋ผ์ฐํ
์ฃผ์ฒด์ธ
video-service์ ์ ๋ชฉํ ์ง, ์๋๋ฉดai-service์ ๋์ง ๊ณ ๋ฏผํ์ต๋๋ค. ๋น๋์ค ์๋ฒ์ ๋ฃ์ผ๋ฉด ๋ฏธ๋์ด ์ฒ๋ฆฌ์ AI ๋ฅ๋ฌ๋(ONNX) ์ค๋ ๋ ์ฐ์ฐ์ด ๋ค์์ฌ ๋ฉ๋ชจ๋ฆฌ ๊ณผ๋ถํ ์ฐ๋ ค๊ฐ ํฌ๋ฉฐ, ์ธํ๋ผ ์ฝ๋์ AI ๋ชจ๋ ํผ์ฌ๋ก ์ฝ๋ ๊ฐ๋ ์ฑ๊ณผ ์์ ๋์ด๋๊ฐ ๊ธ์ฆํ๋ ๋ฆฌ์คํฌ๊ฐ ์์์ต๋๋ค. - ๊ฒฐ๋ก :
video-service๋ ์์ ์ค๋์ค ์ฒญํฌ๋ฅผ ํ์ดํ๋ผ์ธ(Kafka)์ ๋น ๋ฅด๊ฒ ํผ๋ถ๋ ์์ ๋ฆด๋ ์ด ์ญํ ๋ง ๋งก๊ฒจ ์ฑ ์์ ์ ํํ๊ณ , AI ํน์ฑ์ VAD ๊ธฐ๋ฅ์ ๊ทธ ๋๋ฉ์ธ ์ฑ๊ฒฉ์ ์๋ฒฝํ ๋ถํฉํ๋ai-service์ ๊ตฌ(Gateway)์ ๋ฐฐ์นํ์ต๋๋ค. ์ด๋ฅผ ํตํด MSA ๊ด์ฌ์ฌ ๋ถ๋ฆฌ์ ๊ทน๊ฐ์ ์ฝ๋ ๊ฐ๋ ์ฑ์ ๊น๋ํ ๋ฌ์ฑํด ๋์ต๋๋ค. - ๐ก ์ฌํ ์ ๊ทํ (Audio Normalization): ์ถ๊ฐ๋ก VAD ๋ชจ๋ธ ๋์ ์ ๋ง์ดํฌ ๋ณผ๋ฅจ์ด ๋ฌ๋ผ๋ ์ค๋ฅ๋ฅผ ๋ด์ง ์๋๋ก, ํํ ์งํญ์ ๊ฐ์ ๋ก ํฝ์ฐฝ์์ผ ๋ฐธ๋ฐ์ค๋ฅผ ๋ง์ถ๋ **PCM ์ค๋์ค ์ ๊ทํ(Normalization)**๋ฅผ ๋ํด ์์ ์์ญ์๊น์ง๋ ๋๋ฝ ์์ด ๋ถ์ํ๋ ์ด์ ๋ฐ ํํฐ๋ง์ ๊ตฌ์ถํ์ต๋๋ค.
- As-Is: ํ์๊ฐ ๋๋ ๋๊น์ง ํ
์คํธ๋ฅผ ๋ชจ์๋ค๊ฐ S3 ํด๋ผ์ฐ๋ ์คํ ๋ฆฌ์ง์
.jsonํ์ผ ํ๋๋ก ์ฌ๋ ค๋ฒ๋ฆฌ๋ ์ด๊ธฐ ์ํคํ ์ฒ. - ์ต์ข
๋์
๋ชจ๋ธ (Database 1:N ์ ๊ทํ ์ ํ):
S3 ํด๋ผ์ฐ๋ ์
๋ก๋/๋ค์ด๋ก๋์์ ๋ฐ์ํ๋ ๋ณ๋ชฉ ์๊ฐ๊ณผ ํ์์ ๋ถํธํจ์ ์์ ๊ณ ์, RDBMS์ธ MySql์
transcript,transcript_event์์ ํ ์ด๋ธ ๋ชจ๋ธ๋ก **์์ ์ ๊ทํ(Normalization)**ํ์ฌ ์ฆ์ Insert ํ๋๋ก ๋ค๋ฐ๊ฟจ์ต๋๋ค. ๋๋ถ์ S3 ๋ ์ดํด์ 0ํ ๋ฐ ํฅํ ํ์๋ณ ํต๊ณ ํ์ฅ์ด ๊ฐ๋ฅํด์ก์ต๋๋ค.
- As-Is: LLM์ด ์ค๊ธ ํํ๋ก "ํ์ ๋ด์ฉ ์์ฝ์ ๋๋ค: ~" ์์ผ๋ก ๋ฑ์ด๋ด๋ ํ ์คํธ ํต์ง ์ ์ฌ.
- ์ต์ข
๋์
๋ชจ๋ธ (
Strict JSON๊ฐ์ ๋งคํ): ์์ฝ ์ ๋ฌธ ์ฒ๋ฆฌ๋ฅผ ๋ด๋นํ๋ Claude 3.5 Sonnet ๋ชจ๋ธ์ ํ๋กฌํํธ ์์ง๋์ด๋ง์ ํตํด ๊ฒฐ๊ณผ๋ฅผ ๋ฌด์กฐ๊ฑด์ ์ธ ์์ JSON ๊ฐ์ฒด ํฌ๋งท์ผ๋ก ์ถ๋ ฅํ๊ฒ ๋ง๋ค๊ณ , ์๋ฒ๊ฐ ์ญ์ง๋ ฌํํ์ฌ DB์keywords,action_items๋ฑ ์๋ก๊ฒ์ดํธ ์ปฌ๋ผ(Column)์ **'์์ ํ ์๋ฏธ์ ๊ตฌ์กฐํ ์ฒด๊ณ'**๋ฅผ ๊ฐ์ถฐ ์ ์ฌํ๋ ๋ฐ ์ฑ๊ณตํ์ต๋๋ค.
์ค๋์ค ํ์ดํ๋ผ์ธ์ด ์งํํจ์ ๋ฐ๋ผ, ๋จ์ํ ๋ฌด์๋ฏธํ ์ค๋์ค๋ฅผ ๋ฐ์ด ๋ฃ๋ ์ด๊ธฐ ๊ตฌ์กฐ์์ ๋น์ฉ๊ณผ ์ฑ๋ฅ์ ๊ทนํ์ผ๋ก ์ต์ ํํ Multi-LLM ์์๋ธ ์ฒด๊ณ๋ก ๋ชจ๋ธ๋ค์ด ๋ฐ์ ํ์ต๋๋ค.
| ์งํ ๋จ๊ณ (Phase) | ์ ํ๋ ๋ชจ๋ธ (๊ณผ๊ธ ํฌ์ธํธ) | ์ ์ฉ๋ ํต์ฌ ํน์ง (Features) | ๋์ ๊ฒฐ๊ณผ ๋ฐ ํ๊ณ์ (Results & Limits) |
|---|---|---|---|
| ๊ธฐ์กด (Initial) | Cloud STT 1 API |
์ ์ฒด ์ธ์์ ์ค๋์ค๊ฐ ์์ธ ํผํฉ(Mix) ์คํธ๋ฆผ 1๊ฐ๋ฅผ STT๋ก ํต์ง ๋ณํ ์์ฒญ | โ "๋๊ฐ ์ด๋ค ๋ง์ ํ๋์ง" ์๋ฒฝํ ํ์ ๋ถ๋ฆฌ ๋ถ๊ฐ๋ฅ โ ์นจ๋ฌต ๊ตฌ๊ฐ๋ ๊ณผ๊ธ๋์ด ๋น์ฉ ๋ญ๋น ์ฌ๊ฐ |
| ์ค๊ฐ (Intermediate) | whisper-1 +๊ฐ๋ณ ํธ๋ ๋ถํ |
์๋ฒฝํ ํ์ ๋ถ๋ฆฌ๋ฅผ ์ํด ์ฐธ๊ฐ์ ํธ๋ N๊ฐ๋ฅผ ๋ฐ๋ก ๋ถ๋ฆฌํ์ฌ ๊ฐ๊ฐ whisper-1 ํธ์ถ |
โ
์๋ฒฝํ ํ์ ๋ถ๋ฆฌ(Diarization) ํ๋ณด โ N๋ช ์ ๋ฌต์(Silence)๊น์ง ๋ถ๋ฆฌ ์ ์ก๋์ด ํด๋ผ์ฐ๋ ๊ณ ์ ๊ณผ๊ธ์ด N๋ฐฐ๋ก ํญ๋ฐ โ ํ๊ตญ์ด ์ธ์๋ฅ ๋ฐ ์๋ ์์ฌ์ |
| ์ต์ข (Final โ ) | Silero VADโฌ๏ธ gpt-4o-mini-transcribe |
[VAD ํํฐ๋ง + ์ค๋์ค ์ ๊ทํ] ๋ก์ปฌ ๋ฉ๋ชจ๋ฆฌ์์ ๊ฐ๋ฒผ์ด ๋ฅ๋ฌ๋ VAD ๋ชจ๋ธ์ด ๋ฌด์์ ํ๋ฝ์ํค๊ณ , ์ ํจ ์์ฑ๋ง ์ถ์ถํด ์ต์ ์ ์ ๋ ดํ ์ค๋์ค ํนํ ๋ชจ๋ธ๋ก ์ ์ก |
๐ ๋ฌด์ ํ๋ฝ์ ํตํด STT ํด๋ผ์ฐ๋ ๋น์ฉ ์ต๊ณ 70% ์ด์ ๊ทน์ ๋ฐฉ์ด ๐ VAD์ ์ค๋์ค ํํ ํฝ์ฐฝ(์ ๊ทํ) ์ ์ฒ๋ฆฌ๋ก ์ธ์ ์ ํ๋ ๊ทน๋ํ ๐ gpt-4o-mini-transcribe์ ์๋์ ํ๊ตญ์ด ์ธ์๋ฅ ๋ฐ ํ์/ํ์์คํฌํ ์ ํ์ฑ ๋ฌ์ฑ |
์ ์งํ ๊ณผ์ ์ ํต๊ณผํด ํ์ฌ ์ด์ ์ค์ธ ์ต์ ํ ํ์ดํ๋ผ์ธ์ ๊ฐ ๋ชจ๋ธ๋ณ ์์ธ ๋ฐฐ๊ฒฝ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
- ๋์ ๋ชฉ์ : ๊ณผ๊ธ API๋ฅผ ํฅํ ๋ถํ์ํ ํธ๋ํฝ(์นจ๋ฌต) ์ฐจ๋จ ๋ฐ ์ค๋์ค ์ ๊ทํ.
- ์ด์ ๋น์ฉ: ๋ฐฑ์๋์ ๋ด๋ถ
ONNX Runtimeํ๊ฒฝ์ ์ฌ์ฉํ๋ฏ๋ก, ๋คํธ์ํฌ I/O๋ ์ถ๊ฐ ํด๋ผ์ฐ๋ ์๊ธ์ด 0์์ ๋๋ค.
- ์ ํ ์ด์ (vs
whisper-1): ์ด๊ธฐ ํ ์คํธ ์whisper-1๋ชจ๋ธ์ ํ๊ตญ์ด ์ธ์๋ฅ ๋ฌธ์ ์ ํ์ ๋ถ๋ฆฌ(Diarization) ๊ธฐ๋ฅ์ ์์ฌ์์ด ์์์ต๋๋ค. ๋ฐ๋ฉด, ์ต๊ทผ ๋ฐํ๋gpt-4o-mini-transcribe๋ชจ๋ธ์ ํ๊ตญ์ด ํ์ ์ธ์ ๋ฐ ํ์์คํฌํ ๋ฌธ์ฅ ๋ถํด ๋ฅ๋ ฅ์ด ์๋์ ์ ๋๋ค. - ๐ฐ ์ฒ๋ฆฌ ์๋ ๋ฐ ๋น์ฉ ํจ์จ์ฑ:
whisper-1์ด ๋ฌด๊ฑฐ์ด ๋ถ ๋จ์ ๊ณ ์ ๊ณผ๊ธ์ ์๊ตฌํ๋ ๋ฐ๋ฉด, 4o-mini๋ ์ต์ ์ ๊ฒฝ๋ํ ์ํคํ ์ฒ๋ฅผ ์ ์ฉํ์ฌ ๋ฐ์ด๋ ํ๋ฆฌํฐ๋ฅผ ๊ฐ์ ธ๊ฐ์๋ ์ฒ๋ฆฌ ์๋(Latency)๊ฐ ๋น ๋ฅด๋ฉฐ, ๋๋ฑ ํน์ ๊ทธ ์ด์์ ๋น์ฉ(๋ฌ๋ฌ) ์ ๊ฐ ํจ๊ณผ๋ฅผ ๊ฐ์ ธ์ค๋ ํ์ดํ๋ผ์ธ ํต์ฌ ๋๋ ฅ์ ๋๋ค.
- ์ ํ ์ด์ (vs
GPT-4o): 1~2์๊ฐ ๋ถ๋์ ์์ฃผ ๊ธด ๋๋ณธ์ ์ฝ์ ์ ์๋ ๊ฑฐ๋ํ ์ปจํ ์คํธ(Context window) ์์ฉ๋ ฅ์ด ํ์์ ์ด์์ต๋๋ค. ๋ฌด์๋ณด๋ค ๋ค๋ฅธ ๊ธฐ๊ต ์์ด ์๋ฒ๊ฐ ์๊ตฌํ๋ ์๋ฒฝํ JSON ํฌ๋งท์ ๋ณํ ์์ด ํ์ฑํด ๋ด๋ ์ง์ ์ํ๋ ฅ(Strict Formatting) ๋ฉด์์ ํ ๋ชจ๋ธ ๋๋น ํ์ํ๊ฒ ์ฐ์ํ์ต๋๋ค. - ๐ฐ ์ด์ ๋น์ฉ ํจ์จ์ฑ:
์ต๊ณ ์ฑ๋ฅ์ธ
Claude 3.5 Sonnet์ 100๋ง ์ ๋ ฅ ํ ํฐ(1M Input tokens) ๋น ์ฝ $3 ์์ค์ผ๋ก ๋ฐฉ๋ํ ํ ์คํธ ์คํฌ๋ฆฝํธ๋ฅผ ๋ฐ์ด ๋ฃ๊ณ ํ ๋ฒ์ ๋ฝ์๋ด๋ ๋ฐ ์ ๊ฒฉ์ด๋ฉฐ, ๊ณ ๊ฐ์ ํ๊ฒ ๋ชจ๋ธ๋ค ๋๋น ํ๋ฅญํ ๊ฒฝ์ ์ ๋ง์ง์ ์ ์งํฉ๋๋ค.
์ค๋ฌด PPT๋ ๋ฆฌ๋ทฐ ์ ๋ค๋ฃจ๊ธฐ ์ข์ ํต์ฌ ์ฝ๋ ์์ญ์ ๋๋ค.
์ค์๊ฐ ํ์ํ์ ๋ฏธ๋์ด๋ฅผ ๋ผ์ฐํ
ํ๋ video-service๊ฐ ๋ฌด๊ฑฐ์ด AI ๋ฅ๋ฌ๋ ์ฐ์ฐ์ผ๋ก ์ธํด ๋ฉ์ถ๊ฑฐ๋ ์ง์ฐ๋์ง ์๋๋ก(Non-Blocking), **Kafka (๋น๋๊ธฐ ๋ฉ์์ง ํ)**๋ฅผ ๋์
ํ์ฌ ์๋ฒ ๊ฐ์ ๊ฒฐํฉ๋๋ฅผ ์์ ํ ๋์ด๋์ต๋๋ค.
// [video-service ๋ฐํ] KafkaMeetingEventPublisher.java
public void publishAudioSegmentReady(AudioChunkReadyEvent event) {
// 1. ํ์ ๋ฐฉ์์ ๋ฐ์ํ ์ฌ์ฉ์ ์์ฑ ์ฒญํฌ๋ฅผ Kafka Topic์ ๋์ง๊ณ ์ฆ์ ๋ณธ์
๋ณต๊ท (Fire & Forget)
publish("audio-chunk-ready", String.valueOf(event.getRoomId()), event);
}// [ai-service ์๋น] AudioChunkConsumer.java
@KafkaListener(topics = "audio-chunk-ready", groupId = "ai-service-group")
public void consume(String message) {
// 2. AI ์๋ฒ๋ ์์ ์ ๋ฆฌ์์ค ์ฒ๋ฆฌ ์๋์ ๋ง์ถฐ ์์ ํ๊ฒ ํ(Queue)์์ ์ด๋ฒคํธ๋ฅผ ๊บผ๋ด์ด(Pull) ๋ถ์ ์์
AudioChunkReadyEvent event = objectMapper.readValue(message, AudioChunkReadyEvent.class);
sttPipeline.processAudio(event);
}์ ๋ฌ ํฌ์ธํธ: ํ์ ํธ๋ํฝ์ด ํญ์ฃผํ๋๋ผ๋ ๋ฉ์ธ ํ์ ์๋ฒ์๋ ๋จ 1ms์ ๋๋ ์ด๋ ๋ฐ์ํ์ง ์์ต๋๋ค. ์๋ง ๊ฑด์ ์ด๋ฒคํธ๋ Kafka ๋ฒํผ(Topic) ๋ฉ๋ชจ๋ฆฌ์ ์์ ํ๊ฒ ์ผ๋ ฌ๋ก ์์ด๋ฉฐ, AI ์๋ฒ๋ ์์ ์ CPU ํ๊ณ ๋ด์์ ์ฐจ๋ก๋๋ก ์๋นํ๋ฏ๋ก **์ฅ์ ์ ํ ์ฐจ๋จ ๋ฐ ๊ทน๋์ ํ์ฅ์ฑ(Scalability)**์ด๋ผ๋ ๋ง์ดํฌ๋ก์๋น์ค(MSA)์ ์ง์ ํ ์ด์ ์ ์์ทจํ์ต๋๋ค.
๐ฏ ํต์ฌ ํฌ์ธํธ 2: ์์ธ๋๋ ์ฐธ๊ฐ์ ์๋ ํฌ์ฉ์ฑ(Accessibility) ํ๋ณด - ์์ฑ๊ณผ ์ฑํ ์ ์ค์๊ฐ ๋ณํฉ ์ ๋ ฌ (Redis ZSET)
ํ์ ์ค์๋ ๋์๊ด, ์คํ ์คํ์ด์ค ๋ฑ "์์ฑ์ ์ผค ์ ์๋(์ฑํ ์ ์ฉ)" ์ฐธ๊ฐ์๋ ํ์ฐ์ ์ผ๋ก ์กด์ฌํฉ๋๋ค. ์ด๋ค์ ์ฑํ ํ ์คํธ์ ๋ค๋ฅธ ์ฌ๋๋ค์ ์์ฑ(STT) ๋ํ๊ฐ ํ๋์ ๋งค๋๋ฌ์ด ๋งฅ๋ฝ(Context)์ผ๋ก ํ์๋ก์ ์์ฝ๋ ์ ์๋๋ก, ์์ ํ ๋ค๋ฅธ ์ฑ๊ฒฉ์ ๋ ์ด๋ฒคํธ(Voice / Chat)๋ฅผ ์๊ฐ์์ผ๋ก ๊ฟฐ๋งค๋ ๋ณํฉ ์์ ์ด ํ์์ ์ด์์ต๋๋ค.
์ด๋ฅผ ์ํด ๋ฌด์์(๋น๋๊ธฐ)๋ก ๋์ฐฉํ๋ ์์ธก์ ์กฐ๊ฐ๋ค์ 1๊ฐ์ ์ ํ์ ๋ํ๋ก์ผ๋ก ํฉ์น๋ ์กฐ์จ์๋ก **Redis Sorted Set(ZSET)**์ ์ ํํ์ต๋๋ค.
// RedisMeetingEventStore.java ์ ์์ฑ ๊ธฐ๋ก ๋ฉ์๋ (์ฑํ
์ธ appendChat ๋ ๋์ผํ ๋ก์ง ๊ณต์ )
public void appendVoice(VoiceSegmentCreatedEvent e) {
// 1. ๊ฐ๊ธฐ ๋ค๋ฅธ ์๊ฐ๋์ ๋ฐ์ํ ์ด์ง์ ๋ฐ์ดํฐ์ด๋ฏ๋ก, ๋ฐ์ํ Timestamp๋ฅผ ์ ๋ ์๊ณ(score)๋ก ํต์ผ
long score = e.getTimestamp() != null ? e.getTimestamp().toEpochMilli() : e.getSegmentStartMs();
long safeSeq = e.getSeq() != null ? e.getSeq() : 0L; // Unboxing NPE ์ทจ์ฝ์ ๋ฐฉ์ด์ฉ Fallback
// 2. "์์๋ฒํธ | ํ์
(VOICE or CHAT) | UUID | ์ฐธ์ฌ์์ด๋ฆ | JSON๋ฐ์ดํฐ" ํํ์ ํต์ผ๋ ๊ท๊ฒฉ ์กฐ๋ฆฝ
String member = padSeq(safeSeq) + "|VOICE|" + e.getSegmentId() + "|" + e.getParticipantName() + "|" + json;
// 3. ํ์์คํฌํ๋ฅผ score(์ค๋ฆ์ฐจ์ ๊ฐ์ค์น) ๊ฐ์ผ๋ก ์ฃผ์ด, ์ฝ์
์ฆ์ O(log(N))์ ์๋๋ก ์ ๋ ฌ ๋ณด๊ด (ZADD)
redis.opsForZSet().add(eventsKey(e.getRoomId()), member, (double) score);
}์ ๋ฌ ํฌ์ธํธ: ์์ฑ๊ณผ ํ ์คํธ๋ผ๋ ์ด์ง์ ์ธ ๋งค์ฒด๋ฅผ "์๊ฐ(score)" ์ด๋ผ๋ ๋จ์ผ ๊ธฐ์ค์ ์ผ๋ก ๋ฌถ์ด ํ๋์ ๋งฅ๋ฝ ํ๋ฆ์ผ๋ก ์์ฑ์ํต๋๋ค. ์ด Redis ZSET ์ธ๋ฉ๋ชจ๋ฆฌ ์ ๋ ฌ ๊ตฌ์กฐ ๋๋ถ์, ๋ง์ดํฌ๋ฅผ ์ผค ์ ์๋ ์ฐธ๊ฐ์์ ๊ท์คํ ์ฑํ ์๊ฒฌ๋ ์์ธ๋์ง ์๊ณ ๋น๋นํ ์ต์ข ํ์๋ก์ ์์ฝ ๊ฒฐ๊ณผ๋ฌผ(Claude LLM)์ ๊ธฐ์ฌํ๋ ์ง์ ํ ํฌ์ฉ์ฑ(Accessibility) ํตํฉ์ ์ด๋ค๋์ต๋๋ค.
์ ์ฅ์๊ฐ S3 ํด๋ผ์ฐ๋ ํ์ผ์์ ์์ ํ RDBMS(transcript_event ๋จ์ ํ
์ด๋ธ)๋ก ์ด์ ๋์์ ๋, ๊ธฐ์กด ์์ฉ ์ด์ ์๋น์ค ๋ค์ด์ ๋ง์ ์๋ช
์ ์ฝ๋์
๋๋ค.
public TranscriptDocument getTranscript(Long roomId) {
Optional<Transcript> tsOpt = transcriptRepository.findByRoomIdAndStatusCompleted(roomId);
if (tsOpt.isPresent()) {
// [์ ๊ท ์ค๊ณ]: DB์ ๋ ์ฝ๋๊ฐ ์๋ค๋ฉด ๋ฐ๋ก ๊บผ๋ด์ ์ง์ ๋ ๋๋ง
List<TranscriptEvent> dbEvents = transcriptEventRepository.findAll(...);
return renderer.renderFromDb(dbEvents);
} else {
// [์ค๋๋ ํ์ ๋ฐฉ์ด์ฉ]: DB ๋ฐ์ดํฐ๊ฐ ์๋ ๊ตฌ๋ฒ์ S3 ์์ ํ์ ์กฐํ๋ผ๋ฉด,
// Crash ๋์ง ์๊ณ ์ฆ์ ์์ ์ญ์ง๋ ฌํ ์กฐ๋ฆฝ๊ธฐ(Assembler)๋ฅผ ๊ฐ๋ํ์ฌ ์ฐํ ๋ฆฌํด.
return transcriptAssembler.loadTranscriptFallback(roomId);
}
}์ ๋ฌ ํฌ์ธํธ: ๋ผ์ด๋ธ ์ค์ธ ์ด์ DB๋ฅผ ๋ค์๋ ํ์๋ ๋งค์ฐ ์ํํฉ๋๋ค. DB๋ฅผ ์ฒซ ๋ฒ์งธ ์ต์ ์ผ๋ก ์กฐํํ๋, ์์ ๊ฒฝ์ฐ ๊ตฌ๋ฒ์ ํ์ดํ๋ผ์ธ(S3 ํ์ฑ)์ ํ๋๋ก ์ด์คํ๋ Fallback ์ฒด๊ณ๋ฅผ ์ฌ์ด ๋ฐฐํฌ ์์ ์ 100% ๋ฌด์ค๋จ ํธํ์ฑ์ ํ๋ณดํ ๊ท์คํ ๊ฒฝํ ํจํด์ ๋๋ค.
"AI ํ์ดํ๋ผ์ธ์ด ์ค์ ๋ก ์ฝ๋ ์์์ ์ด๋ป๊ฒ ๋ง๋ฌผ๋ ค ๋์๊ฐ๋๊ฐ?"์ ๋ํ 3๋ ํต์ฌ ๋ชจ๋ธ์ ์ค๋ํซ์ ๋๋ค.
์นจ๋ฌต์ ๊ฑธ๋ฌ๋ด๊ณ ๊ณผ๊ธ์ ๋ฐฉ์ดํ๋ ์ฒซ ๋ฒ์งธ ๊ด๋ฌธ์
๋๋ค. ๋ฐฑ์๋ ๋ฉ๋ชจ๋ฆฌ์์ ๊ฐ๋ฒผ์ด ONNX Runtime์ ๋๋ ค ์ค๋์ค ๋ฐฐ์ด์ ํํ์ ์ง์ ๊ณ์ฐํฉ๋๋ค.
// ๋ฅ๋ฌ๋ ์ถ๋ก ์ ๋๋ ค ํด๋น ์ค๋์ค ์ฒญํฌ์ ์์ฑ ํ๋ฅ (prob) ๊ณ์ฐ
float prob = runInference(window, state, sr);
if (prob >= threshold) {
// ์๊ณ๊ฐ(0.5) ์ด์์ด๋ฉด ์ ์๋ฏธํ ๋ฐํ(Speech) ๊ตฌ๊ฐ ์์์ผ๋ก ์บก์ฒ
inSpeech = true;
lastSpeechEndSample = currentSample + WINDOW_SIZE_SAMPLES;
} else if (inSpeech) {
long silenceDurationMs = ((currentSample - lastSpeechEndSample) * 1000L) / sampleRate;
// ์ง์ ๋ ์๊ฐ ์ด์ ์ฐ์์ผ๋ก ์นจ๋ฌตํ๊ฒ ๋๋ฉด, ์์ ์ ์(Voice) ๋ฉ์ด๋ฆฌ๋ฅผ ์น๋
์๋ผ๋ด์ด ๋ฆฌ์คํธ์
if (silenceDurationMs >= minSilenceDurationMs) {
segments.add(new SpeechSegment(startMs, endMs));
inSpeech = false;
}
}VAD๊ฐ ์์๊ฒ ์๋ผ์ค ์ ์(Voice Segment) ๋ฉ์ด๋ฆฌ๋ฅผ WebClient๋ฅผ ์ด์ฉํด ๊ฐ์ฅ ๋น ๋ฅด๊ณ ์ ๋ ดํ 4o-mini ๋ชจ๋ธ๋ก ์์ ํ ์คํธ๋ก ์นํํฉ๋๋ค.
MultipartBodyBuilder builder = new MultipartBodyBuilder();
// 1. VAD๊ฐ ์๋ผ๋ธ ์ ์ ์กฐ๊ฐ(audioBytes) ์ฝ์
builder.part("file", new ByteArrayResource(audioBytes) { ... }, MediaType.valueOf(mimeType));
// 2. ๊ฐ์ฑ๋น/์ธ์๋ฅ ๋ํ์ ์ค๋์ค ์ ์ฉ ๋ชจ๋ธ ํธ์ถ
builder.part("model", "gpt-4o-mini-transcribe");
builder.part("response_format", "text");
return webClient.post()
.uri(apiUrl)
.header("Authorization", "Bearer " + apiKey)
.body(BodyInserters.fromMultipartData(builder.build()))
.retrieve()
.bodyToMono(String.class) // ๋น ๋ฅธ ๋น๋๊ธฐ(Mono) ํ
์คํธ ํ๋
.block();๊ฑฐ๋ํ ๋๋ณธ ํ ์คํธ๋ฅผ ๋์ ธ์ฃผ๊ณ , ์ฐ๋ฆฌ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง์ 1:1๋ก ์ผ์นํ๋ JSON ๊ฐ์ฒด๋ก ๊ฐ๋ ฅํ๊ฒ ํฌ๋งทํ ํ๋ ๋จ๊ณ์ ๋๋ค.
// 1. ํ๋กฌํํธ ์์ง๋์ด๋ง: "์์ JSON ํฌ๋งท ์ด์ธ์ ํ
์คํธ๋ ์ ๋ ๊ธ์ง"
String prompt = String.format("""
You MUST output ONLY a valid JSON object matching this exact schema:
{
"description": "Overall summary of the meeting",
"keywords": ["important", "keywords"],
"decisions": ["Any decisions made"],
"actionItems": ["Who needs to do what"]
}
Transcript:
%s""", transcript);
// 2. ์๋ต ํ๋ ํ ํ์ฑ ๊ฒ์ฆ (Validation) ์ ์ฐจ
String text = extractContent(responseBody);
// Claude๊ฐ ๋ด๋ฑ์ JSON ์คํธ๋ง์ด ์ฐ๋ฆฌ ๋ด๋ถ DTO ๊ท๊ฒฉ๊ณผ ๋ง์ง ์์ผ๋ฉด ์ฆ์ ํ์ฑ ํฌ๋์(Exception) ์ ๋
// ์ด๋ฅผ ํต๊ณผํด์ผ๋ง ์๋ฒฝํ ์ ๊ทํ DB ์ปฌ๋ผ์ ์ฝ์
๋ ์๊ฒฉ์ด ์ฃผ์ด์ง
om.readValue(text, SummaryResult.class); ์ด๋ฒ AI ์๋น์ค ๊ตฌ์ถ์ ๋จ์ํ "OpenAI ์ฐ๋"์ ๊ทธ์น์ง ์๊ณ , ์๋์ ๊ฐ์ ๊ฐ์น ์ฐฝ์ถ์ ์ด๋ฃจ์ด๋์ต๋๋ค:
- ์ง๊ฐ์ ์งํจ ์ํคํ
์ฒ: ์นจ๋ฌต์ผ๋ก ๋๊ฐ๋ ๋ฌด์ง์ฑ ํด๋ผ์ฐ๋ ๊ณผ๊ธ์ ๋ก์ปฌ
VAD Onnx๋์ ๊ณผPCM ์ค๋์ค ์ ๊ทํ๋ฅผ ํตํด ์๋ฒฝํ ๋ฐฉ์ด. - ๋ค์ค LLM ์กฐ์จ ์์๋ธ ์ฒด๊ณ: ๋ก์ปฌ ์ต๋จ ๋๋ ์ด(VAD) + ์ค๋์ค ์ฒญ์ทจ ํนํ(
gpt-4o-mini) + NLP ๊ตฌ์กฐํ ํนํ(Claude) ๋ฑ ์ํฉ์ ๋ง๋ ๊ฐ์ฅ ๋พฐ์กฑํ๊ณ ์ ๋ ดํ ๋ฌด๊ธฐ 3๊ฐ์ง๋ฅผ ์กฐ๋ฆฝํด ๊ฐ์ฑ๋น ์ต์์ ํ์ดํ๋ผ์ธ ๊ฐ์ค. - ๊ธฐ์ ๋ถ์ฑ ์๋ ํผํผํ ๋ง์ด๊ทธ๋ ์ด์
: ์๋ ํ์์ ์๋ฌธ S3 ์ ๊ทผ๊ณผ ์ ๊ท ์ ๊ทํ DB ์กฐํ๋ฅผ ํ๋์ API(
Fallback)๋ก ํฌ์ฅ์์ผ ๊ธฐ์กด ์ฌ์ฉ์ ๊ฒฝํ์ ์จ์ ํ ์ง์ผฐ์ต๋๋ค.