Phase 15.5 구현: 화자 구분 (Speaker Diarization)
- Sidecar: pyannote /diarize 엔드포인트 + requirements.txt 업데이트 - LLM 기반 화자 추정 (Phase 1 — 오디오 보존 없이 전사 텍스트 분석) - CaptionSegment에 speaker 필드 추가 - EditableSegment: 화자별 색상 바 + 화자 Chip 표시 - TranscriptTab: 화자 구분 버튼 + 진행률 - 설정: HuggingFace 토큰 입력 + Diarization 토글 - IPC: DIARIZE + DIARIZATION_PROGRESS 채널 - 에러코드: 895-897 - 12개 locale i18n
This commit is contained in:
parent
da25791c75
commit
fc7628327a
25 changed files with 679 additions and 18 deletions
117
sidecar/main.py
117
sidecar/main.py
|
|
@ -278,6 +278,123 @@ async def transcribe(
|
|||
)
|
||||
|
||||
|
||||
# ── 화자 구분 (Phase 15.5) ────────────────────────────────
|
||||
|
||||
_diarization_pipeline = None
|
||||
|
||||
|
||||
def _get_diarization_pipeline(hf_token: str):
|
||||
"""pyannote speaker diarization 파이프라인을 로드한다 (캐시)."""
|
||||
global _diarization_pipeline
|
||||
if _diarization_pipeline is not None:
|
||||
return _diarization_pipeline
|
||||
|
||||
try:
|
||||
from pyannote.audio import Pipeline
|
||||
|
||||
logger.info("Diarization 파이프라인 로딩 시작")
|
||||
_diarization_pipeline = Pipeline.from_pretrained(
|
||||
"pyannote/speaker-diarization-3.1",
|
||||
use_auth_token=hf_token,
|
||||
)
|
||||
if _gpu_available:
|
||||
import torch
|
||||
_diarization_pipeline.to(torch.device("cuda"))
|
||||
logger.info("Diarization 파이프라인 로딩 완료 (GPU)")
|
||||
else:
|
||||
logger.info("Diarization 파이프라인 로딩 완료 (CPU)")
|
||||
except Exception as exc:
|
||||
logger.error("Diarization 파이프라인 로딩 실패: %s", exc)
|
||||
raise
|
||||
|
||||
return _diarization_pipeline
|
||||
|
||||
|
||||
@app.post("/diarize")
|
||||
async def diarize(
|
||||
audio: UploadFile = File(...),
|
||||
hf_token: str = Form(default=""),
|
||||
num_speakers: int = Form(default=0),
|
||||
) -> JSONResponse:
|
||||
"""오디오 파일의 화자 구분을 수행한다."""
|
||||
if not hf_token:
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={"status": "error", "message": "HuggingFace 토큰이 필요합니다"},
|
||||
)
|
||||
|
||||
start_time = time.monotonic()
|
||||
|
||||
try:
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
pipeline = _get_diarization_pipeline(hf_token)
|
||||
|
||||
# 오디오 파일 임시 저장 (pyannote는 파일 경로 필요)
|
||||
pcm_bytes = await audio.read()
|
||||
if len(pcm_bytes) == 0:
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={"status": "error", "message": "오디오 데이터가 비어있습니다"},
|
||||
)
|
||||
|
||||
# PCM16 → WAV 변환
|
||||
import wave
|
||||
tmp_fd, tmp_path = tempfile.mkstemp(suffix=".wav")
|
||||
try:
|
||||
with wave.open(tmp_path, "wb") as wf:
|
||||
wf.setnchannels(1)
|
||||
wf.setsampwidth(2) # 16-bit
|
||||
wf.setframerate(16000)
|
||||
wf.writeframes(pcm_bytes)
|
||||
|
||||
# diarization 실행
|
||||
diarize_kwargs = {}
|
||||
if num_speakers > 0:
|
||||
diarize_kwargs["num_speakers"] = num_speakers
|
||||
|
||||
logger.info("화자 구분 시작: %.1f초 오디오", len(pcm_bytes) / (16000 * 2))
|
||||
diarization = pipeline(tmp_path, **diarize_kwargs)
|
||||
|
||||
# 결과 파싱
|
||||
segments = []
|
||||
speakers = set()
|
||||
for turn, _, speaker in diarization.itertracks(yield_label=True):
|
||||
segments.append({
|
||||
"speaker": speaker,
|
||||
"start": round(turn.start, 3),
|
||||
"end": round(turn.end, 3),
|
||||
})
|
||||
speakers.add(speaker)
|
||||
|
||||
processing_time = int((time.monotonic() - start_time) * 1000)
|
||||
logger.info(
|
||||
"화자 구분 완료: %d개 세그먼트, %d명 화자, %dms",
|
||||
len(segments),
|
||||
len(speakers),
|
||||
processing_time,
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
"segments": segments,
|
||||
"num_speakers": len(speakers),
|
||||
"processing_time": processing_time,
|
||||
}
|
||||
)
|
||||
finally:
|
||||
os.close(tmp_fd)
|
||||
os.unlink(tmp_path)
|
||||
|
||||
except Exception as exc:
|
||||
logger.error("화자 구분 실패: %s", exc, exc_info=True)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"status": "error", "message": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
@app.post("/shutdown")
|
||||
async def shutdown() -> JSONResponse:
|
||||
"""서버를 graceful하게 종료한다."""
|
||||
|
|
|
|||
|
|
@ -3,3 +3,5 @@ fastapi>=0.109.0
|
|||
uvicorn>=0.27.0
|
||||
python-multipart>=0.0.6
|
||||
numpy>=1.24.0
|
||||
pyannote.audio>=3.3.0
|
||||
torch>=2.0.0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue