Skip to content

Latest commit

ย 

History

34 Commits

Folders and files

NameName
Last commit message
Last commit date
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

๐ŸŽ™๏ธ OnMeet AI ํŒŒ์ดํ”„๋ผ์ธ ์•„ํ‚คํ…์ฒ˜ ๋ฐ ์ง„ํ™” ์Šคํ† ๋ฆฌ

์ด ๋ฌธ์„œ๋Š” ์‹ค์‹œ๊ฐ„ ํ™”์ƒํšŒ์˜์˜ ๋Œ€๋ณธ/์š”์•ฝ ์ƒ์„ฑ์„ ์ด๊ด„ํ•˜๋Š” OnMeet AI ๋ฐฑ์—”๋“œ์˜ ์•„ํ‚คํ…์ฒ˜ ์ง„ํ™” ๊ณผ์ •, ๋น„์šฉ ์ตœ์ ํ™” ์„ค๊ณ„, ๊ทธ๋ฆฌ๊ณ  ๋ชจ๋ธ ์„ ์ • ๋ฐฐ๊ฒฝ์— ๋Œ€ํ•œ ๋ฐœํ‘œ/๋ฆฌ๋ทฐ์šฉ ๋ฐฑ์„œ(Whitepaper)์ž…๋‹ˆ๋‹ค.


1. ๐Ÿš€ ๋„์ž…: ์•„ํ‚คํ…์ฒ˜ ์ง„ํ™” ์—ญ์‚ฌ์™€ ์‹œ๊ฐํ™” (Evolution Flow)

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
Loading

[์ง„ํ™”์˜ ๊ตฌ์ฒด์  ๋งฅ๋ฝ๊ณผ ๋ผˆ๋ฅผ ๊นŽ๋Š” ์˜์‚ฌ๊ฒฐ์ •]

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)**๋ฅผ ๋”ํ•ด ์ž‘์€ ์†์‚ญ์ž„๊นŒ์ง€๋„ ๋ˆ„๋ฝ ์—†์ด ๋ถ„์„ํ•˜๋Š” ์ดˆ์ •๋ฐ€ ํ•„ํ„ฐ๋ง์„ ๊ตฌ์ถ•ํ–ˆ์Šต๋‹ˆ๋‹ค.

[Phase 2: ์›์‹œ ๋Œ€๋ณธ ๋ฌถ์Œ์—์„œ ๊ตฌ์กฐํ™”๋œ DB๋กœ]

  • As-Is: ํšŒ์˜๊ฐ€ ๋๋‚  ๋•Œ๊นŒ์ง€ ํ…์ŠคํŠธ๋ฅผ ๋ชจ์•˜๋‹ค๊ฐ€ S3 ํด๋ผ์šฐ๋“œ ์Šคํ† ๋ฆฌ์ง€์— .json ํŒŒ์ผ ํ•˜๋‚˜๋กœ ์˜ฌ๋ ค๋ฒ„๋ฆฌ๋˜ ์ดˆ๊ธฐ ์•„ํ‚คํ…์ฒ˜.
  • ์ตœ์ข… ๋„์ž… ๋ชจ๋ธ (Database 1:N ์ •๊ทœํ™” ์ „ํ™˜): S3 ํด๋ผ์šฐ๋“œ ์—…๋กœ๋“œ/๋‹ค์šด๋กœ๋“œ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ๋ณ‘๋ชฉ ์‹œ๊ฐ„๊ณผ ํƒ์ƒ‰์˜ ๋ถˆํŽธํ•จ์„ ์—†์• ๊ณ ์ž, RDBMS์ธ MySql์˜ transcript, transcript_event ์ž์‹ ํ…Œ์ด๋ธ” ๋ชจ๋ธ๋กœ **์™„์ „ ์ •๊ทœํ™”(Normalization)**ํ•˜์—ฌ ์ฆ‰์‹œ Insert ํ•˜๋„๋ก ๋’ค๋ฐ”๊ฟจ์Šต๋‹ˆ๋‹ค. ๋•๋ถ„์— S3 ๋ ˆ์ดํ„ด์‹œ 0ํ™” ๋ฐ ํ–ฅํ›„ ํ™”์ž๋ณ„ ํ†ต๊ณ„ ํ™•์žฅ์ด ๊ฐ€๋Šฅํ•ด์กŒ์Šต๋‹ˆ๋‹ค.

[Phase 3: ๋ฌด์ง€์„ฑ ํ…์ŠคํŠธ์—์„œ ๊ฐ•์ œ์  ๊ตฌ์กฐํ™”๋กœ]

  • As-Is: LLM์ด ์ค„๊ธ€ ํ˜•ํƒœ๋กœ "ํšŒ์˜ ๋‚ด์šฉ ์š”์•ฝ์ž…๋‹ˆ๋‹ค: ~" ์‹์œผ๋กœ ๋ฑ‰์–ด๋‚ด๋Š” ํ…์ŠคํŠธ ํ†ต์งœ ์ ์žฌ.
  • ์ตœ์ข… ๋„์ž… ๋ชจ๋ธ (Strict JSON ๊ฐ•์ œ ๋งคํ•‘): ์š”์•ฝ ์ „๋ฌธ ์ฒ˜๋ฆฌ๋ฅผ ๋‹ด๋‹นํ•˜๋Š” Claude 3.5 Sonnet ๋ชจ๋ธ์˜ ํ”„๋กฌํ”„ํŠธ ์—”์ง€๋‹ˆ์–ด๋ง์„ ํ†ตํ•ด ๊ฒฐ๊ณผ๋ฅผ ๋ฌด์กฐ๊ฑด์ ์ธ ์ˆœ์ˆ˜ JSON ๊ฐ์ฒด ํฌ๋งท์œผ๋กœ ์ถœ๋ ฅํ•˜๊ฒŒ ๋งŒ๋“ค๊ณ , ์„œ๋ฒ„๊ฐ€ ์—ญ์ง๋ ฌํ™”ํ•˜์—ฌ DB์˜ keywords, action_items ๋“ฑ ์„œ๋กœ๊ฒŒ์ดํŠธ ์ปฌ๋Ÿผ(Column)์— **'์™„์ „ํ•œ ์˜๋ฏธ์  ๊ตฌ์กฐํ™” ์ฒด๊ณ„'**๋ฅผ ๊ฐ–์ถฐ ์ ์žฌํ•˜๋Š” ๋ฐ ์„ฑ๊ณตํ–ˆ์Šต๋‹ˆ๋‹ค.

2. ๐Ÿง  ํ•ต์‹ฌ ๋ชจ๋ธ ์„ ์ • ๊ณผ์ • ๋ฐ ์ง„ํ™” ๋น„๊ตํ‘œ (Why this model?)

์˜ค๋””์˜ค ํŒŒ์ดํ”„๋ผ์ธ์ด ์ง„ํ™”ํ•จ์— ๋”ฐ๋ผ, ๋‹จ์ˆœํžˆ ๋ฌด์˜๋ฏธํ•œ ์˜ค๋””์˜ค๋ฅผ ๋ฐ€์–ด ๋„ฃ๋˜ ์ดˆ๊ธฐ ๊ตฌ์กฐ์—์„œ ๋น„์šฉ๊ณผ ์„ฑ๋Šฅ์„ ๊ทนํ•œ์œผ๋กœ ์ตœ์ ํ™”ํ•œ Multi-LLM ์•™์ƒ๋ธ” ์ฒด๊ณ„๋กœ ๋ชจ๋ธ๋“ค์ด ๋ฐœ์ „ํ–ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ“Š [Phase๋ณ„] ์Œ์„ฑ ํŒŒ์ดํ”„๋ผ์ธ ๋ชจ๋ธ ์ง„ํ™” ์š”์•ฝํ‘œ

์ง„ํ™” ๋‹จ๊ณ„ (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์˜ ์••๋„์  ํ•œ๊ตญ์–ด ์ธ์‹๋ฅ  ๋ฐ ํ™”์ž/ํƒ€์ž„์Šคํƒฌํ”„ ์ •ํ™•์„ฑ ๋‹ฌ์„ฑ

์œ„ ์ง„ํ™” ๊ณผ์ •์„ ํ†ต๊ณผํ•ด ํ˜„์žฌ ์šด์˜ ์ค‘์ธ ์ตœ์ ํ™” ํŒŒ์ดํ”„๋ผ์ธ์˜ ๊ฐ ๋ชจ๋ธ๋ณ„ ์ƒ์„ธ ๋ฐฐ๊ฒฝ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

โ‘  ์‹ค์‹œ๊ฐ„ ์ „์ฒ˜๋ฆฌ ๊ฒŒ์ดํŠธ์›จ์ด: Silero VAD (ONNX)

  • ๋„์ž… ๋ชฉ์ : ๊ณผ๊ธˆ API๋ฅผ ํ–ฅํ•œ ๋ถˆํ•„์š”ํ•œ ํŠธ๋ž˜ํ”ฝ(์นจ๋ฌต) ์ฐจ๋‹จ ๋ฐ ์˜ค๋””์˜ค ์ •๊ทœํ™”.
  • ์šด์˜ ๋น„์šฉ: ๋ฐฑ์—”๋“œ์˜ ๋‚ด๋ถ€ ONNX Runtime ํ™˜๊ฒฝ์„ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ, ๋„คํŠธ์›Œํฌ I/O๋‚˜ ์ถ”๊ฐ€ ํด๋ผ์šฐ๋“œ ์š”๊ธˆ์ด 0์›์ž…๋‹ˆ๋‹ค.

โ‘ก STT (์Œ์„ฑ ์ธ์‹): gpt-4o-mini-transcribe (OpenAI API)

  • ์„ ํƒ ์ด์œ  (vs whisper-1): ์ดˆ๊ธฐ ํ…Œ์ŠคํŠธ ์‹œ whisper-1 ๋ชจ๋ธ์€ ํ•œ๊ตญ์–ด ์ธ์‹๋ฅ  ๋ฌธ์ œ์™€ ํ™”์ž ๋ถ„๋ฆฌ(Diarization) ๊ธฐ๋Šฅ์— ์•„์‰ฌ์›€์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๋ฐ˜๋ฉด, ์ตœ๊ทผ ๋ฐœํ‘œ๋œ gpt-4o-mini-transcribe ๋ชจ๋ธ์€ ํ•œ๊ตญ์–ด ํ™”์ž ์ธ์‹ ๋ฐ ํƒ€์ž„์Šคํƒฌํ”„ ๋ฌธ์žฅ ๋ถ„ํ•ด ๋Šฅ๋ ฅ์ด ์••๋„์ ์ž…๋‹ˆ๋‹ค.
  • ๐Ÿ’ฐ ์ฒ˜๋ฆฌ ์†๋„ ๋ฐ ๋น„์šฉ ํšจ์œจ์„ฑ: whisper-1์ด ๋ฌด๊ฑฐ์šด ๋ถ„ ๋‹จ์œ„ ๊ณ ์ • ๊ณผ๊ธˆ์„ ์š”๊ตฌํ–ˆ๋˜ ๋ฐ˜๋ฉด, 4o-mini๋Š” ์ตœ์‹ ์˜ ๊ฒฝ๋Ÿ‰ํ™” ์•„ํ‚คํ…์ฒ˜๋ฅผ ์ ์šฉํ•˜์—ฌ ๋›ฐ์–ด๋‚œ ํ€„๋ฆฌํ‹ฐ๋ฅผ ๊ฐ€์ ธ๊ฐ์—๋„ ์ฒ˜๋ฆฌ ์†๋„(Latency)๊ฐ€ ๋น ๋ฅด๋ฉฐ, ๋™๋“ฑ ํ˜น์€ ๊ทธ ์ด์ƒ์˜ ๋น„์šฉ(๋‹ฌ๋Ÿฌ) ์ ˆ๊ฐ ํšจ๊ณผ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ํŒŒ์ดํ”„๋ผ์ธ ํ•ต์‹ฌ ๋™๋ ฅ์ž…๋‹ˆ๋‹ค.

โ‘ข NLP (๊ตฌ์กฐ์  ์š”์•ฝ): Claude 3.5 Sonnet (Anthropic API)

  • ์„ ํƒ ์ด์œ  (vs GPT-4o): 1~2์‹œ๊ฐ„ ๋ถ„๋Ÿ‰์˜ ์•„์ฃผ ๊ธด ๋Œ€๋ณธ์„ ์ฝ์„ ์ˆ˜ ์žˆ๋Š” ๊ฑฐ๋Œ€ํ•œ ์ปจํ…์ŠคํŠธ(Context window) ์ˆ˜์šฉ๋ ฅ์ด ํ•„์ˆ˜์ ์ด์—ˆ์Šต๋‹ˆ๋‹ค. ๋ฌด์—‡๋ณด๋‹ค ๋‹ค๋ฅธ ๊ธฐ๊ต ์—†์ด ์„œ๋ฒ„๊ฐ€ ์š”๊ตฌํ•˜๋Š” ์™„๋ฒฝํ•œ JSON ํฌ๋งท์„ ๋ณ€ํ˜• ์—†์ด ํŒŒ์‹ฑํ•ด ๋‚ด๋Š” ์ง€์‹œ ์ˆ˜ํ–‰๋ ฅ(Strict Formatting) ๋ฉด์—์„œ ํƒ€ ๋ชจ๋ธ ๋Œ€๋น„ ํƒ์›”ํ•˜๊ฒŒ ์šฐ์ˆ˜ํ–ˆ์Šต๋‹ˆ๋‹ค.
  • ๐Ÿ’ฐ ์šด์˜ ๋น„์šฉ ํšจ์œจ์„ฑ: ์ตœ๊ณ  ์„ฑ๋Šฅ์ธ Claude 3.5 Sonnet์€ 100๋งŒ ์ž…๋ ฅ ํ† ํฐ(1M Input tokens) ๋‹น ์•ฝ $3 ์ˆ˜์ค€์œผ๋กœ ๋ฐฉ๋Œ€ํ•œ ํ…์ŠคํŠธ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ๋ฐ€์–ด ๋„ฃ๊ณ  ํ•œ ๋ฒˆ์— ๋ฝ‘์•„๋‚ด๋Š” ๋ฐ ์ œ๊ฒฉ์ด๋ฉฐ, ๊ณ ๊ฐ€์˜ ํƒ€๊ฒŸ ๋ชจ๋ธ๋“ค ๋Œ€๋น„ ํ›Œ๋ฅญํ•œ ๊ฒฝ์ œ์  ๋งˆ์ง„์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.

3. ๐Ÿ’ป ์•„ํ‚คํ…์ฒ˜๋ฅผ ๊ด€ํ†ตํ•˜๋Š” ํ•ต์‹ฌ ์ฝ”๋“œ ๋ผ์ธ ์‹œ์—ฐ

์‹ค๋ฌด PPT๋‚˜ ๋ฆฌ๋ทฐ ์‹œ ๋‹ค๋ฃจ๊ธฐ ์ข‹์€ ํ•ต์‹ฌ ์ฝ”๋“œ ์˜์—ญ์ž…๋‹ˆ๋‹ค.

๐ŸŽฏ ํ•ต์‹ฌ ํฌ์ธํŠธ 1: Kafka๋ฅผ ํ™œ์šฉํ•œ ์™„์ „ ๋น„๋™๊ธฐ ์ด๋ฒคํŠธ ๋ถ„์‚ฐ ์ฒ˜๋ฆฌ (EDA)

์‹ค์‹œ๊ฐ„ ํ™”์ƒํšŒ์˜ ๋ฏธ๋””์–ด๋ฅผ ๋ผ์šฐํŒ…ํ•˜๋Š” 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) ํ†ตํ•ฉ์„ ์ด๋ค„๋ƒˆ์Šต๋‹ˆ๋‹ค.

๐ŸŽฏ ํ•ต์‹ฌ ํฌ์ธํŠธ 3: ๋Œ€๊ทœ๋ชจ ์ธํ”„๋ผ ๋ณ€๊ฒฝ ๊ฐ„, 100% ๋ฌด์ค‘๋‹จ ํ˜ธํ™˜์„ฑ (Fallback)

์ €์žฅ์†Œ๊ฐ€ 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% ๋ฌด์ค‘๋‹จ ํ˜ธํ™˜์„ฑ์„ ํ™•๋ณดํ•œ ๊ท€์ค‘ํ•œ ๊ฒฝํ—˜ ํŒจํ„ด์ž…๋‹ˆ๋‹ค.


4. ๐Ÿง  3๋Œ€ ํ•ต์‹ฌ AI ํŒŒ์ดํ”„๋ผ์ธ ๋ชจ๋ธ ๊ตฌํ˜„์ฒด (VAD / STT / LLM)

"AI ํŒŒ์ดํ”„๋ผ์ธ์ด ์‹ค์ œ๋กœ ์ฝ”๋“œ ์ƒ์—์„œ ์–ด๋–ป๊ฒŒ ๋งž๋ฌผ๋ ค ๋Œ์•„๊ฐ€๋Š”๊ฐ€?"์— ๋Œ€ํ•œ 3๋Œ€ ํ•ต์‹ฌ ๋ชจ๋ธ์˜ ์Šค๋‹ˆํŽซ์ž…๋‹ˆ๋‹ค.

โ‘  ์‹ค์‹œ๊ฐ„ ๋ฌด์Œ ํ•„ํ„ฐ๋ง (VAD): SileroVadClient.java

์นจ๋ฌต์„ ๊ฑธ๋Ÿฌ๋‚ด๊ณ  ๊ณผ๊ธˆ์„ ๋ฐฉ์–ดํ•˜๋Š” ์ฒซ ๋ฒˆ์งธ ๊ด€๋ฌธ์ž…๋‹ˆ๋‹ค. ๋ฐฑ์—”๋“œ ๋ฉ”๋ชจ๋ฆฌ์—์„œ ๊ฐ€๋ฒผ์šด 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;
    }
}

โ‘ก ์Œ์„ฑ ์ธ์‹ ์ฒ˜๋ฆฌ๊ธฐ (STT): OpenAiSttClient.java

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();

โ‘ข ์š”์•ฝ ๋ฐ ๊ตฌ์กฐํ™” ํŒŒ์„œ (LLM): ClaudeSummarizerClient.java

๊ฑฐ๋Œ€ํ•œ ๋Œ€๋ณธ ํ…์ŠคํŠธ๋ฅผ ๋˜์ ธ์ฃผ๊ณ , ์šฐ๋ฆฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ์™€ 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); 

5. ๐Ÿ’ก ์ข…ํ•ฉ ๊ฒฐ๋ก  (Takeaway)

์ด๋ฒˆ AI ์„œ๋น„์Šค ๊ตฌ์ถ•์€ ๋‹จ์ˆœํžˆ "OpenAI ์—ฐ๋™"์— ๊ทธ์น˜์ง€ ์•Š๊ณ , ์•„๋ž˜์™€ ๊ฐ™์€ ๊ฐ€์น˜ ์ฐฝ์ถœ์„ ์ด๋ฃจ์–ด๋ƒˆ์Šต๋‹ˆ๋‹ค:

  1. ์ง€๊ฐ‘์„ ์ง€ํ‚จ ์•„ํ‚คํ…์ฒ˜: ์นจ๋ฌต์œผ๋กœ ๋‚˜๊ฐ€๋Š” ๋ฌด์ง€์„ฑ ํด๋ผ์šฐ๋“œ ๊ณผ๊ธˆ์„ ๋กœ์ปฌ VAD Onnx ๋„์ž…๊ณผ PCM ์˜ค๋””์˜ค ์ •๊ทœํ™”๋ฅผ ํ†ตํ•ด ์™„๋ฒฝํžˆ ๋ฐฉ์–ด.
  2. ๋‹ค์ค‘ LLM ์กฐ์œจ ์•™์ƒ๋ธ” ์ฒด๊ณ„: ๋กœ์ปฌ ์ตœ๋‹จ ๋”œ๋ ˆ์ด(VAD) + ์˜ค๋””์˜ค ์ฒญ์ทจ ํŠนํ™”(gpt-4o-mini) + NLP ๊ตฌ์กฐํ™” ํŠนํ™”(Claude) ๋“ฑ ์ƒํ™ฉ์— ๋งž๋Š” ๊ฐ€์žฅ ๋พฐ์กฑํ•˜๊ณ  ์ €๋ ดํ•œ ๋ฌด๊ธฐ 3๊ฐ€์ง€๋ฅผ ์กฐ๋ฆฝํ•ด ๊ฐ€์„ฑ๋น„ ์ตœ์ƒ์˜ ํŒŒ์ดํ”„๋ผ์ธ ๊ฐœ์„ค.
  3. ๊ธฐ์ˆ ๋ถ€์ฑ„ ์—†๋Š” ํŠผํŠผํ•œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜: ์˜›๋‚  ํšŒ์˜์˜ ์›๋ฌธ S3 ์ ‘๊ทผ๊ณผ ์‹ ๊ทœ ์ •๊ทœํ™” DB ์กฐํšŒ๋ฅผ ํ•˜๋‚˜์˜ API(Fallback)๋กœ ํฌ์žฅ์‹œ์ผœ ๊ธฐ์กด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ์˜จ์ „ํžˆ ์ง€์ผฐ์Šต๋‹ˆ๋‹ค.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages