LabHub
시작하기
배우기 러닝패스 코스

음성 AI 에이전트 — 듣고, 찾고, 말하는 파이프라인

말을 멈춘 뒤 첫 소리까지 — 직렬과 겹침을 재고 예산과 대조한다

LabHub 에서 이어서 보기

목표

듣기부터 말하기까지를 한 줄로 이어 사용자가 느끼는 지연을 잰다. 직렬과 겹침을 같은 질의로 비교하고, 분포와 예산으로 줄일 곳을 찾는다.

왜 중요한가

부품마다 빠르더라도 이어 붙이면 초 단위가 된다. 어디를 줄일지는 '어떻게 이었는가' 와 '어느 단계가 예산을 넘는가' 를 분포로 봐야 안다. 채점기는 기계마다 다른 절대 시간 대신 기록 안의 순서(확정 ≤ 검색 ≤ 첫 토큰 ≤ LLM 끝), 같은 기계에서 잰 두 방식의 비교(겹침 < 직렬), 원자료에서 다시 계산한 백분위·예산 초과를 봅니다. 끝점 지연(오디오 시간)은 채점기가 VAD 를 다시 돌려 대조합니다.

단계

  1. 질의 12개(/opt/lab/fixtures/voice/queries/q01~q12.wav)의 끝점(VAD 가 본 마지막 말 끝 + gap 0.5초)과 실제 말 끝(refs.tsv 넷째 칸)의 차이를 /root/voice/pipeline/endpoint.json 에 적으세요.
  2. 확정 → 검색 → LLM(전부) → TTS(전부) 를 잇는 /root/voice/pipeline/pipe.py 를 만들어 q01·q03·q05·q07·q09 를 직렬로 돌린 기록을 /root/voice/pipeline/serial.jsonl 에 적으세요.
  3. 같은 다섯 질의를 LLM 조각이 생기는 대로 TTS 에 넘기는 겹침으로 돌려 /root/voice/pipeline/overlap.jsonl 에 적으세요.
  4. 겹친 실행 하나를 크롬 추적 형식으로 /root/voice/pipeline/trace.json 에 그리세요.
  5. 답이 있는 질의 10개(q01~q10)를 겹침으로 돌려 사용자 체감 지연(끝점 지연 + 판정 뒤 첫 소리)과 p50·p95 를 /root/voice/pipeline/e2e.json 에 적으세요.
  6. 단계별 예산과 측정 중앙값, 예산을 넘은 단계를 /root/voice/pipeline/budget.json 에 적으세요.
  7. 파이썬 프로세스의 최대 RSS 와 LLM 서버의 RSS, 파드 메모리 한도를 /root/voice/pipeline/memory.json 에 적으세요.
  8. 측정을 모은 /root/voice/pipeline/report.json 을 만드세요.

참고

끝점 지연 — 오디오 시간으로

/opt/lab/fixtures/voice/queries/refs.tsv(이름·원문·발화 시작·발화 끝) 의 질의 12개마다 silero VAD(threshold 0.5 · min_silence 0.1 · min_speech 0.1)의 마지막 구간 끝을 vad_end_s, 거기에 gap 0.5초를 더한 값을 endpoint_s, (endpoint_s − 발화 끝) × 1000 을 delay_ms 로 /root/voice/pipeline/endpoint.json(gap_s·rows)에 적으세요.

모든 값이 소리 파일 안의 시각입니다 — 기계가 빨라도 줄지 않는 몫이라 따로 잽니다. 실제 말 끝은 합성 음성을 놓은 자리라 정확합니다.

직렬로 잇는다

/root/voice/pipeline/pipe.py 에 run(wav, mode) 를 만드세요. 소리를 스트리밍 ASR 에 100 ms 씩 넣은 뒤(재지 않음) 끝점 순간을 0 으로 두고, 확정(asr_ms) → 군말을 뺀 질의로 문단 top-3 검색(retrieve_ms) → 찾은 문단을 붙인 LLM 스트림(첫 조각 llm_first_ms, 끝 llm_done_ms, max_tokens 60) → TTS 로 이어 첫 소리(first_audio_ms)와 합성 끝(tts_done_ms)을 적어 돌려줍니다. mode 가 'serial' 이면 답을 다 받은 뒤 전체를 한 번에 합성합니다. q01·q03·q05·q07·q09 를 직렬로 돌려 id·text(확정 결과)를 더해 /root/voice/pipeline/serial.jsonl 에 적으세요.

모델(ASR·TTS·임베딩)과 KB 임베딩은 모듈을 불러올 때 한 번만 만듭니다. TTS 는 스레드로 돌리면 다음 단계의 겹침에서 그대로 씁니다. 직렬에서는 첫 소리가 곧 합성 끝입니다.

겹쳐 돌린다

run(wav, 'overlap') 은 LLM 글이 쌓이는 대로 조각(문장 끝 · 다섯 낱말 넘는 쉼표 앞 · 열두 낱말)을 잘라 TTS 스레드에 넘기게 하세요. 같은 다섯 질의를 겹침으로 돌려 /root/voice/pipeline/overlap.jsonl 에 적으세요. 채점: 첫 소리 중앙값이 직렬보다 빨라야 합니다.

직렬과 겹침의 차이는 '언제 TTS 에 넘기는가' 하나뿐이어야 공정하게 비교됩니다. 같은 run 함수에 mode 로 가르세요.

막대로 그린다

overlap.jsonl 의 첫 기록으로 asr_final·retrieve·llm(모두 tid 1)과 tts(tid 2, 첫 토큰부터 합성 끝까지) 막대를 "ph": "X" 사건으로, 첫 소리를 "ph": "i" 사건으로 만든 {"traceEvents": [...]} 를 /root/voice/pipeline/trace.json 에 저장하세요. ts·dur 는 마이크로초입니다.

ms × 1000 = µs. chrome://tracing 이나 ui.perfetto.dev 에 파일을 끌어다 놓으면 LLM 과 TTS 가 겹친 모습이 보입니다.

사용자가 느끼는 지연의 분포

q01~q10 을 겹침으로 돌려 질의마다 endpoint_ms(endpoint.json 의 delay_ms)·first_audio_ms·e2e_ms(둘의 합)를 rows 에 적고, e2e_ms 의 p50_ms·p95_ms(가까운 순위)를 /root/voice/pipeline/e2e.json 에 적으세요.

오디오 시간(끝점 지연)과 벽시계 시간(판정 뒤 첫 소리)을 여기서 처음 더합니다. 표본 10개의 p95 는 가장 큰 값입니다.

예산과 대조한다

budget_ms 에 단계 다섯(endpoint·asr_final·retrieve·llm_first_token·first_audio_after_token)의 예산(양수, 합 2500 이하)을 적고, measured_p50_ms 에 중앙값 — endpoint 는 endpoint.json 의 delay_ms, 나머지는 overlap.jsonl 에서 asr_ms · (retrieve_ms − asr_ms) · (llm_first_ms − retrieve_ms) · (first_audio_ms − llm_first_ms) — 을, over_budget 에 측정이 예산을 넘은 단계 이름을 적은 /root/voice/pipeline/budget.json 을 만드세요.

누적 시각의 차이가 곧 그 단계의 몫입니다. 넘친 단계마다 무엇으로 줄일 수 있는지 — 모델, 프롬프트, 이음새 — 생각해 보세요.

메모리도 예산이다

pipe 로 질의 하나를 돌린 뒤, 이 파이썬 프로세스의 /proc/self/status VmHWM(최대 RSS)을 python_peak_mb, pgrep -f llama-server 로 찾은 서버의 VmRSS 를 llm_rss_mb, 합을 total_mb, /sys/fs/cgroup/memory.max 의 한도를 limit_mb(숫자가 아니면 null)로 /root/voice/pipeline/memory.json 에 적으세요.

VmHWM·VmRSS 는 kB 로 나옵니다 — 1024 로 나눠 MB 로. 한도는 바이트입니다. 두 프로세스가 한도의 몇 %를 쓰는지가 모델을 하나 더 올릴 수 있는지의 답입니다.

보고서

/root/voice/pipeline/report.json 에 serial_first_audio_p50_ms·overlap_first_audio_p50_ms(두 jsonl 의 first_audio_ms 중앙값), e2e_p50_ms·e2e_p95_ms, over_budget, total_mb 를 적으세요.

앞 단계 파일에서 계산하거나 옮깁니다.